From d96244946b89ff3232d99c157b4591ae39e39cb3 Mon Sep 17 00:00:00 2001 From: WH-2099 Date: Fri, 28 Aug 2026 21:43:23 +0000 Subject: [PATCH 1/2] feat(runtime): project agent task snapshots --- .../adapters/graphql/schema.generated.graphql | 13 + .../adapters/graphql/schema/session-schema.ts | 13 + .../driver-instance/event-link-assertion.ts | 16 +- .../rpc-event-ingestion-controller.ts | 5 +- .../session-viewer-event-delivery-buffer.ts | 229 +- .../agent-session-retrieve.service.ts | 35 +- .../session-runtime-event-projection.ts | 12 +- .../session-agent-task-snapshot.repository.ts | 134 + .../session-runtime-event-store.repository.ts | 81 +- .../session-runtime-event-store.types.ts | 2 + ...session-viewer-live-snapshot.repository.ts | 54 +- .../session/viewer-socket-hub.ts | 124 +- apps/api/src/platform/db/drizzle.ts | 2 + apps/api/tests/agent-session-retrieve.test.ts | 112 + apps/api/tests/api-driver-boundary.test.ts | 30 + .../tests/driver-finalization-repair.test.ts | 19 +- .../public-api-http-runtime-schema.sql | 10 + apps/api/tests/session-process-events.test.ts | 11 +- .../tests/session-runtime-event-store.test.ts | 315 +- ...ssion-viewer-event-delivery-buffer.test.ts | 232 +- .../session-viewer-socket-state-order.test.ts | 303 + apps/api/tests/session-viewer-state.test.ts | 96 + apps/driver | 2 +- .../session-stream/session-stream-actions.ts | 3 + .../session/api/agent-session-retrieve.ts | 8 + apps/web/src/gql/gql.ts | 6 +- apps/web/src/gql/graphql.ts | 10 +- .../agent-session-panel-model-types.ts | 3 +- .../agent/components/agent-session-panel.tsx | 3 + .../use-agent-session-panel-model.ts | 1 + apps/web/src/routes/threads/controller.tsx | 12 +- apps/web/src/routes/threads/detail/view.tsx | 4 + apps/web/src/routes/threads/model/process.ts | 10 + .../routes/threads/process-modal/modal.tsx | 7 +- apps/web/src/shared/i18n/translations/en.json | 2 + apps/web/src/shared/i18n/translations/ja.json | 2 + .../src/shared/i18n/translations/zh-CN.json | 2 + .../src/shared/i18n/translations/zh-TW.json | 2 + .../ui/session-events/active-agent-tasks.tsx | 68 + .../web/src/shared/ui/session-events/index.ts | 2 + apps/web/tests/active-agent-tasks.test.tsx | 43 + apps/web/tests/thread-process-model.test.ts | 37 +- bun.lock | 396 +- .../src/ag-ui-session-compaction.ts | 25 +- .../ag-ui-session/src/ag-ui-session-events.ts | 2 + .../src/custom-event-registry.ts | 14 +- pkgs/ag-ui-session/src/custom-event-schema.ts | 7 + pkgs/ag-ui-session/src/custom-event-values.ts | 5 + .../src/live-state-custom.reducer.ts | 47 +- .../src/live-state.reducer-core.ts | 1 + pkgs/ag-ui-session/src/live-state.reducer.ts | 15 + pkgs/ag-ui-session/src/live-state.ts | 4 + .../src/session-live-state-schema.ts | 3 + .../tests/ag-ui-session-codec.test.ts | 43 + .../tests/ag-ui-session-compaction.test.ts | 48 + .../tests/live-state.reducer.test.ts | 332 + .../contracts/src/session/session.contract.ts | 102 + .../0012_agent-task-snapshot-state.sql | 9 + pkgs/db/drizzle/meta/0012_snapshot.json | 6596 +++++++++++++++++ pkgs/db/drizzle/meta/_journal.json | 7 + pkgs/db/src/schema/session/events.schema.ts | 13 + pkgs/runtime-events/src/process-draft.ts | 8 + .../src/runtime-event-payload.ts | 49 + pkgs/runtime-events/src/runtime-event.ts | 12 + .../src/session-event-projection.ts | 10 + .../tests/ag-ui-adapter.test.ts | 47 + .../tests/runtime-event-ingress.test.ts | 78 + scripts/check-driver-submodule-cutover.ts | 51 +- 68 files changed, 9546 insertions(+), 453 deletions(-) create mode 100644 apps/api/src/modules/sessions/infrastructure/session-agent-task-snapshot.repository.ts create mode 100644 apps/api/tests/session-viewer-socket-state-order.test.ts create mode 100644 apps/web/src/shared/ui/session-events/active-agent-tasks.tsx create mode 100644 apps/web/tests/active-agent-tasks.test.tsx create mode 100644 pkgs/db/drizzle/0012_agent-task-snapshot-state.sql create mode 100644 pkgs/db/drizzle/meta/0012_snapshot.json diff --git a/apps/api/src/adapters/graphql/schema.generated.graphql b/apps/api/src/adapters/graphql/schema.generated.graphql index bf4b9281..2f6d1c6a 100644 --- a/apps/api/src/adapters/graphql/schema.generated.graphql +++ b/apps/api/src/adapters/graphql/schema.generated.graphql @@ -379,6 +379,7 @@ type AgentSessionRetrieve { capabilities: [AgentSessionActionCapability!]! recoverability: AgentSessionRecoverability! session: Session! + taskSnapshot: AgentTaskSnapshot } type AgentSessionRetrieveConnection { @@ -419,6 +420,18 @@ type AgentSummary { visibility: AgentVisibility! } +type AgentTask { + taskId: String! + taskType: String + title: String +} + +type AgentTaskSnapshot { + driverInstanceId: ULID! + runId: ULID! + tasks: [AgentTask!]! +} + type AgentToolSummary { enabled: Boolean! iconUrl: String diff --git a/apps/api/src/adapters/graphql/schema/session-schema.ts b/apps/api/src/adapters/graphql/schema/session-schema.ts index 72d67df1..eabf43f9 100644 --- a/apps/api/src/adapters/graphql/schema/session-schema.ts +++ b/apps/api/src/adapters/graphql/schema/session-schema.ts @@ -287,10 +287,23 @@ export const sessionSchema = /* GraphQL */ ` status: AgentSessionActionCapabilityStatus! } + type AgentTask { + taskId: String! + taskType: String + title: String + } + + type AgentTaskSnapshot { + driverInstanceId: ULID! + runId: ULID! + tasks: [AgentTask!]! + } + type AgentSessionRetrieve { capabilities: [AgentSessionActionCapability!]! recoverability: AgentSessionRecoverability! session: Session! + taskSnapshot: AgentTaskSnapshot } type AgentSessionRetrieveConnection { diff --git a/apps/api/src/modules/runtime/infrastructure/driver-instance/event-link-assertion.ts b/apps/api/src/modules/runtime/infrastructure/driver-instance/event-link-assertion.ts index 8917df0c..b8abef21 100644 --- a/apps/api/src/modules/runtime/infrastructure/driver-instance/event-link-assertion.ts +++ b/apps/api/src/modules/runtime/infrastructure/driver-instance/event-link-assertion.ts @@ -1,8 +1,11 @@ -import type { RuntimeEventEnvelope } from "@mosoo/runtime-events"; -import type { RuntimeEventKind } from "@mosoo/runtime-events"; +import { readRuntimeAgentTaskSnapshot } from "@mosoo/runtime-events"; +import type { RuntimeEventEnvelope, RuntimeEventKind } from "@mosoo/runtime-events"; +import { ACTIVE_SESSION_RUN_STATUSES } from "../../domain/session-run-lifecycle.machine"; import type { RuntimeSessionLink } from "./event-types"; +const activeSessionRunStatuses = new Set(ACTIVE_SESSION_RUN_STATUSES); + const runBoundRuntimeEventDomains = new Set([ "image", "item", @@ -16,6 +19,7 @@ const runBoundRuntimeEventDomains = new Set([ ]); const runBoundRuntimeEventKinds = new Set([ + "agent.tasks.replaced", "file.change.updated", "mcp.tool.updated", "permission.requested", @@ -56,6 +60,14 @@ export function assertRuntimeEventMatchesDriverLink( if (event.runId !== input.link.sessionRunId) { throw new Error("Runtime driver event run id does not match the driver session link."); } + + if ( + event.kind === "agent.tasks.replaced" && + !activeSessionRunStatuses.has(input.link.sessionRunStatus ?? "") && + readRuntimeAgentTaskSnapshot(event).tasks.length > 0 + ) { + throw new Error("Runtime agent task snapshot requires an active session run."); + } } export function assertRuntimeEventMatchesDriverEnvelope( diff --git a/apps/api/src/modules/runtime/infrastructure/driver-instance/rpc-event-ingestion-controller.ts b/apps/api/src/modules/runtime/infrastructure/driver-instance/rpc-event-ingestion-controller.ts index c226a97f..9c91b197 100644 --- a/apps/api/src/modules/runtime/infrastructure/driver-instance/rpc-event-ingestion-controller.ts +++ b/apps/api/src/modules/runtime/infrastructure/driver-instance/rpc-event-ingestion-controller.ts @@ -107,7 +107,10 @@ export class DriverInstanceRpcEventIngestionController { const cachedLink = state.runtimeSessionLink; const eventSessionRunId = resolveEventSessionRunId(input.events); const shouldRefreshLink = - input.events.some((envelope) => envelope.event.kind === "run.started") || + input.events.some( + (envelope) => + envelope.event.kind === "run.started" || envelope.event.kind === "agent.tasks.replaced", + ) || runtimeSessionLinkNeedsRefresh(cachedLink) || (eventSessionRunId !== undefined && cachedLink?.sessionRunId !== eventSessionRunId); const link = await this.#getRuntimeSessionLink({ diff --git a/apps/api/src/modules/runtime/infrastructure/driver-instance/session-viewer-event-delivery-buffer.ts b/apps/api/src/modules/runtime/infrastructure/driver-instance/session-viewer-event-delivery-buffer.ts index 3ed2826e..485bf42b 100644 --- a/apps/api/src/modules/runtime/infrastructure/driver-instance/session-viewer-event-delivery-buffer.ts +++ b/apps/api/src/modules/runtime/infrastructure/driver-instance/session-viewer-event-delivery-buffer.ts @@ -5,7 +5,6 @@ import { isAgUiSessionRunStartedEvent, isAgUiSessionRunTerminalEvent, } from "@mosoo/ag-ui-session"; -import { discardPromiseResult, ignorePromiseRejection } from "@mosoo/effects"; import type { DriverInstanceId, SessionId } from "@mosoo/id"; import { createErrorLogContext, logError } from "../../../../platform/cloudflare/logger"; @@ -16,6 +15,8 @@ import { publishSessionViewerEvents } from "../../../sessions/application/sessio const SESSION_VIEWER_EVENT_DELIVERY_FLUSH_MS = 150; const SESSION_VIEWER_EVENT_DELIVERY_MAX_DELTA_BYTES = 4 * 1024; const SESSION_VIEWER_EVENT_DELIVERY_MAX_EVENTS = 64; +const SESSION_VIEWER_EVENT_DELIVERY_MAX_SERIALIZED_BYTES = 1_536 * 1024; +const sessionViewerEventEncoder = new TextEncoder(); interface BufferedSessionViewerEvents { deltaBytes: number; @@ -42,16 +43,17 @@ function hasDeltaEvent(events: SessionDeliveryEvent[]): boolean { return events.some((event) => getAgUiSessionEventDeltaLength(event) > 0); } -function estimateDeltaBytes(events: SessionDeliveryEvent[]): number { - return events.reduce((bytes, event) => bytes + getAgUiSessionEventDeltaLength(event), 0); +function measureSerializedBytes(events: SessionDeliveryEvent[]): number { + return sessionViewerEventEncoder.encode(JSON.stringify(events)).byteLength; } export class SessionViewerEventDeliveryBuffer { #buffer: BufferedSessionViewerEvents | null = null; + #delivery: Promise | null = null; #pendingFirstDelta = false; + readonly #pendingBatches: BufferedSessionViewerEvents[] = []; readonly #ctx: DurableObjectState; readonly #env: ApiBindings; - #gate: Promise = Promise.resolve(); readonly #getDriverInstanceId: () => DriverInstanceId | null; #timer: ReturnType | null = null; readonly #withRuntimeLogContext: (fn: () => T) => T; @@ -70,18 +72,9 @@ export class SessionViewerEventDeliveryBuffer { return; } - const buffered = this.#buffer; - const incomingDeltaBytes = estimateDeltaBytes(compactedEvents); - const nextEvents = buffered - ? appendCompactedAgUiSessionEvents(buffered.events, compactedEvents) - : compactedEvents; - const nextDeltaBytes = (buffered?.deltaBytes ?? 0) + incomingDeltaBytes; - - this.#buffer = { - deltaBytes: nextDeltaBytes, - events: nextEvents, - sessionId: buffered?.sessionId ?? sessionId, - }; + for (const event of compactedEvents) { + this.#appendEvent(sessionId, event); + } // O2: cut first-token latency. Arm on RUN_STARTED, then flush the first // delta of the run immediately instead of waiting out the 150ms timer. @@ -95,98 +88,174 @@ export class SessionViewerEventDeliveryBuffer { this.#pendingFirstDelta = false; } - if ( - nextEvents.length >= SESSION_VIEWER_EVENT_DELIVERY_MAX_EVENTS || - nextDeltaBytes >= SESSION_VIEWER_EVENT_DELIVERY_MAX_DELTA_BYTES || - isFirstDeltaOfRun || - hasTerminalEvent(compactedEvents) - ) { + if (isFirstDeltaOfRun || hasTerminalEvent(compactedEvents)) { this.#startFlush(); return; } - this.#scheduleFlush(); + if (this.#buffer) { + this.#scheduleFlush(); + } } async flush(): Promise { - if (this.#timer !== null) { - clearTimeout(this.#timer); - this.#timer = null; + this.#clearTimer(); + this.#queueBuffer(); + await this.#getOrStartDelivery(); + } + + async #deliverPendingBatches(): Promise { + try { + while (this.#pendingBatches.length > 0) { + const batch = this.#pendingBatches.shift(); + + if (!batch) { + return; + } + + try { + await publishSessionViewerEvents(this.#env, batch.sessionId, batch.events); + } catch (error) { + this.#pendingBatches.unshift(batch); + throw error; + } + } + } finally { + this.#delivery = null; } + } - const buffered = this.#buffer; + async flushSafely(): Promise { + try { + await this.flush(); + } catch (error) { + this.#reportDeliveryError(error); - if (!buffered) { - await this.#gate; - return; + if (this.#hasBufferedEvents() && this.#timer === null) { + this.#scheduleFlush(); + } } + } + resetAfterFlush(): void { this.#buffer = null; + this.#pendingFirstDelta = false; + this.#pendingBatches.length = 0; - const task = this.#deliverAfterGate(buffered); + this.#clearTimer(); + } - this.#gate = SessionViewerEventDeliveryBuffer.#discardDeliveryResult(task); + #appendEvent(sessionId: SessionId | null, event: SessionDeliveryEvent): void { + let buffered = this.#buffer; + let events = buffered ? appendCompactedAgUiSessionEvents(buffered.events, [event]) : [event]; + let serializedBytes = measureSerializedBytes(events); - try { - await task; - } catch (error) { - const currentBuffer = this.#getBuffer(); - this.#buffer = { - deltaBytes: buffered.deltaBytes + (currentBuffer?.deltaBytes ?? 0), - events: currentBuffer - ? appendCompactedAgUiSessionEvents(buffered.events, currentBuffer.events) - : buffered.events, - sessionId: buffered.sessionId ?? currentBuffer?.sessionId ?? null, - }; - throw error; + if (buffered && serializedBytes > SESSION_VIEWER_EVENT_DELIVERY_MAX_SERIALIZED_BYTES) { + this.#startFlush(); + buffered = null; + events = [event]; + serializedBytes = measureSerializedBytes(events); } - } - async #deliverAfterGate(buffered: BufferedSessionViewerEvents): Promise { - try { - await this.#gate; - } catch (error) { - ignorePromiseRejection(error); + this.#buffer = { + deltaBytes: (buffered?.deltaBytes ?? 0) + getAgUiSessionEventDeltaLength(event), + events, + sessionId: buffered?.sessionId ?? sessionId, + }; + + if ( + events.length >= SESSION_VIEWER_EVENT_DELIVERY_MAX_EVENTS || + this.#buffer.deltaBytes >= SESSION_VIEWER_EVENT_DELIVERY_MAX_DELTA_BYTES || + serializedBytes >= SESSION_VIEWER_EVENT_DELIVERY_MAX_SERIALIZED_BYTES + ) { + this.#startFlush(); } + } - await publishSessionViewerEvents(this.#env, buffered.sessionId, buffered.events); + #clearTimer(): void { + if (this.#timer !== null) { + clearTimeout(this.#timer); + this.#timer = null; + } } - static async #discardDeliveryResult(task: Promise): Promise { - try { - await task; - } catch (error) { - ignorePromiseRejection(error); + #getOrStartDelivery(): Promise { + if (this.#delivery) { + this.#compactPendingBatches(); + return this.#delivery; + } + + if (this.#pendingBatches.length === 0) { + return Promise.resolve(); } - discardPromiseResult(); + this.#compactPendingBatches(); + const delivery = this.#deliverPendingBatches(); + this.#delivery = delivery; + return delivery; } - async flushSafely(): Promise { - try { - await this.flush(); - } catch (error) { - this.#reportDeliveryError(error); + #compactPendingBatches(): void { + if (this.#pendingBatches.length < 2) { + return; + } - if (this.#buffer && this.#timer === null) { - this.#scheduleFlush(); + const queued = this.#pendingBatches.splice(0); + let events: SessionDeliveryEvent[] = []; + let sessionId = queued[0]?.sessionId ?? null; + + for (const batch of queued) { + if (batch.sessionId !== sessionId) { + this.#queueCompactedEvents(sessionId, events); + events = []; + sessionId = batch.sessionId; } + + events = appendCompactedAgUiSessionEvents(events, batch.events); } + + this.#queueCompactedEvents(sessionId, events); } - resetAfterFlush(): void { - this.#buffer = null; - this.#pendingFirstDelta = false; - this.#gate = Promise.resolve(); + #queueCompactedEvents(sessionId: SessionId | null, events: SessionDeliveryEvent[]): void { + for (const event of events) { + const previous = this.#pendingBatches.at(-1); + const nextEvents = previous + ? appendCompactedAgUiSessionEvents(previous.events, [event]) + : [event]; + const nextDeltaBytes = (previous?.deltaBytes ?? 0) + getAgUiSessionEventDeltaLength(event); + + if ( + previous && + previous.sessionId === sessionId && + nextEvents.length <= SESSION_VIEWER_EVENT_DELIVERY_MAX_EVENTS && + nextDeltaBytes <= SESSION_VIEWER_EVENT_DELIVERY_MAX_DELTA_BYTES && + measureSerializedBytes(nextEvents) <= SESSION_VIEWER_EVENT_DELIVERY_MAX_SERIALIZED_BYTES + ) { + previous.deltaBytes = nextDeltaBytes; + previous.events = nextEvents; + continue; + } - if (this.#timer !== null) { - clearTimeout(this.#timer); - this.#timer = null; + this.#pendingBatches.push({ + deltaBytes: getAgUiSessionEventDeltaLength(event), + events: [event], + sessionId, + }); } } - #getBuffer(): BufferedSessionViewerEvents | null { - return this.#buffer; + #hasBufferedEvents(): boolean { + return this.#buffer !== null || this.#pendingBatches.length > 0; + } + + #queueBuffer(): void { + if (!this.#buffer) { + return; + } + + this.#pendingBatches.push(this.#buffer); + this.#buffer = null; } #scheduleFlush(): void { @@ -201,6 +270,18 @@ export class SessionViewerEventDeliveryBuffer { } #startFlush(): void { + this.#clearTimer(); + this.#queueBuffer(); + + if (this.#delivery) { + this.#compactPendingBatches(); + return; + } + + if (this.#pendingBatches.length === 0) { + return; + } + const task = this.#flushAndReportDeliveryErrors(); this.#ctx.waitUntil(task); @@ -212,7 +293,7 @@ export class SessionViewerEventDeliveryBuffer { } catch (error) { this.#reportDeliveryError(error); - if (this.#buffer && this.#timer === null) { + if (this.#hasBufferedEvents() && this.#timer === null) { this.#scheduleFlush(); } } diff --git a/apps/api/src/modules/sessions/application/agent-session-retrieve.service.ts b/apps/api/src/modules/sessions/application/agent-session-retrieve.service.ts index 9fbcc12c..f991f428 100644 --- a/apps/api/src/modules/sessions/application/agent-session-retrieve.service.ts +++ b/apps/api/src/modules/sessions/application/agent-session-retrieve.service.ts @@ -4,6 +4,7 @@ import type { AgentSessionNativeRuntimeRefDiagnostics, AgentSessionRecoverability, AgentSessionRetrieveResult, + AgentTaskSnapshot, SessionExecutionSkillReference, SessionExecutionToolReference, SessionSummary, @@ -17,6 +18,7 @@ import { eq } from "drizzle-orm"; import { getAppDatabase } from "../../../platform/db/drizzle"; import type { AuthenticatedViewer } from "../../auth/application/viewer-auth.service"; import { findSessionExecutionPlan } from "../../runtime/application/session-definition/session-execution.repository"; +import { loadSessionAgentTaskSnapshot } from "../infrastructure/session-agent-task-snapshot.repository"; import { loadSessionViewerState } from "./session-live-state.service"; import { getSessionSummaryAccessById, @@ -68,8 +70,12 @@ export async function retrieveAgentSession( input: AgentSessionLookupInput, ): Promise { const access = await getSessionSummaryAccessById(database, viewer.id, input); + const taskSnapshot = selectCurrentAgentTaskSnapshot( + access.session, + await loadSessionAgentTaskSnapshot(database, input.sessionId), + ); - return toAgentSessionRetrieveResult(access); + return toAgentSessionRetrieveResult({ ...access, taskSnapshot }); } export async function retrieveThreadAgentSession( @@ -78,16 +84,42 @@ export async function retrieveThreadAgentSession( input: AgentSessionLookupInput, ): Promise { const session = await getSessionSummaryForCreator(database, viewer.id, input); + const taskSnapshot = selectCurrentAgentTaskSnapshot( + session, + await loadSessionAgentTaskSnapshot(database, input.sessionId), + ); return toAgentSessionRetrieveResult({ isSessionCreator: true, session, + taskSnapshot, }); } +function selectCurrentAgentTaskSnapshot( + session: SessionSummary, + snapshot: AgentTaskSnapshot | null, +): AgentTaskSnapshot | null { + if ( + snapshot === null || + session.archivedAt !== null || + session.status !== "RUNNING" || + session.lastRun?.id !== snapshot.runId || + session.lastRun.status === "cancelled" || + session.lastRun.status === "completed" || + session.lastRun.status === "expired" || + session.lastRun.status === "failed" + ) { + return null; + } + + return snapshot; +} + export function toAgentSessionRetrieveResult(input: { isSessionCreator: boolean; session: SessionSummary; + taskSnapshot?: AgentTaskSnapshot | null; }): AgentSessionRetrieveResult { return { capabilities: getAgentSessionActionCapabilities({ @@ -96,6 +128,7 @@ export function toAgentSessionRetrieveResult(input: { }), recoverability: getAgentSessionRecoverability(input.session), session: input.session, + taskSnapshot: input.taskSnapshot ?? null, }; } diff --git a/apps/api/src/modules/sessions/domain/session-runtime-event-projection.ts b/apps/api/src/modules/sessions/domain/session-runtime-event-projection.ts index 56daa299..4631806d 100644 --- a/apps/api/src/modules/sessions/domain/session-runtime-event-projection.ts +++ b/apps/api/src/modules/sessions/domain/session-runtime-event-projection.ts @@ -142,6 +142,14 @@ function readProjectedProcessStatus( return draft.status ?? "available"; } +function readProjectedVisibility(event: RuntimeEventEnvelope): SessionRuntimeEventVisibility { + // The canonical participant event still drives live AG-UI delivery. Its + // durable row is a state receipt, not a historical Process timeline entry. + return event.kind === "agent.tasks.replaced" + ? "owner_debug" + : getRuntimeEventParticipantVisibility(event); +} + export function createSessionRuntimeEventProjection( event: RuntimeEventEnvelope, ): SessionRuntimeEventProjection { @@ -152,7 +160,7 @@ export function createSessionRuntimeEventProjection( return { contentText: readProjectedContentText(event, draft), eventType: event.kind, - family: getRuntimeEventSessionFamily(event), + family: event.kind === "agent.tasks.replaced" ? "state" : getRuntimeEventSessionFamily(event), processStatus: readProjectedProcessStatus(event, draft), processType: draft.type, runId: event.runId ?? null, @@ -160,6 +168,6 @@ export function createSessionRuntimeEventProjection( ...toolCall, traceId: event.traceId ?? null, tokens: draft.tokens ?? null, - visibility: getRuntimeEventParticipantVisibility(event), + visibility: readProjectedVisibility(event), }; } diff --git a/apps/api/src/modules/sessions/infrastructure/session-agent-task-snapshot.repository.ts b/apps/api/src/modules/sessions/infrastructure/session-agent-task-snapshot.repository.ts new file mode 100644 index 00000000..d9c7b874 --- /dev/null +++ b/apps/api/src/modules/sessions/infrastructure/session-agent-task-snapshot.repository.ts @@ -0,0 +1,134 @@ +import { AgentTaskSnapshot, AgentTasksReplacedPayload } from "@mosoo/contracts/session"; +import { parseSchemaValue } from "@mosoo/contracts/validation"; +import { sessionAgentTaskSnapshotsTable, sessionRunsTable, sessionsTable } from "@mosoo/db"; +import type { RuntimeEventId, SessionId } from "@mosoo/id"; +import { and, eq, inArray, isNull } from "drizzle-orm"; + +import { createErrorLogContext, logWarn } from "../../../platform/cloudflare/logger"; +import { getAppDatabase } from "../../../platform/db/drizzle"; +import { ACTIVE_SESSION_RUN_STATUSES } from "../../runtime/domain/session-run-lifecycle.machine"; + +export function prepareSessionAgentTaskSnapshotUpsert( + database: D1Database, + input: { + eventId: RuntimeEventId; + snapshot: AgentTaskSnapshot; + }, +): D1PreparedStatement { + return database + .prepare( + `INSERT INTO session_agent_task_snapshot ( + session_id, + run_id, + driver_instance_id, + seq, + tasks_json + ) + SELECT + receipt.session_id, + receipt.run_id, + current_run.driver_instance_id, + receipt.seq, + ? + FROM session_event AS receipt + JOIN session AS current_session + ON current_session.id = receipt.session_id + JOIN session_run AS current_run + ON current_run.id = receipt.run_id + AND current_run.session_id = receipt.session_id + WHERE receipt.id = ? + AND receipt.event_type = 'agent.tasks.replaced' + AND receipt.run_id = ? + AND current_session.last_run_id = receipt.run_id + AND current_session.archived_at IS NULL + AND current_session.status = 'RUNNING' + AND current_run.driver_instance_id = ? + AND current_run.status IN ('queued', 'booting', 'running', 'waiting_input') + ON CONFLICT (session_id) DO UPDATE SET + run_id = excluded.run_id, + driver_instance_id = excluded.driver_instance_id, + seq = excluded.seq, + tasks_json = excluded.tasks_json + WHERE excluded.seq > session_agent_task_snapshot.seq`, + ) + .bind( + JSON.stringify({ tasks: input.snapshot.tasks }), + input.eventId, + input.snapshot.runId, + input.snapshot.driverInstanceId, + ); +} + +export function parseStoredAgentTaskSnapshot(input: { + driverInstanceId: string; + runId: string; + sessionId: string; + tasksJson: string; +}): AgentTaskSnapshot | null { + try { + const payload = parseSchemaValue(AgentTasksReplacedPayload, JSON.parse(input.tasksJson)); + + return parseSchemaValue(AgentTaskSnapshot, { + driverInstanceId: input.driverInstanceId, + runId: input.runId, + tasks: payload.tasks, + }); + } catch (error) { + logWarn("session.agent_task_snapshot.invalid", { + ...createErrorLogContext(error), + runId: input.runId, + sessionId: input.sessionId, + }); + return null; + } +} + +export async function loadSessionAgentTaskSnapshot( + database: D1Database, + sessionId: SessionId, +): Promise { + const row = + (await getAppDatabase(database) + .select({ + driverInstanceId: sessionAgentTaskSnapshotsTable.driverInstanceId, + runId: sessionAgentTaskSnapshotsTable.runId, + tasksJson: sessionAgentTaskSnapshotsTable.tasksJson, + }) + .from(sessionAgentTaskSnapshotsTable) + .innerJoin( + sessionsTable, + and( + eq(sessionsTable.id, sessionAgentTaskSnapshotsTable.sessionId), + eq(sessionsTable.lastRunId, sessionAgentTaskSnapshotsTable.runId), + ), + ) + .innerJoin( + sessionRunsTable, + and( + eq(sessionRunsTable.id, sessionAgentTaskSnapshotsTable.runId), + eq(sessionRunsTable.sessionId, sessionAgentTaskSnapshotsTable.sessionId), + eq(sessionRunsTable.driverInstanceId, sessionAgentTaskSnapshotsTable.driverInstanceId), + ), + ) + .where( + and( + eq(sessionAgentTaskSnapshotsTable.sessionId, sessionId), + isNull(sessionsTable.archivedAt), + eq(sessionsTable.status, "RUNNING"), + inArray(sessionRunsTable.status, ACTIVE_SESSION_RUN_STATUSES), + ), + ) + .limit(1) + .get()) ?? null; + + if (row === null) { + return null; + } + + return parseStoredAgentTaskSnapshot({ + driverInstanceId: row.driverInstanceId, + runId: row.runId, + sessionId, + tasksJson: row.tasksJson, + }); +} diff --git a/apps/api/src/modules/sessions/infrastructure/session-runtime-event-store.repository.ts b/apps/api/src/modules/sessions/infrastructure/session-runtime-event-store.repository.ts index 13232410..86dd1707 100644 --- a/apps/api/src/modules/sessions/infrastructure/session-runtime-event-store.repository.ts +++ b/apps/api/src/modules/sessions/infrastructure/session-runtime-event-store.repository.ts @@ -1,11 +1,14 @@ import { sessionEventsTable, sessionRunsTable, sessionsTable } from "@mosoo/db"; import { createPlatformId } from "@mosoo/id"; import type { RuntimeEventId, SessionId, SessionRunId } from "@mosoo/id"; +import { readRuntimeAgentTaskSnapshot } from "@mosoo/runtime-events"; import { and, eq, inArray, isNull, sql } from "drizzle-orm"; import { getAppDatabase } from "../../../platform/db/drizzle"; +import type { AppDatabase } from "../../../platform/db/drizzle"; import { currentTimestampMs } from "../../../time"; import { createSessionRuntimeEventProjection } from "../domain/session-runtime-event-projection"; +import { prepareSessionAgentTaskSnapshotUpsert } from "./session-agent-task-snapshot.repository"; import type { InsertSessionEventResult, OneRuntimeEventPerSessionAllocation, @@ -35,7 +38,7 @@ export type { } from "./session-runtime-event-store.types"; const MAX_SESSION_RUNTIME_EVENT_INSERT_ATTEMPTS = 5; -// D1 accepts at most 100 bound parameters; each session_event row binds 21. +// D1 accepts at most 100 bound parameters; each fenced session_event row binds 22. const MAX_SESSION_EVENT_ROWS_PER_INSERT = 4; const WRITABLE_SESSION_STATUSES = ["IDLE", "RUNNING", "RESCHEDULING"] as const; const TERMINAL_LIFECYCLE_WRITABLE_SESSION_STATUSES = [ @@ -109,6 +112,10 @@ function readRuntimeEventEndedAt(event: SessionRuntimeEventRecord, fallbackMs: n return Number.isFinite(endedAt) && endedAt >= fallbackMs ? endedAt : fallbackMs; } +function selectedValue(value: T, alias: string) { + return sql`${value}`.as(alias); +} + async function allocateSessionRuntimeEventBatch( database: D1Database, input: { @@ -423,11 +430,25 @@ async function insertSessionEventRows( const appDatabase = getAppDatabase(database); const statements: D1PreparedStatement[] = []; + const receiptStatementIndexes: number[] = []; for (let index = 0; index < values.length; index += MAX_SESSION_EVENT_ROWS_PER_INSERT) { + const chunk = values.slice(index, index + MAX_SESSION_EVENT_ROWS_PER_INSERT); + const firstValue = chunk[0]; + + if (firstValue === undefined) { + continue; + } + + const selection = createSessionEventInsertSelect(appDatabase, firstValue); + + for (const value of chunk.slice(1)) { + selection.unionAll(createSessionEventInsertSelect(appDatabase, value)); + } + const query = appDatabase .insert(sessionEventsTable) - .values(values.slice(index, index + MAX_SESSION_EVENT_ROWS_PER_INSERT)) + .select(selection) .onConflictDoNothing({ target: [sessionEventsTable.sessionId, sessionEventsTable.sourceEventId], }) @@ -437,18 +458,32 @@ async function insertSessionEventRows( }) .toSQL(); + receiptStatementIndexes.push(statements.length); statements.push(database.prepare(query.sql).bind(...query.params)); + + for (const value of chunk) { + if (value.agentTaskSnapshot !== null) { + statements.push( + prepareSessionAgentTaskSnapshotUpsert(database, { + eventId: value.id, + snapshot: value.agentTaskSnapshot, + }), + ); + } + } } const results = await database.batch<{ session_id: SessionId; source_event_id: string }>( statements, ); - const insertedRows = results.flatMap((result) => - result.results.map((row) => ({ + const insertedRows = receiptStatementIndexes.flatMap((resultIndex) => { + const result = results[resultIndex]; + + return (result?.results ?? []).map((row) => ({ sessionId: row.session_id, sourceEventId: row.source_event_id, - })), - ); + })); + }); return { insertedCount: insertedRows.length, @@ -548,6 +583,10 @@ function toSessionRuntimeEventInsertValue(input: { const occurredAt = input.row.occurredAt ?? input.timestampMs + input.sourceIndex; return { + agentTaskSnapshot: + input.row.event.kind === "agent.tasks.replaced" + ? readRuntimeAgentTaskSnapshot(input.row.event) + : null, agentId: input.allocation.agentId, contentText: input.row.projection.contentText, createdAt: input.timestampMs + input.sourceIndex, @@ -572,6 +611,36 @@ function toSessionRuntimeEventInsertValue(input: { }; } +function createSessionEventInsertSelect(database: AppDatabase, value: SessionEventInsertValue) { + return database + .select({ + agentId: selectedValue(value.agentId, "agent_id"), + contentText: selectedValue(value.contentText, "content_text"), + createdAt: selectedValue(value.createdAt, "created_at"), + endedAt: selectedValue(value.endedAt, "ended_at"), + eventType: selectedValue(value.eventType, "event_type"), + family: selectedValue(value.family, "family"), + id: selectedValue(value.id, "id"), + occurredAt: selectedValue(value.occurredAt, "occurred_at"), + processStatus: selectedValue(value.processStatus, "process_status"), + processType: selectedValue(value.processType, "process_type"), + runId: selectedValue(value.runId, "run_id"), + seq: selectedValue(value.seq, "seq"), + sessionId: selectedValue(value.sessionId, "session_id"), + sourceEventId: selectedValue(value.sourceEventId, "source_event_id"), + source: selectedValue(value.source, "source"), + toolCallId: selectedValue(value.toolCallId, "tool_call_id"), + toolInputJson: selectedValue(value.toolInputJson, "tool_input_json"), + toolName: selectedValue(value.toolName, "tool_name"), + tokens: selectedValue(value.tokens, "tokens"), + traceId: selectedValue(value.traceId, "trace_id"), + visibility: selectedValue(value.visibility, "visibility"), + }) + .from(sessionsTable) + .where(and(eq(sessionsTable.id, value.sessionId), isNull(sessionsTable.archivedAt))) + .$dynamic(); +} + function toSessionRuntimeEventInsertValues(input: { allocation: SessionRuntimeEventBatchAllocation; rows: ProjectedSessionRuntimeEventRowInput[]; diff --git a/apps/api/src/modules/sessions/infrastructure/session-runtime-event-store.types.ts b/apps/api/src/modules/sessions/infrastructure/session-runtime-event-store.types.ts index d6f8ee2b..7e37637b 100644 --- a/apps/api/src/modules/sessions/infrastructure/session-runtime-event-store.types.ts +++ b/apps/api/src/modules/sessions/infrastructure/session-runtime-event-store.types.ts @@ -1,3 +1,4 @@ +import type { AgentTaskSnapshot } from "@mosoo/contracts/session"; import type { AgentId, RuntimeEventId, SessionId, SessionRunId } from "@mosoo/id"; import type { RuntimeEventEnvelope } from "@mosoo/runtime-events"; @@ -73,6 +74,7 @@ export interface SessionRuntimeEventSourceReceipt { } export interface SessionEventInsertValue { + readonly agentTaskSnapshot: AgentTaskSnapshot | null; readonly agentId: AgentId; readonly contentText: string; readonly createdAt: number; diff --git a/apps/api/src/modules/sessions/infrastructure/session-viewer-live-snapshot.repository.ts b/apps/api/src/modules/sessions/infrastructure/session-viewer-live-snapshot.repository.ts index 9b35e3a7..9380d536 100644 --- a/apps/api/src/modules/sessions/infrastructure/session-viewer-live-snapshot.repository.ts +++ b/apps/api/src/modules/sessions/infrastructure/session-viewer-live-snapshot.repository.ts @@ -18,18 +18,20 @@ import { parseSchemaValue, } from "@mosoo/contracts/validation"; import { + sessionAgentTaskSnapshotsTable, sessionPermissionRequestsTable, sessionReadinessSnapshotsTable, sessionRunsTable, sessionsTable, } from "@mosoo/db"; import type { AgentDeploymentVersionId, PlatformId, SessionId, SessionRunId } from "@mosoo/id"; -import { asc, eq } from "drizzle-orm"; +import { and, asc, eq, isNull } from "drizzle-orm"; import { getAppDatabase } from "../../../platform/db/drizzle"; import { isTruthy } from "../../../shared/truthiness"; import { toIsoString } from "../../../time"; import { fileStore } from "../../files/application/file-store"; +import { parseStoredAgentTaskSnapshot } from "./session-agent-task-snapshot.repository"; import { createInitialSessionLiveState } from "./session-live-state.reducer"; import { loadStoredSessionMessages } from "./session-message-snapshot.repository"; @@ -45,6 +47,7 @@ interface SessionViewerStateJoinedRow extends SessionViewerStateSessionRow { run_created_at: number | null; run_deployment_version_id: AgentDeploymentVersionId | null; run_deployment_version_number: number | null; + run_driver_instance_id: string | null; run_error_code: string | null; run_error_details_json: string | null; run_error_message: string | null; @@ -56,6 +59,9 @@ interface SessionViewerStateJoinedRow extends SessionViewerStateSessionRow { run_trace_id: string | null; run_trigger: SessionRunTrigger | null; run_updated_at: number | null; + task_driver_instance_id: string | null; + task_run_id: string | null; + task_tasks_json: string | null; } interface SessionViewerStateSnapshotRow { @@ -108,6 +114,7 @@ async function listSessionViewerStateSnapshotRows( run_created_at: sessionRunsTable.createdAt, run_deployment_version_id: sessionRunsTable.deploymentVersionId, run_deployment_version_number: sessionRunsTable.deploymentVersionNumber, + run_driver_instance_id: sessionRunsTable.driverInstanceId, run_error_code: sessionRunsTable.errorCode, run_error_details_json: sessionRunsTable.errorDetailsJson, run_error_message: sessionRunsTable.errorMessage, @@ -120,12 +127,24 @@ async function listSessionViewerStateSnapshotRows( run_trigger: sessionRunsTable.trigger, run_updated_at: sessionRunsTable.updatedAt, status: sessionsTable.status, + task_driver_instance_id: sessionAgentTaskSnapshotsTable.driverInstanceId, + task_run_id: sessionAgentTaskSnapshotsTable.runId, + task_tasks_json: sessionAgentTaskSnapshotsTable.tasksJson, title: sessionsTable.title, updated_at: sessionsTable.updatedAt, }, }) .from(sessionsTable) .leftJoin(sessionRunsTable, eq(sessionRunsTable.id, sessionsTable.lastRunId)) + .leftJoin( + sessionAgentTaskSnapshotsTable, + and( + eq(sessionAgentTaskSnapshotsTable.sessionId, sessionsTable.id), + eq(sessionAgentTaskSnapshotsTable.runId, sessionRunsTable.id), + eq(sessionAgentTaskSnapshotsTable.driverInstanceId, sessionRunsTable.driverInstanceId), + isNull(sessionsTable.archivedAt), + ), + ) .where(eq(sessionsTable.id, sessionId)) .limit(1) .all(); @@ -313,6 +332,28 @@ function isTerminalRunStatus(status: SessionRunView["status"]): boolean { ); } +function toJoinedAgentTaskSnapshot( + row: SessionViewerStateJoinedRow, +): SessionLiveState["taskSnapshot"] { + if ( + row.status !== "RUNNING" || + row.run_status === null || + isTerminalRunStatus(row.run_status) || + row.task_driver_instance_id === null || + row.task_run_id === null || + row.task_tasks_json === null + ) { + return null; + } + + return parseStoredAgentTaskSnapshot({ + driverInstanceId: row.task_driver_instance_id, + runId: row.task_run_id, + sessionId: row.id, + tasksJson: row.task_tasks_json, + }); +} + function toCanonicalLifecycleStatus( sessionStatus: SessionStatus, runStatus: SessionRunView["status"], @@ -334,6 +375,7 @@ function applyCanonicalSessionState( input: { files: SessionViewFile[]; latestRun: SessionRunSummary | null; + runDriverInstanceId: string | null; session: SessionViewerStateSessionRow; viewerId: PlatformId; }, @@ -347,6 +389,13 @@ function applyCanonicalSessionState( return { ...state, files: input.files, + infra: { + ...state.infra, + driverInstanceId: + input.session.status === "RUNNING" && !isTerminalRunStatus(run.status) + ? input.runDriverInstanceId + : null, + }, lifecycle: toCanonicalLifecycleStatus(input.session.status, run.status), permissionRequests, run, @@ -375,6 +424,7 @@ export async function loadSessionViewerState( ]); const session = getFirstSnapshotRow(snapshotRows).session; const latestRun = toJoinedSessionRunSummary(session); + const taskSnapshot = toJoinedAgentTaskSnapshot(session); const baseState = createInitialSessionLiveState({ sessionId: input.sessionId, title: session.title, @@ -385,10 +435,12 @@ export async function loadSessionViewerState( messages, permissionRequests, readiness, + taskSnapshot, }; const state = applyCanonicalSessionState(stateWithMessages, { files: sessionFiles, latestRun, + runDriverInstanceId: session.run_driver_instance_id, session, viewerId: input.viewerId, }); diff --git a/apps/api/src/modules/sessions/infrastructure/session/viewer-socket-hub.ts b/apps/api/src/modules/sessions/infrastructure/session/viewer-socket-hub.ts index 1ca33a7f..e19f809a 100644 --- a/apps/api/src/modules/sessions/infrastructure/session/viewer-socket-hub.ts +++ b/apps/api/src/modules/sessions/infrastructure/session/viewer-socket-hub.ts @@ -47,6 +47,7 @@ export class SessionViewerSocketHub { readonly #getSessionId: () => string | null; #liveStateCache: SessionLiveState | null = null; readonly #rememberSessionId: (sessionId: string) => void; + #stateOperationTail: Promise = Promise.resolve(); readonly #withSessionLogContext: (fn: () => T) => T; constructor(options: SessionViewerSocketHubOptions) { @@ -62,50 +63,54 @@ export class SessionViewerSocketHub { return; } - const broadcast = buildViewerBroadcastFrames({ - cachedState: this.#liveStateCache, - events, - }); + await this.#runStateOperation(async () => { + const broadcast = buildViewerBroadcastFrames({ + cachedState: this.#liveStateCache, + events, + }); - if (!broadcast) { - return; - } + if (!broadcast) { + return; + } - if (broadcast.state) { - this.#liveStateCache = broadcast.state; - } + if (broadcast.state) { + this.#liveStateCache = broadcast.state; + } - for (const socket of this.#getViewerSockets()) { - const attachment = getSocketAttachment(socket); + for (const socket of this.#getViewerSockets()) { + const attachment = getSocketAttachment(socket); - if (!attachment || socket.readyState !== WebSocket.OPEN) { - continue; - } + if (!attachment || socket.readyState !== WebSocket.OPEN) { + continue; + } - sendFrames(socket, broadcast.frames); - } + sendFrames(socket, broadcast.frames); + } + }); } async broadcastStateSync(): Promise { - const sockets = this.#getViewerSockets() - .map((socket) => ({ attachment: getSocketAttachment(socket), socket })) - .filter( - ( - candidate, - ): candidate is { - attachment: ViewerSocketAttachment; - socket: WebSocket; - } => candidate.attachment !== null, - ); - - await sendViewerSocketStateSyncBatch({ - cachedState: this.#liveStateCache, - database: this.#env.DB, - getLatestCachedState: () => this.#liveStateCache, - sockets, - updateLiveStateCache: (state) => { - this.#rememberLoadedLiveState(state); - }, + await this.#runStateOperation(async () => { + const sockets = this.#getViewerSockets() + .map((socket) => ({ attachment: getSocketAttachment(socket), socket })) + .filter( + ( + candidate, + ): candidate is { + attachment: ViewerSocketAttachment; + socket: WebSocket; + } => candidate.attachment !== null, + ); + + await sendViewerSocketStateSyncBatch({ + cachedState: this.#liveStateCache, + database: this.#env.DB, + getLatestCachedState: () => this.#liveStateCache, + sockets, + updateLiveStateCache: (state) => { + this.#rememberLoadedLiveState(state); + }, + }); }); } @@ -213,30 +218,43 @@ export class SessionViewerSocketHub { } async handleAlarm(): Promise { - await runViewerPermissionCleanupAlarm({ - cachedState: this.#liveStateCache, - env: this.#env, - hasOpenViewer: (sessionId) => this.#hasOpenViewer(sessionId), - storage: this.#ctx.storage, - updateLiveStateCache: (state) => { - this.#rememberLoadedLiveState(state); - }, + await this.#runStateOperation(async () => { + await runViewerPermissionCleanupAlarm({ + cachedState: this.#liveStateCache, + env: this.#env, + hasOpenViewer: (sessionId) => this.#hasOpenViewer(sessionId), + storage: this.#ctx.storage, + updateLiveStateCache: (state) => { + this.#rememberLoadedLiveState(state); + }, + }); }); } async #sendViewerStateSync(ws: WebSocket, attachment: ViewerSocketAttachment): Promise { - await sendViewerSocketStateSync({ - attachment, - cachedState: this.#liveStateCache, - database: this.#env.DB, - getLatestCachedState: () => this.#liveStateCache, - updateLiveStateCache: (state) => { - this.#rememberLoadedLiveState(state); - }, - ws, + await this.#runStateOperation(async () => { + await sendViewerSocketStateSync({ + attachment, + cachedState: this.#liveStateCache, + database: this.#env.DB, + getLatestCachedState: () => this.#liveStateCache, + updateLiveStateCache: (state) => { + this.#rememberLoadedLiveState(state); + }, + ws, + }); }); } + #runStateOperation(operation: () => Promise): Promise { + const result = this.#stateOperationTail.then(operation); + this.#stateOperationTail = result.then( + () => undefined, + () => undefined, + ); + return result; + } + #rememberLoadedLiveState(state: SessionLiveState | null): void { if (!state) { return; diff --git a/apps/api/src/platform/db/drizzle.ts b/apps/api/src/platform/db/drizzle.ts index e435c746..90cdb28a 100644 --- a/apps/api/src/platform/db/drizzle.ts +++ b/apps/api/src/platform/db/drizzle.ts @@ -33,6 +33,7 @@ import { sandboxesTable, sandboxSessionsTable, sessionExecutionSnapshotsTable, + sessionAgentTaskSnapshotsTable, sessionEventsTable, sessionMessagesTable, sessionModelCallsTable, @@ -88,6 +89,7 @@ const schema = { sandboxesTable, sandboxSessionsTable, sessionExecutionSnapshotsTable, + sessionAgentTaskSnapshotsTable, sessionEventsTable, sessionMessagesTable, sessionModelCallsTable, diff --git a/apps/api/tests/agent-session-retrieve.test.ts b/apps/api/tests/agent-session-retrieve.test.ts index d272228a..7df37cae 100644 --- a/apps/api/tests/agent-session-retrieve.test.ts +++ b/apps/api/tests/agent-session-retrieve.test.ts @@ -59,12 +59,14 @@ function createAgentSessionRetrieveDatabase(): SqliteD1Database { created_at integer, deployment_version_id text, deployment_version_number integer, + driver_instance_id text, error_code text, error_details_json text, error_message text, id text PRIMARY KEY NOT NULL, model text, provider text, + session_id text, started_at integer, status text, trace_id text, @@ -72,6 +74,14 @@ function createAgentSessionRetrieveDatabase(): SqliteD1Database { updated_at integer ); + CREATE TABLE session_agent_task_snapshot ( + driver_instance_id text NOT NULL, + run_id text NOT NULL, + seq integer NOT NULL, + session_id text PRIMARY KEY NOT NULL, + tasks_json text NOT NULL + ); + INSERT INTO session ( agent_id, created_at, @@ -151,6 +161,108 @@ describe("agent session retrieve", () => { expect(result.session.id).toBe("session-1"); }); + test("returns the current schema-validated task snapshot", async () => { + const database = createAgentSessionRetrieveDatabase(); + database.execute(` + UPDATE session + SET last_run_id = 'run-1', status = 'RUNNING' + WHERE id = 'session-1'; + + INSERT INTO session_run ( + created_at, + driver_instance_id, + id, + session_id, + status, + trace_id, + trigger, + updated_at + ) + VALUES ( + 2, + 'driver-1', + 'run-1', + 'session-1', + 'running', + 'trace-1', + 'user_message', + 2 + ); + + INSERT INTO session_agent_task_snapshot ( + driver_instance_id, + run_id, + seq, + session_id, + tasks_json + ) + VALUES ( + 'driver-1', + 'run-1', + 3, + 'session-1', + '{"tasks":[{"taskId":"task-1","taskType":"review"}]}' + ); + `); + + const result = await retrieveAgentSession(database, VIEWER, { + appId: APP_ID, + sessionId: "session-1", + }); + + expect(result.taskSnapshot).toEqual({ + driverInstanceId: "driver-1", + runId: "run-1", + tasks: [{ taskId: "task-1", taskType: "review" }], + }); + }); + + test.each([ + ["terminal run", null, "completed", '{"tasks":[{"taskId":"stale"}]}'], + ["malformed state", null, "running", '{"tasks":"invalid"}'], + ["archived running session", 3, "running", '{"tasks":[{"taskId":"stale"}]}'], + ])("fails closed for %s task snapshots", async (_label, archivedAt, runStatus, tasksJson) => { + const database = createAgentSessionRetrieveDatabase(); + await database + .prepare("UPDATE session SET archived_at = ?, last_run_id = ?, status = ? WHERE id = ?") + .bind(archivedAt, "run-1", "RUNNING", "session-1") + .run(); + await database + .prepare( + `INSERT INTO session_run ( + created_at, + driver_instance_id, + id, + session_id, + status, + trace_id, + trigger, + updated_at + ) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`, + ) + .bind(2, "driver-1", "run-1", "session-1", runStatus, "trace-1", "user_message", 2) + .run(); + await database + .prepare( + `INSERT INTO session_agent_task_snapshot ( + driver_instance_id, + run_id, + seq, + session_id, + tasks_json + ) VALUES (?, ?, ?, ?, ?)`, + ) + .bind("driver-1", "run-1", 3, "session-1", tasksJson) + .run(); + + const result = await retrieveAgentSession(database, VIEWER, { + appId: APP_ID, + sessionId: "session-1", + }); + + expect(result.taskSnapshot).toBeNull(); + }); + test("apps terminal cleanup rows as not recoverable even with archive marker", async () => { const database = createAgentSessionRetrieveDatabase(); diff --git a/apps/api/tests/api-driver-boundary.test.ts b/apps/api/tests/api-driver-boundary.test.ts index 10eb2e48..30287f02 100644 --- a/apps/api/tests/api-driver-boundary.test.ts +++ b/apps/api/tests/api-driver-boundary.test.ts @@ -804,6 +804,36 @@ describe("API to driver boundary", () => { ).not.toThrow(); }); + test("rejects non-empty task snapshots after run terminal but permits an explicit clear", () => { + const event = (tasks: { taskId: string }[]) => + createRuntimeEvent({ + driverInstanceId: API_DRIVER_BOUNDARY_IDS.driverInstance, + id: API_DRIVER_BOUNDARY_IDS.runtimeEvent, + kind: "agent.tasks.replaced", + occurredAt: "1970-01-01T00:00:00.010Z", + payload: { tasks }, + runId: API_DRIVER_BOUNDARY_IDS.sessionRun, + sessionId: API_DRIVER_BOUNDARY_IDS.session, + }); + const terminalLink = { + ...createRuntimeSessionLink(), + sessionRunStatus: "completed" as const, + }; + + expect(() => + assertRuntimeEventMatchesDriverLink(event([{ taskId: "stale" }]), { + driverInstanceId: API_DRIVER_BOUNDARY_IDS.driverInstance, + link: terminalLink, + }), + ).toThrow("requires an active session run"); + expect(() => + assertRuntimeEventMatchesDriverLink(event([]), { + driverInstanceId: API_DRIVER_BOUNDARY_IDS.driverInstance, + link: terminalLink, + }), + ).not.toThrow(); + }); + test("rejects canonical driver events whose source id disagrees with the envelope", () => { expect(() => assertRuntimeEventMatchesDriverEnvelope( diff --git a/apps/api/tests/driver-finalization-repair.test.ts b/apps/api/tests/driver-finalization-repair.test.ts index 9a65fafc..85cf364e 100644 --- a/apps/api/tests/driver-finalization-repair.test.ts +++ b/apps/api/tests/driver-finalization-repair.test.ts @@ -9,7 +9,7 @@ import { DriverCommandDispatcher } from "../../driver/src/core/driver-command-di import { DriverPermissionBroker } from "../../driver/src/core/driver-permission-broker"; import type { DriverRuntimeIo } from "../../driver/src/core/driver-runtime-io"; import { DriverRuntimeStateMachine } from "../../driver/src/core/driver-runtime-state"; -import type { AgentDriverMcpPort } from "../../driver/src/host-ports"; +import type { AgentDriverMcpExecution } from "../../driver/src/host-ports"; import { createBufferedSinkLogger } from "../../driver/src/observability"; import { createDriverStartInputFromBootPayload } from "../../driver/src/protocol/start"; import type { RuntimeCommand as DriverRuntimeCommand } from "../../driver/src/runtime-command"; @@ -181,7 +181,7 @@ class PersistentEffectDriverIo implements DriverRuntimeIo { function createPersistentEffectDispatcher(input: { io: PersistentEffectDriverIo; - mcpExecute: AgentDriverMcpPort["execute"]; + mcpExecute: AgentDriverMcpExecution["execute"]; }): { dispatcher: DriverCommandDispatcher; logger: ReturnType } { const logger = createBufferedSinkLogger({ level: "debug", @@ -210,7 +210,12 @@ function createPersistentEffectDispatcher(input: { permission: { request: async () => "reject_once" }, ports: { commandSource: { nextCommand: (signal) => socket.nextCommand(signal) }, - mcp: { execute: input.mcpExecute }, + mcp: { + prepare: async () => ({ + [Symbol.asyncDispose]: async () => {}, + execute: input.mcpExecute, + }), + }, }, }), runtimeState: new DriverRuntimeStateMachine("ready"), @@ -813,14 +818,14 @@ describe("driver finalization repair", () => { }); const first = createPersistentEffectDispatcher({ io: firstIo, - mcpExecute: async (request) => { + mcpExecute: async () => { providerCalls += 1; return { outputText: "created issue A-1", providerReceiptJson: '{"orderId":"A-1"}', - requestId: request.requestId, - serverId: request.serverId, - toolName: request.toolName, + requestId: command.requestId, + serverId: command.serverId, + toolName: command.toolName, }; }, }); diff --git a/apps/api/tests/helpers/public-api-http-runtime-schema.sql b/apps/api/tests/helpers/public-api-http-runtime-schema.sql index b95294ec..d457e45c 100644 --- a/apps/api/tests/helpers/public-api-http-runtime-schema.sql +++ b/apps/api/tests/helpers/public-api-http-runtime-schema.sql @@ -159,6 +159,16 @@ CREATE TABLE session_event ( created_at integer NOT NULL ); +CREATE TABLE session_agent_task_snapshot ( + driver_instance_id text NOT NULL, + run_id text NOT NULL, + seq integer NOT NULL, + session_id text PRIMARY KEY NOT NULL, + tasks_json text NOT NULL, + FOREIGN KEY (run_id) REFERENCES session_run (id) ON DELETE CASCADE, + FOREIGN KEY (session_id) REFERENCES session (id) ON DELETE CASCADE +); + CREATE TABLE session_permission_request ( created_at integer NOT NULL, driver_instance_id text NOT NULL, diff --git a/apps/api/tests/session-process-events.test.ts b/apps/api/tests/session-process-events.test.ts index 123ac20d..6d4796cb 100644 --- a/apps/api/tests/session-process-events.test.ts +++ b/apps/api/tests/session-process-events.test.ts @@ -546,11 +546,20 @@ describe("session process event projection", () => { seq: 1, visibility: "owner_debug", }); + await insertSessionProcessEvent(innerDatabase, { + content: "1 background task active.", + eventType: "agent.tasks.replaced", + id: "event-task-state", + occurredAt: 950, + processType: "session.status", + seq: 2, + visibility: "owner_debug", + }); await insertSessionProcessEvent(innerDatabase, { content: "run-1", id: "event-public", occurredAt: 1000, - seq: 2, + seq: 3, }); const events = await getThreadSessionProcessEvents( diff --git a/apps/api/tests/session-runtime-event-store.test.ts b/apps/api/tests/session-runtime-event-store.test.ts index 73ddd860..082ebcc2 100644 --- a/apps/api/tests/session-runtime-event-store.test.ts +++ b/apps/api/tests/session-runtime-event-store.test.ts @@ -8,6 +8,7 @@ import type { } from "@mosoo/runtime-events"; import { RuntimeEventPersistenceCompactor } from "../src/modules/runtime/infrastructure/driver-instance/runtime-event-persistence-compactor"; +import { loadSessionAgentTaskSnapshot } from "../src/modules/sessions/infrastructure/session-agent-task-snapshot.repository"; import { persistOneRuntimeEventPerSession, persistSessionRuntimeEvents, @@ -39,6 +40,37 @@ function runtimeEvent(input: { }); } +function agentTasksEvent(input: { + driverInstanceId: string; + id: string; + occurredAtMs: number; + runId: string; + taskId?: string; +}): RuntimeEventEnvelope { + return runtimeEvent({ + driverInstanceId: input.driverInstanceId, + id: input.id, + kind: "agent.tasks.replaced", + occurredAtMs: input.occurredAtMs, + payload: { tasks: input.taskId === undefined ? [] : [{ taskId: input.taskId }] }, + runId: input.runId, + }); +} + +function activateRun( + database: SqliteD1Database, + input: { driverInstanceId: string; runId: string }, +): void { + database + .prepare("UPDATE session SET last_run_id = ?, status = 'RUNNING' WHERE id = 'session-1'") + .bind(input.runId) + .run(); + database + .prepare("UPDATE session_run SET driver_instance_id = ?, status = 'running' WHERE id = ?") + .bind(input.driverInstanceId, input.runId) + .run(); +} + function createRuntimeEventStoreDatabase( input: { maxBoundParams?: number } = {}, ): SqliteD1Database { @@ -52,13 +84,24 @@ function createRuntimeEventStoreDatabase( id text PRIMARY KEY NOT NULL, agent_id text NOT NULL, archived_at integer, + last_run_id text, status text NOT NULL, runtime_event_seq_cursor integer DEFAULT 0 NOT NULL ); CREATE TABLE session_run ( id text PRIMARY KEY NOT NULL, - session_id text NOT NULL + driver_instance_id text, + session_id text NOT NULL, + status text DEFAULT 'running' NOT NULL + ); + + CREATE TABLE session_agent_task_snapshot ( + driver_instance_id text NOT NULL, + run_id text NOT NULL, + seq integer NOT NULL, + session_id text PRIMARY KEY NOT NULL, + tasks_json text NOT NULL ); CREATE TABLE session_event ( @@ -147,6 +190,276 @@ function createRuntimeEventStoreDatabase( } describe("session runtime event store", () => { + test("atomically persists only the latest current-run task snapshot", async () => { + const database = createRuntimeEventStoreDatabase(); + activateRun(database, { driverInstanceId: "driver-1", runId: "run-1" }); + + const result = await persistSessionRuntimeEvents(database, { + records: [ + { + event: agentTasksEvent({ + driverInstanceId: "driver-1", + id: "tasks-1", + occurredAtMs: 1_000, + runId: "run-1", + taskId: "first", + }), + occurredAt: 1_000, + sourceEventId: "source-tasks-1", + }, + { + event: agentTasksEvent({ + driverInstanceId: "driver-1", + id: "tasks-2", + occurredAtMs: 1_001, + runId: "run-1", + taskId: "latest", + }), + occurredAt: 1_001, + sourceEventId: "source-tasks-2", + }, + ], + sessionId: "session-1", + }); + const snapshot = await database + .prepare( + "SELECT driver_instance_id, run_id, seq, tasks_json FROM session_agent_task_snapshot", + ) + .first<{ + driver_instance_id: string; + run_id: string; + seq: number; + tasks_json: string; + }>(); + const eventRows = await database + .prepare("SELECT content_text, visibility FROM session_event ORDER BY seq") + .all<{ content_text: string; visibility: string }>(); + + expect(result.persistedCount).toBe(2); + expect(snapshot).toEqual({ + driver_instance_id: "driver-1", + run_id: "run-1", + seq: 2, + tasks_json: JSON.stringify({ tasks: [{ taskId: "latest" }] }), + }); + expect(eventRows.results).toEqual([ + { content_text: "1 background task active.", visibility: "owner_debug" }, + { content_text: "1 background task active.", visibility: "owner_debug" }, + ]); + }); + + test("rolls back the task event receipt when snapshot persistence fails", async () => { + const database = createRuntimeEventStoreDatabase(); + activateRun(database, { driverInstanceId: "driver-1", runId: "run-1" }); + database.execute(` + CREATE TRIGGER reject_agent_task_snapshot + BEFORE INSERT ON session_agent_task_snapshot + BEGIN + SELECT RAISE(ABORT, 'forced snapshot failure'); + END; + `); + + await expect( + persistSessionRuntimeEvents(database, { + records: [ + { + event: agentTasksEvent({ + driverInstanceId: "driver-1", + id: "tasks-1", + occurredAtMs: 1_000, + runId: "run-1", + taskId: "task-1", + }), + occurredAt: 1_000, + sourceEventId: "source-tasks-1", + }, + ], + sessionId: "session-1", + }), + ).rejects.toThrow("forced snapshot failure"); + + expect(await database.prepare("SELECT COUNT(*) AS count FROM session_event").first()).toEqual({ + count: 0, + }); + }); + + test("rejects the receipt and snapshot when archive wins after sequence allocation", async () => { + const database = createRuntimeEventStoreDatabase(); + activateRun(database, { driverInstanceId: "driver-1", runId: "run-1" }); + const persistBatch = database.batch.bind(database) as D1Database["batch"]; + let archivedBeforeBatch = false; + + database.batch = async (statements: D1PreparedStatement[]) => { + if (!archivedBeforeBatch) { + archivedBeforeBatch = true; + database.execute("UPDATE session SET archived_at = 2 WHERE id = 'session-1'"); + } + + return persistBatch(statements); + }; + + const result = await persistSessionRuntimeEvents(database, { + records: [ + { + event: agentTasksEvent({ + driverInstanceId: "driver-1", + id: "tasks-after-archive", + occurredAtMs: 1_000, + runId: "run-1", + taskId: "must-not-land", + }), + occurredAt: 1_000, + sourceEventId: "source-after-archive", + }, + ], + sessionId: "session-1", + }); + const session = await database + .prepare("SELECT archived_at, runtime_event_seq_cursor FROM session WHERE id = 'session-1'") + .first<{ archived_at: number | null; runtime_event_seq_cursor: number }>(); + + expect(archivedBeforeBatch).toBe(true); + expect(session).toEqual({ archived_at: 2, runtime_event_seq_cursor: 1 }); + expect(result.persistedCount).toBe(0); + expect(await database.prepare("SELECT COUNT(*) AS count FROM session_event").first()).toEqual({ + count: 0, + }); + expect( + await database.prepare("SELECT COUNT(*) AS count FROM session_agent_task_snapshot").first(), + ).toEqual({ count: 0 }); + expect(await loadSessionAgentTaskSnapshot(database, "session-1")).toBeNull(); + }); + + test("persists an explicit empty snapshot and hides it after archive", async () => { + const database = createRuntimeEventStoreDatabase(); + activateRun(database, { driverInstanceId: "driver-1", runId: "run-1" }); + + for (const [index, taskId] of ["task-1", undefined].entries()) { + await persistSessionRuntimeEvents(database, { + records: [ + { + event: agentTasksEvent({ + driverInstanceId: "driver-1", + id: `tasks-${index + 1}`, + occurredAtMs: 1_000 + index, + runId: "run-1", + ...(taskId === undefined ? {} : { taskId }), + }), + occurredAt: 1_000 + index, + sourceEventId: `source-tasks-${index + 1}`, + }, + ], + sessionId: "session-1", + }); + } + + expect( + await database + .prepare("SELECT tasks_json FROM session_agent_task_snapshot WHERE session_id = ?") + .bind("session-1") + .first<{ tasks_json: string }>(), + ).toEqual({ tasks_json: JSON.stringify({ tasks: [] }) }); + expect(await loadSessionAgentTaskSnapshot(database, "session-1")).toEqual({ + driverInstanceId: "driver-1", + runId: "run-1", + tasks: [], + }); + + database.execute("UPDATE session SET archived_at = 2 WHERE id = 'session-1'"); + expect(await loadSessionAgentTaskSnapshot(database, "session-1")).toBeNull(); + }); + + test("does not let duplicate receipts or stale run and driver snapshots replace current state", async () => { + const database = createRuntimeEventStoreDatabase(); + activateRun(database, { driverInstanceId: "driver-1", runId: "run-1" }); + + await persistSessionRuntimeEvents(database, { + records: [ + { + event: agentTasksEvent({ + driverInstanceId: "driver-1", + id: "tasks-1", + occurredAtMs: 1_000, + runId: "run-1", + taskId: "first", + }), + occurredAt: 1_000, + sourceEventId: "source-tasks-1", + }, + ], + sessionId: "session-1", + }); + await persistSessionRuntimeEvents(database, { + records: [ + { + event: agentTasksEvent({ + driverInstanceId: "driver-1", + id: "tasks-replay", + occurredAtMs: 1_001, + runId: "run-1", + taskId: "duplicate-must-not-win", + }), + occurredAt: 1_001, + sourceEventId: "source-tasks-1", + }, + ], + sessionId: "session-1", + }); + + database.execute( + "INSERT INTO session_run (id, driver_instance_id, session_id, status) VALUES ('run-3', 'driver-2', 'session-1', 'running')", + ); + activateRun(database, { driverInstanceId: "driver-2", runId: "run-3" }); + await persistSessionRuntimeEvents(database, { + records: [ + { + event: agentTasksEvent({ + driverInstanceId: "driver-2", + id: "tasks-current", + occurredAtMs: 1_002, + runId: "run-3", + taskId: "current", + }), + occurredAt: 1_002, + sourceEventId: "source-current", + }, + { + event: agentTasksEvent({ + driverInstanceId: "driver-1", + id: "tasks-old-run", + occurredAtMs: 1_003, + runId: "run-1", + taskId: "old-run", + }), + occurredAt: 1_003, + sourceEventId: "source-old-run", + }, + { + event: agentTasksEvent({ + driverInstanceId: "driver-1", + id: "tasks-old-driver", + occurredAtMs: 1_004, + runId: "run-3", + taskId: "old-driver", + }), + occurredAt: 1_004, + sourceEventId: "source-old-driver", + }, + ], + sessionId: "session-1", + }); + + expect( + await database + .prepare("SELECT driver_instance_id, run_id, tasks_json FROM session_agent_task_snapshot") + .first(), + ).toEqual({ + driver_instance_id: "driver-2", + run_id: "run-3", + tasks_json: JSON.stringify({ tasks: [{ taskId: "current" }] }), + }); + }); + test("keeps runtime event inserts within D1's bound parameter limit", async () => { const database = createRuntimeEventStoreDatabase({ maxBoundParams: 100 }); const records = Array.from({ length: 6 }, (_, index) => { diff --git a/apps/api/tests/session-viewer-event-delivery-buffer.test.ts b/apps/api/tests/session-viewer-event-delivery-buffer.test.ts index a887253b..cfebf8ae 100644 --- a/apps/api/tests/session-viewer-event-delivery-buffer.test.ts +++ b/apps/api/tests/session-viewer-event-delivery-buffer.test.ts @@ -17,6 +17,44 @@ interface PublishedRequest { sessionId: string; } +const largeTaskMetadata = "x".repeat(4_096); +const maxSerializedBatchBytes = 1_536 * 1024; + +function createLargeTaskSnapshot(input: { + driverInstanceId: string; + marker: string; + runId: string; +}): AgUiSessionEvent { + return createServerCustomEvent(MOSOO_CUSTOM_EVENT.sessionTasksReplaced.name, { + driverInstanceId: input.driverInstanceId, + runId: input.runId, + tasks: Array.from({ length: 120 }, (_, index) => ({ + taskId: `${input.marker}-${index}`, + taskType: largeTaskMetadata, + title: largeTaskMetadata, + })), + }); +} + +function createTerminalEvent(runId = "run-1"): AgUiSessionEvent { + return createServerCustomEvent(MOSOO_CUSTOM_EVENT.sessionRunUpdated.name, { + driverInstanceId: null, + lifecycle: "IDLE", + run: { + completedAt: "2026-04-30T00:00:01.000Z", + error: null, + id: runId, + startedAt: "2026-04-30T00:00:00.000Z", + status: "completed", + traceId: null, + }, + }); +} + +function serializedEventBytes(events: AgUiSessionEvent[]): number { + return new TextEncoder().encode(JSON.stringify(events)).byteLength; +} + function createDeferred(): Deferred { let rejectDeferred: (reason?: unknown) => void = () => {}; let resolveDeferred: (value: T) => void = () => {}; @@ -35,12 +73,14 @@ function createDeferred(): Deferred { function createBufferHarness(): { buffer: SessionViewerEventDeliveryBuffer; published: PublishedRequest[]; + pushAfterResponse: (callback: () => void) => void; pushResponse: (response: Promise | Response) => void; waitForPublish: () => Promise; waitForWaitUntil: () => Promise; } { const publishWaiters: Array<() => void> = []; const published: PublishedRequest[] = []; + const afterResponseCallbacks: Array<() => void> = []; const responses: Array | Response> = []; const waitUntilTasks: Promise[] = []; const sessionStub = { @@ -56,6 +96,8 @@ function createBufferHarness(): { if (!response.ok) { throw new Error(`Session event publish failed with status ${response.status}.`); } + + afterResponseCallbacks.shift()?.(); }, }; const env = { @@ -79,6 +121,9 @@ function createBufferHarness(): { return { buffer, published, + pushAfterResponse: (callback) => { + afterResponseCallbacks.push(callback); + }, pushResponse: (response) => { responses.push(response); }, @@ -139,17 +184,7 @@ describe("SessionViewerEventDeliveryBuffer", () => { test("flushes terminal events immediately", async () => { const { buffer, published, waitForWaitUntil } = createBufferHarness(); - const terminalEvent = createServerCustomEvent(MOSOO_CUSTOM_EVENT.sessionRunUpdated.name, { - lifecycle: "IDLE", - run: { - completedAt: "2026-04-30T00:00:01.000Z", - error: null, - id: "run-1", - startedAt: "2026-04-30T00:00:00.000Z", - status: "completed", - traceId: null, - }, - }); + const terminalEvent = createTerminalEvent(); buffer.enqueue("session-1", [ { delta: "done", messageId: "assistant-1", type: "TEXT_MESSAGE_CONTENT" }, @@ -200,21 +235,136 @@ describe("SessionViewerEventDeliveryBuffer", () => { expect(published).toHaveLength(2); }); - test("requeues failed deliveries before events enqueued during the failed publish", async () => { + test("does not strand a terminal batch queued as the prior delivery settles", async () => { + const { buffer, published, pushAfterResponse, waitForWaitUntil } = createBufferHarness(); + const terminalEvent = createTerminalEvent(); + + pushAfterResponse(() => { + // Cross the Session stub, client, and drain continuations so this lands + // after the drain resolves but before a chained cleanup reaction can run. + queueMicrotask(() => { + queueMicrotask(() => { + queueMicrotask(() => { + buffer.enqueue("session-1", [terminalEvent]); + }); + }); + }); + }); + buffer.enqueue("session-1", [ + { delta: "done", messageId: "assistant-1", type: "TEXT_MESSAGE_CONTENT" }, + ]); + await buffer.flush(); + await waitForWaitUntil(); + + expect(published.map((request) => request.events)).toEqual([ + [{ delta: "done", messageId: "assistant-1", type: "TEXT_MESSAGE_CONTENT" }], + [terminalEvent], + ]); + }); + + test("coalesces a same-generation large task snapshot flood to the latest event", async () => { + const { buffer, published } = createBufferHarness(); + let latestSnapshot: AgUiSessionEvent | null = null; + + for (let index = 0; index < 34; index += 1) { + latestSnapshot = createLargeTaskSnapshot({ + driverInstanceId: "driver-1", + marker: `snapshot-${index}`, + runId: "run-1", + }); + buffer.enqueue("session-1", [latestSnapshot]); + } + await buffer.flush(); + + expect(published).toHaveLength(1); + expect(published[0]?.events).toEqual([latestSnapshot]); + }); + + test("keeps only the in-flight and latest same-generation snapshot during a slow publish", async () => { + const { buffer, published, pushResponse, waitForPublish } = createBufferHarness(); + const response = createDeferred(); + const inFlightSnapshot = createLargeTaskSnapshot({ + driverInstanceId: "driver-1", + marker: "in-flight", + runId: "run-1", + }); + const pendingSnapshots = Array.from({ length: 5 }, (_, index) => + createLargeTaskSnapshot({ + driverInstanceId: "driver-1", + marker: `pending-${index}`, + runId: "run-1", + }), + ); + pushResponse(response.promise); + + buffer.enqueue("session-1", [inFlightSnapshot]); + const firstPublish = waitForPublish(); + const flush = buffer.flush(); + await firstPublish; + + for (const [index, snapshot] of pendingSnapshots.entries()) { + buffer.enqueue("session-1", [snapshot, createTerminalEvent(`terminal-${index}`)]); + } + + expect(published.map((request) => request.events)).toEqual([[inFlightSnapshot]]); + + response.resolve(new Response(null, { status: 204 })); + await flush; + + const deliveredSnapshots = published + .flatMap((request) => request.events) + .filter( + (event) => + event.type === "CUSTOM" && event.name === MOSOO_CUSTOM_EVENT.sessionTasksReplaced.name, + ); + expect(deliveredSnapshots).toEqual([inFlightSnapshot, pendingSnapshots.at(-1)]); + }); + + test("splits cross-generation task snapshots into byte-bounded batches", async () => { + const { buffer, published } = createBufferHarness(); + const snapshots = Array.from({ length: 3 }, (_, index) => + createLargeTaskSnapshot({ + driverInstanceId: `driver-${index}`, + marker: `snapshot-${index}`, + runId: `run-${index}`, + }), + ); + + buffer.enqueue("session-1", [snapshots[0]]); + expect(published).toHaveLength(0); + buffer.enqueue("session-1", [snapshots[1]]); + expect(published[0]?.events).toEqual([snapshots[0]]); + buffer.enqueue("session-1", [snapshots[2]]); + await buffer.flush(); + + expect(published).toHaveLength(3); + expect(published.map((request) => request.events.length)).toEqual([1, 1, 1]); + expect( + published.every((request) => serializedEventBytes(request.events) <= maxSerializedBatchBytes), + ).toBe(true); + }); + + test("retries failed bounded batches before events enqueued during the failed publish", async () => { const { buffer, published, pushResponse, waitForPublish } = createBufferHarness(); const failedResponse = createDeferred(); + const failedSnapshot = createLargeTaskSnapshot({ + driverInstanceId: "driver-1", + marker: "failed", + runId: "run-1", + }); + const nextSnapshot = createLargeTaskSnapshot({ + driverInstanceId: "driver-2", + marker: "next", + runId: "run-2", + }); pushResponse(failedResponse.promise); - buffer.enqueue("session-1", [ - { delta: "A", messageId: "assistant-1", type: "TEXT_MESSAGE_CONTENT" }, - ]); + buffer.enqueue("session-1", [failedSnapshot]); const firstPublish = waitForPublish(); const failedFlush = buffer.flush().catch((error: unknown) => error); await firstPublish; - buffer.enqueue("session-1", [ - { delta: "B", messageId: "assistant-1", type: "TEXT_MESSAGE_CONTENT" }, - ]); + buffer.enqueue("session-1", [nextSnapshot]); failedResponse.resolve( new Response(JSON.stringify({ error: "publish failed" }), { headers: { "content-type": "application/json" }, @@ -225,15 +375,43 @@ describe("SessionViewerEventDeliveryBuffer", () => { expect(await failedFlush).toBeInstanceOf(Error); await buffer.flush(); - expect(published).toEqual([ - { - events: [{ delta: "A", messageId: "assistant-1", type: "TEXT_MESSAGE_CONTENT" }], - sessionId: "session-1", - }, - { - events: [{ delta: "AB", messageId: "assistant-1", type: "TEXT_MESSAGE_CONTENT" }], - sessionId: "session-1", - }, + expect(published.map((request) => request.events)).toEqual([ + [failedSnapshot], + [failedSnapshot], + [nextSnapshot], + ]); + expect( + published.every((request) => serializedEventBytes(request.events) <= maxSerializedBatchBytes), + ).toBe(true); + }); + + test("keeps only the latest same-generation snapshot across repeated delivery failures", async () => { + const { buffer, published, pushResponse } = createBufferHarness(); + const snapshots = Array.from({ length: 5 }, (_, index) => + createLargeTaskSnapshot({ + driverInstanceId: "driver-1", + marker: `snapshot-${index}`, + runId: "run-1", + }), + ); + + for (const snapshot of snapshots) { + pushResponse(new Response(null, { status: 500 })); + buffer.enqueue("session-1", [snapshot]); + await buffer.flushSafely(); + } + await buffer.flush(); + + expect(published.map((request) => request.events)).toEqual([ + [snapshots[0]], + [snapshots[1]], + [snapshots[2]], + [snapshots[3]], + [snapshots[4]], + [snapshots[4]], ]); + expect( + published.every((request) => serializedEventBytes(request.events) <= maxSerializedBatchBytes), + ).toBe(true); }); }); diff --git a/apps/api/tests/session-viewer-socket-state-order.test.ts b/apps/api/tests/session-viewer-socket-state-order.test.ts new file mode 100644 index 00000000..4532ffec --- /dev/null +++ b/apps/api/tests/session-viewer-socket-state-order.test.ts @@ -0,0 +1,303 @@ +import { describe, expect, test } from "bun:test"; + +import { + applyAgUiEventsToSessionLiveState, + createInitialSessionLiveState, + createServerCustomEvent, + createViewerCustomEvent, + MOSOO_CUSTOM_EVENT, + parseAgUiSessionEventJson, +} from "@mosoo/ag-ui-session"; +import { createPromiseDeferred } from "@mosoo/effects"; + +import type { AuthenticatedViewer } from "../src/modules/auth/application/viewer-auth.service"; +import { writeSessionViewerSocketHeaders } from "../src/modules/sessions/infrastructure/session/socket-headers"; +import { SessionViewerSocketHub } from "../src/modules/sessions/infrastructure/session/viewer-socket-hub"; +import type { ApiBindings } from "../src/platform/cloudflare/worker-types"; +import { + createPublicHttpContractDatabase, + createPublicHttpTestBindings, + insertOwnerSession, + PUBLIC_API_TEST_IDS, +} from "./helpers/public-api-http-test-fixture"; +import type { SqliteD1Database } from "./helpers/sqlite-d1"; + +const DRIVER_ID = PUBLIC_API_TEST_IDS.driverOwner; +const RUN_ID = PUBLIC_API_TEST_IDS.run; +const SESSION_ID = PUBLIC_API_TEST_IDS.ownerSession; +const VIEWER: AuthenticatedViewer = { + email: "owner@example.com", + emailVerified: true, + id: PUBLIC_API_TEST_IDS.ownerAccount, + imageUrl: null, + name: "Owner", +}; + +class TestSocket { + attachment: unknown = null; + readonly frames: string[] = []; + readyState = WebSocket.OPEN; + + close(): void { + this.readyState = WebSocket.CLOSED; + } + + deserializeAttachment(): unknown { + return this.attachment; + } + + send(frame: string): void { + this.frames.push(frame); + } + + serializeAttachment(attachment: unknown): void { + this.attachment = attachment; + } +} + +function createContext(): { + ctx: DurableObjectState; + pending: Promise[]; +} { + const accepted: { socket: TestSocket; tags: string[] }[] = []; + const pending: Promise[] = []; + const storage = { + delete: async () => true, + deleteAlarm: async () => {}, + }; + const ctx = { + acceptWebSocket(socket: TestSocket, tags: string[]) { + accepted.push({ socket, tags }); + }, + getWebSockets(tag?: string) { + return accepted + .filter( + ({ socket, tags }) => + socket.readyState === WebSocket.OPEN && (tag === undefined || tags.includes(tag)), + ) + .map(({ socket }) => socket); + }, + storage, + waitUntil(promise: Promise) { + pending.push(promise); + }, + } as unknown as DurableObjectState; + + return { ctx, pending }; +} + +function deferFirstTaskSnapshotRead(database: SqliteD1Database): { + database: D1Database; + readStarted: Promise; + releaseRead: () => void; +} { + const readStarted = createPromiseDeferred(); + const releaseRead = createPromiseDeferred(); + let deferred = false; + + function wrapStatement(statement: D1PreparedStatement): D1PreparedStatement { + return new Proxy(statement, { + get(target, property) { + if (property === "all" || property === "first" || property === "raw") { + return async (...args: unknown[]) => { + const read = Reflect.get(target, property) as ( + ...values: unknown[] + ) => Promise; + const result = await read.apply(target, args); + + if (!deferred) { + deferred = true; + readStarted.resolve(); + await releaseRead.promise; + } + + return result; + }; + } + + if (property === "bind") { + return (...values: unknown[]) => wrapStatement(target.bind(...values)); + } + + const value: unknown = Reflect.get(target, property); + return typeof value === "function" ? value.bind(target) : value; + }, + }); + } + + return { + database: new Proxy(database, { + get(target, property) { + if (property === "prepare") { + return (query: string) => { + const statement = target.prepare(query); + return query.includes("session_agent_task_snapshot") + ? wrapStatement(statement) + : statement; + }; + } + + const value: unknown = Reflect.get(target, property); + return typeof value === "function" ? value.bind(target) : value; + }, + }), + readStarted: readStarted.promise, + releaseRead: () => { + releaseRead.resolve(); + }, + }; +} + +async function createDatabase(): Promise { + const database = await createPublicHttpContractDatabase(); + const now = Date.now(); + await insertOwnerSession(database); + database.execute(` + UPDATE session + SET last_run_id = '${RUN_ID}', status = 'RUNNING' + WHERE id = '${SESSION_ID}'; + + INSERT INTO session_run ( + agent_id, + created_at, + created_by_account_id, + driver_instance_id, + id, + model, + provider, + session_id, + started_at, + status, + trace_id, + trigger, + updated_at + ) VALUES ( + '${PUBLIC_API_TEST_IDS.agent}', + ${now}, + '${VIEWER.id}', + '${DRIVER_ID}', + '${RUN_ID}', + 'gpt-5.4', + 'openai', + '${SESSION_ID}', + ${now}, + 'running', + 'trace-1', + 'user_message', + ${now} + ); + + INSERT INTO session_agent_task_snapshot ( + driver_instance_id, + run_id, + seq, + session_id, + tasks_json + ) VALUES ( + '${DRIVER_ID}', + '${RUN_ID}', + 1, + '${SESSION_ID}', + '{"tasks":[{"taskId":"stale"}]}' + ); + `); + return database; +} + +describe("session viewer socket state ordering", () => { + test("linearizes a cold initial snapshot before a concurrent task broadcast", async () => { + const database = await createDatabase(); + const deferredRead = deferFirstTaskSnapshotRead(database); + const bindings = { + ...createPublicHttpTestBindings(database), + DB: deferredRead.database, + } as ApiBindings; + const { ctx, pending } = createContext(); + const hub = new SessionViewerSocketHub({ + ctx, + env: bindings, + getSessionId: () => SESSION_ID, + rememberSessionId: () => {}, + withSessionLogContext: (operation) => operation(), + }); + const sockets: TestSocket[] = []; + const originalWebSocketPair = Reflect.get(globalThis, "WebSocketPair"); + Reflect.set(globalThis, "WebSocketPair", function TestWebSocketPair() { + const client = new TestSocket(); + const server = new TestSocket(); + sockets.push(client, server); + return [client, server]; + }); + + try { + const headers = new Headers({ upgrade: "websocket" }); + writeSessionViewerSocketHeaders(headers, { + publicOrigin: "https://mosoo.ai", + appId: PUBLIC_API_TEST_IDS.app, + sessionId: SESSION_ID, + viewer: VIEWER, + }); + expect( + hub.connect(new Request("https://session.internal/viewer/ws", { headers })).status, + ).toBe(101); + const server = sockets[1]; + + if (!server) { + throw new Error("Expected a server websocket."); + } + + await deferredRead.readStarted; + database.execute(` + UPDATE session_agent_task_snapshot + SET seq = 2, tasks_json = '{"tasks":[{"taskId":"latest"}]}' + WHERE session_id = '${SESSION_ID}'; + `); + const latestTaskEvent = createServerCustomEvent( + MOSOO_CUSTOM_EVENT.sessionTasksReplaced.name, + { + driverInstanceId: DRIVER_ID, + runId: RUN_ID, + tasks: [{ taskId: "latest" }], + }, + ); + const broadcast = hub.broadcastEvents([latestTaskEvent]); + + await Promise.resolve(); + expect(server.frames).toEqual([]); + + deferredRead.releaseRead(); + await broadcast; + await Promise.all(pending); + + const firstEvents = server.frames.map(parseAgUiSessionEventJson); + expect(firstEvents.map((event) => event.type)).toEqual(["STATE_SNAPSHOT", "CUSTOM"]); + const state = applyAgUiEventsToSessionLiveState( + createInitialSessionLiveState({ sessionId: SESSION_ID, title: null, viewerId: VIEWER.id }), + firstEvents, + ); + expect(state.taskSnapshot?.tasks).toEqual([{ taskId: "latest" }]); + + await hub.handleSocketMessage( + server as unknown as WebSocket, + JSON.stringify( + createViewerCustomEvent(MOSOO_CUSTOM_EVENT.sessionSyncRequest.name, { + reason: "reconnect", + }), + ), + ); + + const reconnectSnapshot = parseAgUiSessionEventJson(server.frames.at(-1) ?? ""); + expect(reconnectSnapshot.type).toBe("STATE_SNAPSHOT"); + if (reconnectSnapshot.type !== "STATE_SNAPSHOT") { + throw new Error("Expected reconnect state snapshot."); + } + expect(reconnectSnapshot.snapshot.taskSnapshot?.tasks).toEqual([{ taskId: "latest" }]); + } finally { + if (originalWebSocketPair === undefined) { + Reflect.deleteProperty(globalThis, "WebSocketPair"); + } else { + Reflect.set(globalThis, "WebSocketPair", originalWebSocketPair); + } + } + }); +}); diff --git a/apps/api/tests/session-viewer-state.test.ts b/apps/api/tests/session-viewer-state.test.ts index 4de2257a..6179bde3 100644 --- a/apps/api/tests/session-viewer-state.test.ts +++ b/apps/api/tests/session-viewer-state.test.ts @@ -8,6 +8,7 @@ function createSessionViewerStateDatabase(): SqliteD1Database { database.execute(` CREATE TABLE session ( + archived_at integer, id text PRIMARY KEY NOT NULL, last_run_id text, status text NOT NULL, @@ -20,6 +21,7 @@ function createSessionViewerStateDatabase(): SqliteD1Database { created_at integer NOT NULL, deployment_version_id text, deployment_version_number integer, + driver_instance_id text, error_code text, error_details_json text, error_message text, @@ -34,6 +36,14 @@ function createSessionViewerStateDatabase(): SqliteD1Database { updated_at integer NOT NULL ); + CREATE TABLE session_agent_task_snapshot ( + driver_instance_id text NOT NULL, + run_id text NOT NULL, + seq integer NOT NULL, + session_id text PRIMARY KEY NOT NULL, + tasks_json text NOT NULL + ); + CREATE TABLE session_message ( content_text text NOT NULL, created_at integer NOT NULL, @@ -101,6 +111,7 @@ function createSessionViewerStateDatabase(): SqliteD1Database { INSERT INTO session_run ( completed_at, created_at, + driver_instance_id, id, model, provider, @@ -114,6 +125,7 @@ function createSessionViewerStateDatabase(): SqliteD1Database { VALUES ( NULL, 10, + 'driver-1', 'run-1', 'gpt-5.4', 'openai', @@ -199,6 +211,7 @@ describe("session viewer state", () => { expect(state.run.id).toBe("run-1"); expect(state.run.status).toBe("running"); + expect(state.infra.driverInstanceId).toBe("driver-1"); expect(state.files).toHaveLength(1); expect(state.messages).toHaveLength(1); expect(state.title).toBe("Investigate issue"); @@ -217,6 +230,89 @@ describe("session viewer state", () => { expect(state.messages).toHaveLength(1); }); + test("loads the schema-validated current task snapshot from the same canonical read", async () => { + const database = createSessionViewerStateDatabase(); + database.execute(` + INSERT INTO session_agent_task_snapshot ( + driver_instance_id, + run_id, + seq, + session_id, + tasks_json + ) + VALUES ( + 'driver-1', + 'run-1', + 7, + 'session-1', + '{"tasks":[{"taskId":"task-1","title":"Inspect"}]}' + ); + `); + + const state = await loadSessionViewerState(database, { + sessionId: "session-1", + viewerId: "viewer-1", + }); + + expect(state.taskSnapshot).toEqual({ + driverInstanceId: "driver-1", + runId: "run-1", + tasks: [{ taskId: "task-1", title: "Inspect" }], + }); + }); + + test.each([ + ["terminal run", "UPDATE session_run SET status = 'completed' WHERE id = 'run-1'"], + ["rescheduling session", "UPDATE session SET status = 'RESCHEDULING' WHERE id = 'session-1'"], + ["archived running session", "UPDATE session SET archived_at = 9 WHERE id = 'session-1'"], + ["new run", "UPDATE session SET last_run_id = 'run-2' WHERE id = 'session-1'"], + [ + "new driver generation", + "UPDATE session_run SET driver_instance_id = 'driver-2' WHERE id = 'run-1'", + ], + ])("does not leak task history across a %s boundary", async (_label, boundarySql) => { + const database = createSessionViewerStateDatabase(); + database.execute(` + INSERT INTO session_agent_task_snapshot ( + driver_instance_id, + run_id, + seq, + session_id, + tasks_json + ) + VALUES ('driver-1', 'run-1', 7, 'session-1', '{"tasks":[{"taskId":"stale"}]}'); + ${boundarySql}; + `); + + const state = await loadSessionViewerState(database, { + sessionId: "session-1", + viewerId: "viewer-1", + }); + + expect(state.taskSnapshot).toBeNull(); + }); + + test("fails closed when persisted task JSON is malformed", async () => { + const database = createSessionViewerStateDatabase(); + database.execute(` + INSERT INTO session_agent_task_snapshot ( + driver_instance_id, + run_id, + seq, + session_id, + tasks_json + ) + VALUES ('driver-1', 'run-1', 7, 'session-1', '{"tasks":"invalid"}'); + `); + + const state = await loadSessionViewerState(database, { + sessionId: "session-1", + viewerId: "viewer-1", + }); + + expect(state.taskSnapshot).toBeNull(); + }); + test("loads active permissions and readiness projections", async () => { const database = createSessionViewerStateDatabase(); database.execute(` diff --git a/apps/driver b/apps/driver index 50fde116..446f136e 160000 --- a/apps/driver +++ b/apps/driver @@ -1 +1 @@ -Subproject commit 50fde116e301efeeee83418f43fc9ee6759ac82d +Subproject commit 446f136ef1ba752d6f1b142e5bcee5e48273b965 diff --git a/apps/web/src/domains/runtime/session-stream/session-stream-actions.ts b/apps/web/src/domains/runtime/session-stream/session-stream-actions.ts index 64c48302..0bf3fa3a 100644 --- a/apps/web/src/domains/runtime/session-stream/session-stream-actions.ts +++ b/apps/web/src/domains/runtime/session-stream/session-stream-actions.ts @@ -4,6 +4,7 @@ import type { SessionPermissionRequestView, SessionRunView, } from "@mosoo/ag-ui-session"; +import type { AgentTask } from "@mosoo/contracts/session"; import { useCallback, useMemo } from "react"; import type { MutableRefObject } from "react"; @@ -37,6 +38,7 @@ interface UseSessionStreamActionsInput { } export function useSessionStreamActions(input: UseSessionStreamActionsInput): { + activeTasks: AgentTask[]; messages: SessionLiveState["messages"]; lifecycle: SessionLiveState["lifecycle"]; permissionRequests: SessionPermissionRequestView[]; @@ -150,6 +152,7 @@ export function useSessionStreamActions(input: UseSessionStreamActionsInput): { const streaming = useMemo(() => isSessionStreamStreaming(input.liveState), [input.liveState]); return { + activeTasks: input.liveState?.taskSnapshot?.tasks ?? [], lifecycle: input.liveState?.lifecycle ?? "IDLE", messages, permissionRequests, diff --git a/apps/web/src/domains/session/api/agent-session-retrieve.ts b/apps/web/src/domains/session/api/agent-session-retrieve.ts index 91359223..a74bdd03 100644 --- a/apps/web/src/domains/session/api/agent-session-retrieve.ts +++ b/apps/web/src/domains/session/api/agent-session-retrieve.ts @@ -20,6 +20,14 @@ const THREAD_AGENT_SESSION_RETRIEVE_QUERY = graphql(/* GraphQL */ ` reason status } + taskSnapshot { + runId + tasks { + taskId + taskType + title + } + } session { agentId archivedAt diff --git a/apps/web/src/gql/gql.ts b/apps/web/src/gql/gql.ts index 807c6f62..6141f9ad 100644 --- a/apps/web/src/gql/gql.ts +++ b/apps/web/src/gql/gql.ts @@ -74,7 +74,7 @@ type Documents = { "\n query McpOAuthFlowStatus($flowId: ULID!) {\n mcpOAuthFlowStatus(flowId: $flowId) {\n authorizationState\n errorMessage\n flowId\n serverId\n status\n subjectLabel\n }\n }\n": typeof types.McpOAuthFlowStatusDocument, "\n mutation OnboardingBootstrap($input: BootstrapOnboardingInput!) {\n onboardingBootstrap(input: $input) {\n completed\n organization {\n avatarUrl\n createdAt\n id\n name\n }\n }\n }\n": typeof types.OnboardingBootstrapDocument, "\n mutation RenameOrganization($input: RenameOrganizationInput!) {\n renameOrganization(input: $input) {\n avatarUrl\n createdAt\n id\n name\n }\n }\n": typeof types.RenameOrganizationDocument, - "\n query ThreadAgentSessionRetrieve($appId: ULID!, $sessionId: ULID!) {\n threadAgentSessionRetrieve(appId: $appId, sessionId: $sessionId) {\n capabilities {\n action\n reason\n status\n }\n recoverability {\n reason\n status\n }\n session {\n agentId\n archivedAt\n createdAt\n deploymentVersionId\n deploymentVersionNumber\n id\n kind\n lastMessageAt\n lastRun {\n completedAt\n createdAt\n deploymentVersionId\n deploymentVersionNumber\n error {\n code\n details\n message\n retryable\n }\n id\n model\n provider\n startedAt\n status\n traceId\n trigger\n updatedAt\n }\n model\n provider\n appId\n runtimeId\n status\n title\n updatedAt\n }\n }\n }\n": typeof types.ThreadAgentSessionRetrieveDocument, + "\n query ThreadAgentSessionRetrieve($appId: ULID!, $sessionId: ULID!) {\n threadAgentSessionRetrieve(appId: $appId, sessionId: $sessionId) {\n capabilities {\n action\n reason\n status\n }\n recoverability {\n reason\n status\n }\n taskSnapshot {\n runId\n tasks {\n taskId\n taskType\n title\n }\n }\n session {\n agentId\n archivedAt\n createdAt\n deploymentVersionId\n deploymentVersionNumber\n id\n kind\n lastMessageAt\n lastRun {\n completedAt\n createdAt\n deploymentVersionId\n deploymentVersionNumber\n error {\n code\n details\n message\n retryable\n }\n id\n model\n provider\n startedAt\n status\n traceId\n trigger\n updatedAt\n }\n model\n provider\n appId\n runtimeId\n status\n title\n updatedAt\n }\n }\n }\n": typeof types.ThreadAgentSessionRetrieveDocument, "\n query AgentSessionDiagnostics($appId: ULID!, $sessionId: ULID!) {\n agentSessionDiagnostics(appId: $appId, sessionId: $sessionId) {\n execution {\n binding {\n deploymentVersionId\n deploymentVersionNumber\n kind\n model\n provider\n runtimeId\n sessionId\n }\n skills {\n skillId\n skillName\n }\n tools {\n credentialMode\n serverId\n }\n }\n generatedAt\n nativeRuntimeRef {\n kind\n runtimeId\n status\n valuePreview\n }\n pendingPermissionCount\n session {\n deploymentVersionId\n deploymentVersionNumber\n id\n kind\n lastRun {\n deploymentVersionId\n deploymentVersionNumber\n id\n model\n provider\n status\n traceId\n }\n model\n provider\n runtimeId\n status\n title\n }\n }\n }\n": typeof types.AgentSessionDiagnosticsDocument, "\n mutation CreateAgentSession($input: CreateAgentSessionInput!) {\n createAgentSession(input: $input) {\n agentId\n archivedAt\n createdAt\n deploymentVersionId\n deploymentVersionNumber\n id\n kind\n lastMessageAt\n lastRun {\n completedAt\n createdAt\n deploymentVersionId\n deploymentVersionNumber\n error {\n code\n details\n message\n retryable\n }\n id\n model\n provider\n startedAt\n status\n traceId\n trigger\n updatedAt\n }\n model\n provider\n appId\n runtimeId\n status\n title\n type\n updatedAt\n }\n }\n": typeof types.CreateAgentSessionDocument, "\n query AgentSessionList(\n $agentId: ULID!\n $archived: Boolean\n $participantOnly: Boolean\n $appId: ULID!\n $type: SessionType\n ) {\n agentSessionList(\n agentId: $agentId\n archived: $archived\n participantOnly: $participantOnly\n appId: $appId\n type: $type\n ) {\n nodes {\n agentId\n archivedAt\n createdAt\n deploymentVersionId\n deploymentVersionNumber\n id\n kind\n lastMessageAt\n lastRun {\n completedAt\n createdAt\n deploymentVersionId\n deploymentVersionNumber\n error {\n code\n details\n message\n retryable\n }\n id\n model\n provider\n startedAt\n status\n traceId\n trigger\n updatedAt\n }\n model\n provider\n appId\n runtimeId\n status\n title\n type\n updatedAt\n }\n }\n }\n": typeof types.AgentSessionListDocument, @@ -165,7 +165,7 @@ const documents: Documents = { "\n query McpOAuthFlowStatus($flowId: ULID!) {\n mcpOAuthFlowStatus(flowId: $flowId) {\n authorizationState\n errorMessage\n flowId\n serverId\n status\n subjectLabel\n }\n }\n": types.McpOAuthFlowStatusDocument, "\n mutation OnboardingBootstrap($input: BootstrapOnboardingInput!) {\n onboardingBootstrap(input: $input) {\n completed\n organization {\n avatarUrl\n createdAt\n id\n name\n }\n }\n }\n": types.OnboardingBootstrapDocument, "\n mutation RenameOrganization($input: RenameOrganizationInput!) {\n renameOrganization(input: $input) {\n avatarUrl\n createdAt\n id\n name\n }\n }\n": types.RenameOrganizationDocument, - "\n query ThreadAgentSessionRetrieve($appId: ULID!, $sessionId: ULID!) {\n threadAgentSessionRetrieve(appId: $appId, sessionId: $sessionId) {\n capabilities {\n action\n reason\n status\n }\n recoverability {\n reason\n status\n }\n session {\n agentId\n archivedAt\n createdAt\n deploymentVersionId\n deploymentVersionNumber\n id\n kind\n lastMessageAt\n lastRun {\n completedAt\n createdAt\n deploymentVersionId\n deploymentVersionNumber\n error {\n code\n details\n message\n retryable\n }\n id\n model\n provider\n startedAt\n status\n traceId\n trigger\n updatedAt\n }\n model\n provider\n appId\n runtimeId\n status\n title\n updatedAt\n }\n }\n }\n": types.ThreadAgentSessionRetrieveDocument, + "\n query ThreadAgentSessionRetrieve($appId: ULID!, $sessionId: ULID!) {\n threadAgentSessionRetrieve(appId: $appId, sessionId: $sessionId) {\n capabilities {\n action\n reason\n status\n }\n recoverability {\n reason\n status\n }\n taskSnapshot {\n runId\n tasks {\n taskId\n taskType\n title\n }\n }\n session {\n agentId\n archivedAt\n createdAt\n deploymentVersionId\n deploymentVersionNumber\n id\n kind\n lastMessageAt\n lastRun {\n completedAt\n createdAt\n deploymentVersionId\n deploymentVersionNumber\n error {\n code\n details\n message\n retryable\n }\n id\n model\n provider\n startedAt\n status\n traceId\n trigger\n updatedAt\n }\n model\n provider\n appId\n runtimeId\n status\n title\n updatedAt\n }\n }\n }\n": types.ThreadAgentSessionRetrieveDocument, "\n query AgentSessionDiagnostics($appId: ULID!, $sessionId: ULID!) {\n agentSessionDiagnostics(appId: $appId, sessionId: $sessionId) {\n execution {\n binding {\n deploymentVersionId\n deploymentVersionNumber\n kind\n model\n provider\n runtimeId\n sessionId\n }\n skills {\n skillId\n skillName\n }\n tools {\n credentialMode\n serverId\n }\n }\n generatedAt\n nativeRuntimeRef {\n kind\n runtimeId\n status\n valuePreview\n }\n pendingPermissionCount\n session {\n deploymentVersionId\n deploymentVersionNumber\n id\n kind\n lastRun {\n deploymentVersionId\n deploymentVersionNumber\n id\n model\n provider\n status\n traceId\n }\n model\n provider\n runtimeId\n status\n title\n }\n }\n }\n": types.AgentSessionDiagnosticsDocument, "\n mutation CreateAgentSession($input: CreateAgentSessionInput!) {\n createAgentSession(input: $input) {\n agentId\n archivedAt\n createdAt\n deploymentVersionId\n deploymentVersionNumber\n id\n kind\n lastMessageAt\n lastRun {\n completedAt\n createdAt\n deploymentVersionId\n deploymentVersionNumber\n error {\n code\n details\n message\n retryable\n }\n id\n model\n provider\n startedAt\n status\n traceId\n trigger\n updatedAt\n }\n model\n provider\n appId\n runtimeId\n status\n title\n type\n updatedAt\n }\n }\n": types.CreateAgentSessionDocument, "\n query AgentSessionList(\n $agentId: ULID!\n $archived: Boolean\n $participantOnly: Boolean\n $appId: ULID!\n $type: SessionType\n ) {\n agentSessionList(\n agentId: $agentId\n archived: $archived\n participantOnly: $participantOnly\n appId: $appId\n type: $type\n ) {\n nodes {\n agentId\n archivedAt\n createdAt\n deploymentVersionId\n deploymentVersionNumber\n id\n kind\n lastMessageAt\n lastRun {\n completedAt\n createdAt\n deploymentVersionId\n deploymentVersionNumber\n error {\n code\n details\n message\n retryable\n }\n id\n model\n provider\n startedAt\n status\n traceId\n trigger\n updatedAt\n }\n model\n provider\n appId\n runtimeId\n status\n title\n type\n updatedAt\n }\n }\n }\n": types.AgentSessionListDocument, @@ -436,7 +436,7 @@ export function graphql(source: "\n mutation RenameOrganization($input: RenameO /** * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. */ -export function graphql(source: "\n query ThreadAgentSessionRetrieve($appId: ULID!, $sessionId: ULID!) {\n threadAgentSessionRetrieve(appId: $appId, sessionId: $sessionId) {\n capabilities {\n action\n reason\n status\n }\n recoverability {\n reason\n status\n }\n session {\n agentId\n archivedAt\n createdAt\n deploymentVersionId\n deploymentVersionNumber\n id\n kind\n lastMessageAt\n lastRun {\n completedAt\n createdAt\n deploymentVersionId\n deploymentVersionNumber\n error {\n code\n details\n message\n retryable\n }\n id\n model\n provider\n startedAt\n status\n traceId\n trigger\n updatedAt\n }\n model\n provider\n appId\n runtimeId\n status\n title\n updatedAt\n }\n }\n }\n"): typeof import('./graphql').ThreadAgentSessionRetrieveDocument; +export function graphql(source: "\n query ThreadAgentSessionRetrieve($appId: ULID!, $sessionId: ULID!) {\n threadAgentSessionRetrieve(appId: $appId, sessionId: $sessionId) {\n capabilities {\n action\n reason\n status\n }\n recoverability {\n reason\n status\n }\n taskSnapshot {\n runId\n tasks {\n taskId\n taskType\n title\n }\n }\n session {\n agentId\n archivedAt\n createdAt\n deploymentVersionId\n deploymentVersionNumber\n id\n kind\n lastMessageAt\n lastRun {\n completedAt\n createdAt\n deploymentVersionId\n deploymentVersionNumber\n error {\n code\n details\n message\n retryable\n }\n id\n model\n provider\n startedAt\n status\n traceId\n trigger\n updatedAt\n }\n model\n provider\n appId\n runtimeId\n status\n title\n updatedAt\n }\n }\n }\n"): typeof import('./graphql').ThreadAgentSessionRetrieveDocument; /** * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. */ diff --git a/apps/web/src/gql/graphql.ts b/apps/web/src/gql/graphql.ts index 5a64e859..00be0926 100644 --- a/apps/web/src/gql/graphql.ts +++ b/apps/web/src/gql/graphql.ts @@ -933,7 +933,7 @@ export type ThreadAgentSessionRetrieveQueryVariables = Exact<{ }>; -export type ThreadAgentSessionRetrieveQuery = { threadAgentSessionRetrieve: { capabilities: Array<{ action: AgentSessionActionCapabilityName, reason: string | null, status: AgentSessionActionCapabilityStatus }>, recoverability: { reason: string | null, status: AgentSessionRecoverabilityStatus }, session: { agentId: PlatformId, archivedAt: string | null, createdAt: string, deploymentVersionId: PlatformId | null, deploymentVersionNumber: number | null, id: PlatformId, kind: AgentKind, lastMessageAt: string | null, model: string, provider: string, appId: PlatformId, runtimeId: string, status: SessionStatus, title: string | null, updatedAt: string, lastRun: { completedAt: string | null, createdAt: string, deploymentVersionId: PlatformId | null, deploymentVersionNumber: number | null, id: PlatformId, model: string | null, provider: string | null, startedAt: string | null, status: RunStatus, traceId: string, trigger: SessionRunTrigger, updatedAt: string, error: { code: string, details: PrimitiveRecord, message: string, retryable: boolean } | null } | null } } }; +export type ThreadAgentSessionRetrieveQuery = { threadAgentSessionRetrieve: { capabilities: Array<{ action: AgentSessionActionCapabilityName, reason: string | null, status: AgentSessionActionCapabilityStatus }>, recoverability: { reason: string | null, status: AgentSessionRecoverabilityStatus }, taskSnapshot: { runId: PlatformId, tasks: Array<{ taskId: string, taskType: string | null, title: string | null }> } | null, session: { agentId: PlatformId, archivedAt: string | null, createdAt: string, deploymentVersionId: PlatformId | null, deploymentVersionNumber: number | null, id: PlatformId, kind: AgentKind, lastMessageAt: string | null, model: string, provider: string, appId: PlatformId, runtimeId: string, status: SessionStatus, title: string | null, updatedAt: string, lastRun: { completedAt: string | null, createdAt: string, deploymentVersionId: PlatformId | null, deploymentVersionNumber: number | null, id: PlatformId, model: string | null, provider: string | null, startedAt: string | null, status: RunStatus, traceId: string, trigger: SessionRunTrigger, updatedAt: string, error: { code: string, details: PrimitiveRecord, message: string, retryable: boolean } | null } | null } } }; export type AgentSessionDiagnosticsQueryVariables = Exact<{ appId: PlatformId; @@ -3103,6 +3103,14 @@ export const ThreadAgentSessionRetrieveDocument = /*#__PURE__*/ new TypedDocumen reason status } + taskSnapshot { + runId + tasks { + taskId + taskType + title + } + } session { agentId archivedAt diff --git a/apps/web/src/routes/agent/components/agent-session-panel-model-types.ts b/apps/web/src/routes/agent/components/agent-session-panel-model-types.ts index fd4de199..6233aa39 100644 --- a/apps/web/src/routes/agent/components/agent-session-panel-model-types.ts +++ b/apps/web/src/routes/agent/components/agent-session-panel-model-types.ts @@ -1,6 +1,6 @@ import type { SessionLiveState, SessionRunView } from "@mosoo/ag-ui-session"; import type { AgentReadiness } from "@mosoo/contracts/agent"; -import type { SessionSummary, SessionType } from "@mosoo/contracts/session"; +import type { AgentTask, SessionSummary, SessionType } from "@mosoo/contracts/session"; import type { Dispatch, KeyboardEvent, RefObject, SetStateAction } from "react"; import type { PermissionRequest } from "@/domains/runtime/use-session-stream"; @@ -38,6 +38,7 @@ export interface UseAgentSessionPanelModelInput { } export interface AgentSessionPanelModel { + activeTasks: AgentTask[]; activeSession: SessionSummary | null; activeSessionId: string | null; cancel: () => Promise; diff --git a/apps/web/src/routes/agent/components/agent-session-panel.tsx b/apps/web/src/routes/agent/components/agent-session-panel.tsx index 7825d674..2855dc26 100644 --- a/apps/web/src/routes/agent/components/agent-session-panel.tsx +++ b/apps/web/src/routes/agent/components/agent-session-panel.tsx @@ -21,6 +21,7 @@ import { uploadSessionResource } from "@/features/session-files/session-resource import { toAppId, toSessionId } from "@/routes/typed-id"; import { useTranslation } from "@/shared/i18n"; import { Button } from "@/shared/ui/button"; +import { ActiveAgentTasks } from "@/shared/ui/session-events"; import { isTruthy } from "../../../shared/lib/truthiness"; import { AgentReadinessBlockersBanner } from "./agent-readiness-blockers-banner"; @@ -229,6 +230,8 @@ export function AgentSessionPanel({ ) : null} + +
{model.isConversationLoading ? (
diff --git a/apps/web/src/routes/agent/components/use-agent-session-panel-model.ts b/apps/web/src/routes/agent/components/use-agent-session-panel-model.ts index c9002cea..6736493e 100644 --- a/apps/web/src/routes/agent/components/use-agent-session-panel-model.ts +++ b/apps/web/src/routes/agent/components/use-agent-session-panel-model.ts @@ -592,6 +592,7 @@ export function useAgentSessionPanelModel( ); return { + activeTasks: stream.activeTasks, activeSession, activeSessionId, cancel, diff --git a/apps/web/src/routes/threads/controller.tsx b/apps/web/src/routes/threads/controller.tsx index eca4c856..a22bdff8 100644 --- a/apps/web/src/routes/threads/controller.tsx +++ b/apps/web/src/routes/threads/controller.tsx @@ -20,9 +20,10 @@ import { } from "./list/view"; import { useThreadCompletionNotifications } from "./model/completion-notifications"; import { getMutationErrorMessage } from "./model/format"; +import { selectCurrentAgentTasks } from "./model/process"; import { useSelectedThreadReadSync } from "./model/read-sync"; import { useThreadRouteState } from "./model/route-state"; -import { SECTION_ORDER } from "./model/thread"; +import { isThreadWorking, SECTION_ORDER } from "./model/thread"; import { useThreadUiState } from "./model/ui-state"; import { useThreadActions } from "./model/use-actions"; import { useThreadQueries } from "./model/use-queries"; @@ -99,6 +100,14 @@ function ThreadsWorkspace({ onError: handleReadSyncError, selectedThread: threads.selectedThread, }); + const activeTasks = + threads.selectedThread === null + ? [] + : selectCurrentAgentTasks({ + currentRunId: threads.selectedThread.session.lastRun?.id ?? null, + snapshot: threads.retrieveQuery.data?.agentSessionRetrieve.taskSnapshot ?? null, + threadWorking: isThreadWorking(threads.selectedThread.session), + }); if (route.activeThreadId !== null) { return ( @@ -106,6 +115,7 @@ function ThreadsWorkspace({ {threads.selectedThread ? ( ) => string; +export function selectCurrentAgentTasks(input: { + currentRunId: string | null; + snapshot: { runId: string; tasks: readonly T[] } | null; + threadWorking: boolean; +}): readonly T[] { + return input.threadWorking && input.snapshot?.runId === input.currentRunId + ? input.snapshot.tasks + : []; +} + function formatProcessValue( value: number | null, unit: "ms" | "tokens", diff --git a/apps/web/src/routes/threads/process-modal/modal.tsx b/apps/web/src/routes/threads/process-modal/modal.tsx index bcc908f4..3813cf8d 100644 --- a/apps/web/src/routes/threads/process-modal/modal.tsx +++ b/apps/web/src/routes/threads/process-modal/modal.tsx @@ -13,7 +13,8 @@ import { DialogHeader, DialogTitle, } from "@/shared/ui/dialog"; -import { SessionEventDrawerCore } from "@/shared/ui/session-events"; +import { ActiveAgentTasks, SessionEventDrawerCore } from "@/shared/ui/session-events"; +import type { AgentTaskView } from "@/shared/ui/session-events"; import { AgentAvatar } from "../agent-avatar"; import { createProcessCopyText } from "../model/process"; @@ -22,6 +23,7 @@ import { ProcessEventRow, ProcessLegend, ProcessTimeline } from "./events"; import { formatTokens, formatTotalDuration } from "./format"; interface ThreadProcessModalProps { + activeTasks: readonly AgentTaskView[]; agent: AgentSummary | null; agentName: string; errorMessage: string | null; @@ -33,6 +35,7 @@ interface ThreadProcessModalProps { } export function ThreadProcessModal({ + activeTasks, agent, agentName, errorMessage, @@ -110,6 +113,8 @@ export function ThreadProcessModal({
+ + & { + taskType?: AgentTask["taskType"] | null; + title?: AgentTask["title"] | null; +}; + +export function ActiveAgentTasks({ + className, + tasks, +}: { + className?: string; + tasks: readonly AgentTaskView[]; +}): ReactElement | null { + const { t } = useTranslation(); + const headingId = useId(); + + if (tasks.length === 0) { + return null; + } + + return ( +
+
+

+ {t("sessionEvents.activeBackgroundTasks")} +

+ + {t("sessionEvents.activeBackgroundTaskCount", { count: String(tasks.length) })} + +
+ +
    + {tasks.map((task) => ( +
  • +
  • + ))} +
+
+ ); +} diff --git a/apps/web/src/shared/ui/session-events/index.ts b/apps/web/src/shared/ui/session-events/index.ts index a9987d83..c95dd69c 100644 --- a/apps/web/src/shared/ui/session-events/index.ts +++ b/apps/web/src/shared/ui/session-events/index.ts @@ -1,3 +1,5 @@ +export { ActiveAgentTasks } from "./active-agent-tasks"; +export type { AgentTaskView } from "./active-agent-tasks"; export { SESSION_EVENT_DOMAIN_TONE, getSessionEventChipTone, diff --git a/apps/web/tests/active-agent-tasks.test.tsx b/apps/web/tests/active-agent-tasks.test.tsx new file mode 100644 index 00000000..44c000d1 --- /dev/null +++ b/apps/web/tests/active-agent-tasks.test.tsx @@ -0,0 +1,43 @@ +import { describe, expect, test } from "bun:test"; + +import type { AgentTask } from "@mosoo/contracts/session"; +import { renderToStaticMarkup } from "react-dom/server"; + +import { I18nProvider } from "../src/shared/i18n"; +import { ActiveAgentTasks } from "../src/shared/ui/session-events"; + +function render(tasks: AgentTask[]): string { + return renderToStaticMarkup( + + + , + ); +} + +describe("active agent tasks", () => { + test("renders the authoritative current task list by task id", () => { + const html = render([ + { taskId: "task-search", taskType: "local", title: "Search repository" }, + { taskId: "task-review", title: "Review changes" }, + ]); + + expect(html).toContain("Active background tasks"); + expect(html).toContain("2 active"); + expect(html).toContain("Search repository"); + expect(html).toContain("task-search"); + expect(html).toContain("Review changes"); + expect(html).toContain("task-review"); + expect(html).toContain(" { + expect(render([])).toBe(""); + }); + + test("uses the task id when optional metadata is absent", () => { + const html = render([{ taskId: "task-id-only" }]); + + expect(html).toContain(">task-id-only"); + expect(html.match(/task-id-only/g)).toHaveLength(1); + }); +}); diff --git a/apps/web/tests/thread-process-model.test.ts b/apps/web/tests/thread-process-model.test.ts index 27620dc0..701e12a6 100644 --- a/apps/web/tests/thread-process-model.test.ts +++ b/apps/web/tests/thread-process-model.test.ts @@ -1,7 +1,10 @@ import { describe, expect, test } from "bun:test"; import type { ThreadProcessEvent } from "../src/routes/threads/model/process"; -import { getProcessEventVariant } from "../src/routes/threads/model/process"; +import { + getProcessEventVariant, + selectCurrentAgentTasks, +} from "../src/routes/threads/model/process"; function toolEvent(content: string): ThreadProcessEvent { return { @@ -20,4 +23,36 @@ describe("thread process event model", () => { expect(getProcessEventVariant(toolEvent("WebFetch details: {}"))).toBe("Web Fetch"); expect(getProcessEventVariant(toolEvent("WebSearch details: {}"))).toBe("Web Search"); }); + + test("clears cached tasks when the session list becomes terminal first", () => { + expect( + selectCurrentAgentTasks({ + currentRunId: "run-1", + snapshot: { runId: "run-1", tasks: ["old-task"] }, + threadWorking: false, + }), + ).toEqual([]); + }); + + test("does not show the previous run snapshot when a new run appears first", () => { + expect( + selectCurrentAgentTasks({ + currentRunId: "run-2", + snapshot: { runId: "run-1", tasks: ["old-task"] }, + threadWorking: true, + }), + ).toEqual([]); + }); + + test("keeps the matching active run snapshot", () => { + const tasks = ["current-task"]; + + expect( + selectCurrentAgentTasks({ + currentRunId: "run-2", + snapshot: { runId: "run-2", tasks }, + threadWorking: true, + }), + ).toBe(tasks); + }); }); diff --git a/bun.lock b/bun.lock index 8557b560..dafa3d36 100644 --- a/bun.lock +++ b/bun.lock @@ -60,22 +60,22 @@ "agent-driver": "./dist/driver.mjs", }, "dependencies": { - "@agentclientprotocol/sdk": "1.2.1", - "@anthropic-ai/claude-agent-sdk": "0.3.211", - "@anthropic-ai/sdk": "0.111.0", - "@modelcontextprotocol/client": "^2.0.0-alpha.2", - "@orpc/client": "^1.14.3", + "@agentclientprotocol/sdk": "1.4.0", + "@anthropic-ai/claude-agent-sdk": "0.3.251", + "@anthropic-ai/sdk": "0.121.0", + "@modelcontextprotocol/client": "^2.0.0", + "@orpc/client": "^1.15.0", "fflate": "^0.8.3", - "vestig": "^0.23.0", + "vestig": "^0.24.1", "zod": "^4.4.3", }, "devDependencies": { - "@openai/codex-sdk": "0.144.5", - "@types/bun": "1.3.14", - "@types/node": "^25.8.0", - "opencode-ai": "1.18.4", - "typescript": "^6.0.3", - "vite-plus": "0.2.5", + "@openai/codex": "0.150.1", + "@types/bun": "1.4.0", + "@types/node": "^26.3.0", + "opencode-ai": "1.18.23", + "typescript": "^7.0.2", + "vite-plus": "0.3.0", }, }, "apps/web": { @@ -286,29 +286,29 @@ "packages": { "@ag-ui/core": ["@ag-ui/core@0.0.57", "", { "dependencies": { "zod": "^3.22.4" } }, "sha512-gho1OWjNE6E3Rl7ZEZ1wr2CEpUHjLFU0FqzCZZk439TicLu+BfLCMkMokB07bMGlRmbJ60hM6LW60iOVauCx+Q=="], - "@agentclientprotocol/sdk": ["@agentclientprotocol/sdk@1.2.1", "", { "peerDependencies": { "zod": "^3.25.0 || ^4.0.0" } }, "sha512-jwYUdOQR7tc+Zfch53VL4JJyUNK/46q03uUTYb+PjECsmnNl94XFXOfYLJ8RBpMNidXd1rpOAVgb0vqD98xImA=="], + "@agentclientprotocol/sdk": ["@agentclientprotocol/sdk@1.4.0", "", { "peerDependencies": { "zod": "^3.25.0 || ^4.0.0" } }, "sha512-/eufudw+aFY1LKLolT6yFE6UMmYRl7fMJ/DEONSIyR6wI3slHWITBsANRGqXEY8FRzqUxwh7QEaGiZHcJPVThg=="], "@antfu/install-pkg": ["@antfu/install-pkg@1.1.0", "", { "dependencies": { "package-manager-detector": "^1.3.0", "tinyexec": "^1.0.1" } }, "sha512-MGQsmw10ZyI+EJo45CdSER4zEb+p31LpDAFp2Z3gkSd1yqVZGi0Ebx++YTEMonJy4oChEMLsxZ64j8FH6sSqtQ=="], - "@anthropic-ai/claude-agent-sdk": ["@anthropic-ai/claude-agent-sdk@0.3.211", "", { "optionalDependencies": { "@anthropic-ai/claude-agent-sdk-darwin-arm64": "0.3.211", "@anthropic-ai/claude-agent-sdk-darwin-x64": "0.3.211", "@anthropic-ai/claude-agent-sdk-linux-arm64": "0.3.211", "@anthropic-ai/claude-agent-sdk-linux-arm64-musl": "0.3.211", "@anthropic-ai/claude-agent-sdk-linux-x64": "0.3.211", "@anthropic-ai/claude-agent-sdk-linux-x64-musl": "0.3.211", "@anthropic-ai/claude-agent-sdk-win32-arm64": "0.3.211", "@anthropic-ai/claude-agent-sdk-win32-x64": "0.3.211" }, "peerDependencies": { "@anthropic-ai/sdk": ">=0.93.0", "@modelcontextprotocol/sdk": "^1.29.0", "zod": "^4.0.0" } }, "sha512-JhbLu6o1v2g9fjqkO+LDNPWrE0bgd9UeRQQ41JBGouAgows3KyPPYgU2WU0q7M2onuwQxR5plGDpas01F+oaUA=="], + "@anthropic-ai/claude-agent-sdk": ["@anthropic-ai/claude-agent-sdk@0.3.251", "", { "optionalDependencies": { "@anthropic-ai/claude-agent-sdk-darwin-arm64": "0.3.251", "@anthropic-ai/claude-agent-sdk-darwin-x64": "0.3.251", "@anthropic-ai/claude-agent-sdk-linux-arm64": "0.3.251", "@anthropic-ai/claude-agent-sdk-linux-arm64-musl": "0.3.251", "@anthropic-ai/claude-agent-sdk-linux-x64": "0.3.251", "@anthropic-ai/claude-agent-sdk-linux-x64-musl": "0.3.251", "@anthropic-ai/claude-agent-sdk-win32-arm64": "0.3.251", "@anthropic-ai/claude-agent-sdk-win32-x64": "0.3.251" }, "peerDependencies": { "@anthropic-ai/sdk": ">=0.93.0", "@modelcontextprotocol/sdk": "^1.29.0", "zod": "^4.0.0" } }, "sha512-DqSi8mH2tQYRlVV0G+lJnQ/WbjJZ/a+8cJ3vPuYoqh8esIIvXHm1ZOXV1UPGsFYRnbBytEoiSGitguEXd+sQ+Q=="], - "@anthropic-ai/claude-agent-sdk-darwin-arm64": ["@anthropic-ai/claude-agent-sdk-darwin-arm64@0.3.211", "", { "os": "darwin", "cpu": "arm64" }, "sha512-Iwhm4kfcs20LdXffZ2RGRjj+BFdUOrT/JjhGtICjlGlBPlrLkkkAiHtGzqO9K36v5B/kSIHwOw9CM836kYYPHQ=="], + "@anthropic-ai/claude-agent-sdk-darwin-arm64": ["@anthropic-ai/claude-agent-sdk-darwin-arm64@0.3.251", "", { "os": "darwin", "cpu": "arm64" }, "sha512-C23h+Dbddcc4gai95+lhm4/94am2IOq+bf3IdEDDS7EPqDQqajo2s5q1/ZSwq9rOc0RJLT7Ws72ZCzYJh67KSg=="], - "@anthropic-ai/claude-agent-sdk-darwin-x64": ["@anthropic-ai/claude-agent-sdk-darwin-x64@0.3.211", "", { "os": "darwin", "cpu": "x64" }, "sha512-sMBW1CLe2Hq4PwwvEbz9r8LxF84UErgB45TSf+iEa8M/EjYZCsXSoTRT0vKnN4TvrAN5mWoKgi39dkUxS9ybsA=="], + "@anthropic-ai/claude-agent-sdk-darwin-x64": ["@anthropic-ai/claude-agent-sdk-darwin-x64@0.3.251", "", { "os": "darwin", "cpu": "x64" }, "sha512-HrLnb3ggk+vMbymUvbosgPmxWp4W6Ot+0mNVjoBYDn5OZrTV3C3LRtbWf7jwcGyKMcg0xJf0AqiudQ2BRrlr8A=="], - "@anthropic-ai/claude-agent-sdk-linux-arm64": ["@anthropic-ai/claude-agent-sdk-linux-arm64@0.3.211", "", { "os": "linux", "cpu": "arm64" }, "sha512-orZm8p+BzVRZ7I8c5yD43hEZ5TvBZ+UbKTZTlvID00Y8HSn3M3rNX3sW4RUvNGF2e0eNMaXKysPyEHPEj3kqpg=="], + "@anthropic-ai/claude-agent-sdk-linux-arm64": ["@anthropic-ai/claude-agent-sdk-linux-arm64@0.3.251", "", { "os": "linux", "cpu": "arm64" }, "sha512-3E27F5j82EWOyEniRW7crmo0jWYYQmPDdP52jWq6Y6aytiyYIdUFMEGZZ6chVHNlZiU7GsjTa3teKk2xlQ68lQ=="], - "@anthropic-ai/claude-agent-sdk-linux-arm64-musl": ["@anthropic-ai/claude-agent-sdk-linux-arm64-musl@0.3.211", "", { "os": "linux", "cpu": "arm64" }, "sha512-X1eg+lCwNH2VXyqLQR722dsDDtWPfUnz7OtnmPVoNMxcMexDlSLMMuvm5fNNi94kYT0pxmBdhIvudew145JuHg=="], + "@anthropic-ai/claude-agent-sdk-linux-arm64-musl": ["@anthropic-ai/claude-agent-sdk-linux-arm64-musl@0.3.251", "", { "os": "linux", "cpu": "arm64" }, "sha512-RTUf7TPBUkQ6oV3pDkEdcD47I0uH0ZpvmZlXHnrLGznFqyLr+7XHupOxUMJD0Jwm9v5qeqpNiPAYB0dL4RYiHg=="], - "@anthropic-ai/claude-agent-sdk-linux-x64": ["@anthropic-ai/claude-agent-sdk-linux-x64@0.3.211", "", { "os": "linux", "cpu": "x64" }, "sha512-ohDS5EGKQvKiUUMtDNPjyWUDvaeIa+DlzUVjrZ8Y4hPtoWFpvOBtOFIYChJGpllwZ4YULS4H3gywVnLGB4do6Q=="], + "@anthropic-ai/claude-agent-sdk-linux-x64": ["@anthropic-ai/claude-agent-sdk-linux-x64@0.3.251", "", { "os": "linux", "cpu": "x64" }, "sha512-qCD87XPjcNM1u4ukqmcgpHDl3Y6l+cW8j7kPzH91Y5yLBYJ5xYZA2KtJ7AYNEPOteVyOGJw/efmva0S8CRr0fg=="], - "@anthropic-ai/claude-agent-sdk-linux-x64-musl": ["@anthropic-ai/claude-agent-sdk-linux-x64-musl@0.3.211", "", { "os": "linux", "cpu": "x64" }, "sha512-cR12YFMVGSj38074OYkkjgwhYeblgVRK3Uw7ZXw3ZTOevjQLASPajVruFuDRW07ixxTa1ZWxqI3kSsIl71RXYA=="], + "@anthropic-ai/claude-agent-sdk-linux-x64-musl": ["@anthropic-ai/claude-agent-sdk-linux-x64-musl@0.3.251", "", { "os": "linux", "cpu": "x64" }, "sha512-SzXrdy7jjrjJIuTG+VMRAY0YZ3G/8w21WuhAozwnvnEUAPo6NDv6lgFinu7NO8J6CPE+MK2sGze6JeCF+ljgUg=="], - "@anthropic-ai/claude-agent-sdk-win32-arm64": ["@anthropic-ai/claude-agent-sdk-win32-arm64@0.3.211", "", { "os": "win32", "cpu": "arm64" }, "sha512-AOIQRFO0YMDUCrG8W0NUWitBQQUB6fYdNW3SMcPPq3mXpYC/pCNURFyvRdrFm729xXkxDAP5OMLk9x2/IFrTmA=="], + "@anthropic-ai/claude-agent-sdk-win32-arm64": ["@anthropic-ai/claude-agent-sdk-win32-arm64@0.3.251", "", { "os": "win32", "cpu": "arm64" }, "sha512-FH1ZLcyc97ie3h7CSlIxA1ppXILgf0V8sFQrWnHyKBrthJXQkMHXhsTq9OZV/2KhXslNf2hTe0gHpZ1nCL1Gmg=="], - "@anthropic-ai/claude-agent-sdk-win32-x64": ["@anthropic-ai/claude-agent-sdk-win32-x64@0.3.211", "", { "os": "win32", "cpu": "x64" }, "sha512-pwzNuJg2xRBsv3kSSVVhgLdGAFxd5DqPkQX5ZLrT4uBZDJ+QM4xWKZyN8ZoFLLzNl+u0/4U83Q1ZQ0NdG/9JsQ=="], + "@anthropic-ai/claude-agent-sdk-win32-x64": ["@anthropic-ai/claude-agent-sdk-win32-x64@0.3.251", "", { "os": "win32", "cpu": "x64" }, "sha512-/jmtFIvfF0UFMxSZ8WV2Aus8aGA3+8Ft6AHvWuBJkY5jM2ubfqi6GnBjKCAL831TTllp2rKZlViANtWWRBvpvg=="], - "@anthropic-ai/sdk": ["@anthropic-ai/sdk@0.111.0", "", { "dependencies": { "json-schema-to-ts": "^3.1.1", "standardwebhooks": "^1.0.0" }, "peerDependencies": { "zod": "^3.25.0 || ^4.0.0" }, "optionalPeers": ["zod"], "bin": { "anthropic-ai-sdk": "bin/cli" } }, "sha512-1hUqKi+uJQoS5X90+InwHbFAXMvgq0DnsC5hVLEeSRaODiU5WvmqDAcVCmGS2wC0pN9Z8jtWCbWw7JLzeDdm/Q=="], + "@anthropic-ai/sdk": ["@anthropic-ai/sdk@0.121.0", "", { "dependencies": { "json-schema-to-ts": "^3.1.1", "standardwebhooks": "^1.0.0" }, "peerDependencies": { "zod": "^3.25.0 || ^4.0.0" }, "optionalPeers": ["zod"], "bin": { "anthropic-ai-sdk": "bin/cli" } }, "sha512-WFzwcH8l49CZHv0vQ9QQT51VJ1/DLDQfNSs9awpGlnKBdaSnqtdZa+tw/3cxxosTPIgK8uw1GqsJ2dSbCBD1Lg=="], "@apm-js-collab/code-transformer": ["@apm-js-collab/code-transformer@0.18.0", "", { "dependencies": { "@types/estree": "^1.0.8", "astring": "^1.9.0", "esquery": "^1.7.0", "meriyah": "^6.1.4", "semifies": "^1.0.0", "source-map": "^0.6.0" }, "bin": { "code-transformer": "cli.js" } }, "sha512-aN3Oq8r1J3gPJtCwErP664gM0+HhM1I1lujPr9TMTCcEl/joQQbpGpeMdts9B1+W2wHMsvioDMv5F4PvMWE6gw=="], @@ -774,9 +774,9 @@ "@mermaid-js/parser": ["@mermaid-js/parser@1.2.0", "", { "dependencies": { "@chevrotain/types": "~11.1.2" } }, "sha512-oYPyv8A4As1yH5Bx+04iQEQxXuIQDe0GKCNSRgao6z8AM9jixXIfP0vsppRLvGf+nKIOb9/LdpWA4YuJiVvESA=="], - "@modelcontextprotocol/client": ["@modelcontextprotocol/client@2.0.0-beta.4", "", { "dependencies": { "@modelcontextprotocol/core": "2.0.0-beta.4", "cross-spawn": "^7.0.5", "eventsource": "^3.0.2", "eventsource-parser": "^3.0.0", "jose": "^6.1.3", "pkce-challenge": "^5.0.0", "zod": "^4.2.0" } }, "sha512-VNHA/UXDk7mCpVl+jOg5B4WMRRD2OEl+it360Lhi6HiCbrIB2f6pZ0cqSDKXQVUtZvqnhdQHzZAM9h8EKWhq/A=="], + "@modelcontextprotocol/client": ["@modelcontextprotocol/client@2.0.0", "", { "dependencies": { "@modelcontextprotocol/core": "2.0.0", "cross-spawn": "^7.0.5", "eventsource": "^3.0.2", "eventsource-parser": "^3.0.0", "jose": "^6.1.3", "pkce-challenge": "^5.0.0", "zod": "^4.2.0" } }, "sha512-8f1OghQ2rjzIOfqgUCP+8GiUWqRs89njoWLNqAe8kWmDePv3s1fZXseej+QXemssEuuOvLLmLO/kqM3IQHtISw=="], - "@modelcontextprotocol/core": ["@modelcontextprotocol/core@2.0.0-beta.4", "", { "dependencies": { "zod": "^4.2.0" } }, "sha512-nsMXd4wQBKzmph6r+WOhum+mXjDYljTAqwY/XUg3hLtvNOQ8+JVqBSJOVCMJvx9lXhTpTOPrGZ3BuNiaNPjSvg=="], + "@modelcontextprotocol/core": ["@modelcontextprotocol/core@2.0.0", "", { "dependencies": { "zod": "^4.2.0" } }, "sha512-pJCEwGG7Lfr/+PQp9ZTwKXNeO5wzbfKL7H3MYpCorM4oFBoQrdjnBgEoqG+RjhsvS1FKrDbKux+M1HhlnGWqcA=="], "@modelcontextprotocol/sdk": ["@modelcontextprotocol/sdk@1.29.0", "", { "dependencies": { "@hono/node-server": "^1.19.9", "ajv": "^8.17.1", "ajv-formats": "^3.0.1", "content-type": "^1.0.5", "cors": "^2.8.5", "cross-spawn": "^7.0.5", "eventsource": "^3.0.2", "eventsource-parser": "^3.0.0", "express": "^5.2.1", "express-rate-limit": "^8.2.1", "hono": "^4.11.4", "jose": "^6.1.3", "json-schema-typed": "^8.0.2", "pkce-challenge": "^5.0.0", "raw-body": "^3.0.0", "zod": "^3.25 || ^4.0", "zod-to-json-schema": "^3.25.1" }, "peerDependencies": { "@cfworker/json-schema": "^4.1.1" }, "optionalPeers": ["@cfworker/json-schema"] }, "sha512-zo37mZA9hJWpULgkRpowewez1y6ML5GsXJPY8FI0tBBCd77HEvza4jDqRKOXgHNn867PVGCyTdzqpz0izu5ZjQ=="], @@ -828,21 +828,19 @@ "@nodelib/fs.walk": ["@nodelib/fs.walk@1.2.8", "", { "dependencies": { "@nodelib/fs.scandir": "2.1.5", "fastq": "^1.6.0" } }, "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg=="], - "@openai/codex": ["@openai/codex@0.144.5", "", { "optionalDependencies": { "@openai/codex-darwin-arm64": "npm:@openai/codex@0.144.5-darwin-arm64", "@openai/codex-darwin-x64": "npm:@openai/codex@0.144.5-darwin-x64", "@openai/codex-linux-arm64": "npm:@openai/codex@0.144.5-linux-arm64", "@openai/codex-linux-x64": "npm:@openai/codex@0.144.5-linux-x64", "@openai/codex-win32-arm64": "npm:@openai/codex@0.144.5-win32-arm64", "@openai/codex-win32-x64": "npm:@openai/codex@0.144.5-win32-x64" }, "bin": { "codex": "bin/codex.js" } }, "sha512-jjB+K+OMv572mKhS+2QuLxWXDJNdpwbPenf+V+8bdq7wg4Scqt3cn6WEekD8wPqDVZqck0HSX17K9rD9kbDJQA=="], + "@openai/codex": ["@openai/codex@0.150.1", "", { "optionalDependencies": { "@openai/codex-darwin-arm64": "npm:@openai/codex@0.150.1-darwin-arm64", "@openai/codex-darwin-x64": "npm:@openai/codex@0.150.1-darwin-x64", "@openai/codex-linux-arm64": "npm:@openai/codex@0.150.1-linux-arm64", "@openai/codex-linux-x64": "npm:@openai/codex@0.150.1-linux-x64", "@openai/codex-win32-arm64": "npm:@openai/codex@0.150.1-win32-arm64", "@openai/codex-win32-x64": "npm:@openai/codex@0.150.1-win32-x64" }, "bin": { "codex": "bin/codex.js" } }, "sha512-knrbhpJH3mEULAVStcZW4F5WEt9MQhBj6KFOonBSIUGTLcHlu9CE7FRmr95E33y94+sWNZSeVBBV/kYvlfgxkQ=="], - "@openai/codex-darwin-arm64": ["@openai/codex@0.144.5-darwin-arm64", "", { "os": "darwin", "cpu": "arm64" }, "sha512-zcT6NfBCqLFt+BReNSETTZW6v6PdbH0dzNtm9j7l7mDGqwPbKZDGJdnpkBao2389I0ZacyIKgSZoI0vez1d4Dw=="], + "@openai/codex-darwin-arm64": ["@openai/codex@0.150.1-darwin-arm64", "", { "os": "darwin", "cpu": "arm64" }, "sha512-Z614kKSI3/p+YMotBJMxCc4kvRCYEgsVmnaCG6U8HDraqTFxYcQYQ79B4/GBCBS88/KtacHI6qzjA+4Mu5BynA=="], - "@openai/codex-darwin-x64": ["@openai/codex@0.144.5-darwin-x64", "", { "os": "darwin", "cpu": "x64" }, "sha512-//Mo0m1MwaoT6psu5xsmofXpKx4/0irIkeq10xJvk59+886EG355ibjA+ZmlRcKhE3bLjsKD7p81nTbAdRL/bw=="], + "@openai/codex-darwin-x64": ["@openai/codex@0.150.1-darwin-x64", "", { "os": "darwin", "cpu": "x64" }, "sha512-vcvJ7Q4IP2vA5ZR3v9Pj9VLKtlhD7efujxHqqW/NJ8F2/XqTD4iPjErV07190Om3wONW48yploTgul16QUk9Mg=="], - "@openai/codex-linux-arm64": ["@openai/codex@0.144.5-linux-arm64", "", { "os": "linux", "cpu": "arm64" }, "sha512-zAHggxVwR2TBxKmybXY7ZMiB0G8DMonY2YPdwNNjwXcf+LOIqNGgswwNCDMbP/HEe6r8j+R9ZX/yYoo8f+n/RQ=="], + "@openai/codex-linux-arm64": ["@openai/codex@0.150.1-linux-arm64", "", { "os": "linux", "cpu": "arm64" }, "sha512-U0BY5UDsCy1up/O2YzGtVtDbcumNerVyZVVx55PW9gGQsFOL0utOyBJiG/UiocFAXix8JNvNJxYMRt2Jx0FJKg=="], - "@openai/codex-linux-x64": ["@openai/codex@0.144.5-linux-x64", "", { "os": "linux", "cpu": "x64" }, "sha512-FalLJlBQGFdK8Gc3kj9sa/ekNdgkHhUawLaKkvy5CtB18JaP2YxtTP/Pe1pD2iBiq8mMUliRnafpF6AdBdQMbg=="], + "@openai/codex-linux-x64": ["@openai/codex@0.150.1-linux-x64", "", { "os": "linux", "cpu": "x64" }, "sha512-B3Bu5MF/G9qFhbSMrdyZz9ocknXOo45DnOdWrREJaxWTjiuUO1ogoDR0/ttdc29JRFkbbXs3aC+Td8vIx3X0oA=="], - "@openai/codex-sdk": ["@openai/codex-sdk@0.144.5", "", { "dependencies": { "@openai/codex": "0.144.5" } }, "sha512-90wHPEGyk74On6gwQPNtw+wzuDJ2zYpiVADDxw43S1cWn3QbBh/21zFS2xlAWWCrdY0gE90yXfmmrzwdUDlBGw=="], + "@openai/codex-win32-arm64": ["@openai/codex@0.150.1-win32-arm64", "", { "os": "win32", "cpu": "arm64" }, "sha512-8I7N3ppKS1ql9GUr2RHS5Gw+KkRCKfIMIDDK/brxq6HizLgHoDK4CIR0OTvEgdX3Yh/2reBbxr9P4L1e+61e+Q=="], - "@openai/codex-win32-arm64": ["@openai/codex@0.144.5-win32-arm64", "", { "os": "win32", "cpu": "arm64" }, "sha512-0Pj7iqjEOEvPQPO3kFfCy9vGX4BTu76ChFFZHr2eNNIfVc3FOENAv/X98u4L+iIUtDOK9DbqmfUudW3DPapshg=="], - - "@openai/codex-win32-x64": ["@openai/codex@0.144.5-win32-x64", "", { "os": "win32", "cpu": "x64" }, "sha512-DnsSTlnnzleTxvLwIGnBitKInscxn2I7qASqosS8Fv+qysBygd+ZiBn/SQsRCgQ28PAlsNzmd3Gf3ZTecolAmg=="], + "@openai/codex-win32-x64": ["@openai/codex@0.150.1-win32-x64", "", { "os": "win32", "cpu": "x64" }, "sha512-1P1tscFnw4A4ip/WN1R5WYPdfuAlxLctPpcE1QAPQnOtbkJR5NX8da4kNTQmiDN/Flx3TLJPDTRHP9Jn435eJg=="], "@opentelemetry/api": ["@opentelemetry/api@1.9.1", "", {}, "sha512-gLyJlPHPZYdAk1JENA9LeHejZe1Ti77/pTeFm/nMXmQH/HFZlcS/O2XJB+L8fkbrNSqhdtlvjBVjxwUYanNH5Q=="], @@ -1208,8 +1206,6 @@ "@rolldown/binding-openharmony-arm64": ["@rolldown/binding-openharmony-arm64@1.2.4", "", { "os": "none", "cpu": "arm64" }, "sha512-e0F355MSTMm3+UOqtV3L24gFUp2N5m1f8L/7d56deik6va+AXdrt9F8LbzGpeWGWRbZEDq4m8NVnJDeBtf9DZg=="], - "@rolldown/binding-wasm32-wasi": ["@rolldown/binding-wasm32-wasi@1.1.5", "", { "dependencies": { "@emnapi/core": "1.11.1", "@emnapi/runtime": "1.11.1", "@napi-rs/wasm-runtime": "^1.1.6" }, "cpu": "none" }, "sha512-VbQGPX2b4r48TAMIM2cjgluIM1HYutm4pcTEJsle7iEP7sB1dFqtPLBVbdLAZCxy1txCcPxf4QFf4v8uvltPqA=="], - "@rolldown/binding-win32-arm64-msvc": ["@rolldown/binding-win32-arm64-msvc@1.2.4", "", { "os": "win32", "cpu": "arm64" }, "sha512-AWLi0uBRYh6QlE7OKhiz+phZC0qwtij2QZmhmOdsLdFn64m7oMpooE9ICE3lhm9xMb4SpDo2WbHcxX1iFLFtqw=="], "@rolldown/binding-win32-x64-msvc": ["@rolldown/binding-win32-x64-msvc@1.2.4", "", { "os": "win32", "cpu": "x64" }, "sha512-UwSDJOg3dqCAejWdxclJjCsh3Qq4vLYMDxmyHqo1btz3stK2VqgwNd3mm5tuIwzSlGIQ/1H9Hr+Zn09mrezNqQ=="], @@ -1280,7 +1276,7 @@ "@types/aria-query": ["@types/aria-query@5.0.4", "", {}, "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw=="], - "@types/bun": ["@types/bun@1.3.14", "", { "dependencies": { "bun-types": "1.3.14" } }, "sha512-h1hFqFVcvAvD9j9K7ZW7vd82aSA+rTdznZa+5bwvCwqSB1jmmfLcbIWhOLx1/+boy/xmjgCs/OMUL8hRJSmnPw=="], + "@types/bun": ["@types/bun@1.4.0", "", { "dependencies": { "bun-types": "1.4.0" } }, "sha512-K+lZULY23vRgK/CfTjFIV+tyifaNdSMlPh9j+6mQ/cLfpOznLyAuzgV/JQysyECpkBQLVMSyvjlr2fBUSA9wFQ=="], "@types/chai": ["@types/chai@5.2.3", "", { "dependencies": { "@types/deep-eql": "*", "assertion-error": "^2.0.1" } }, "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA=="], @@ -1366,7 +1362,7 @@ "@types/ms": ["@types/ms@2.1.0", "", {}, "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA=="], - "@types/node": ["@types/node@25.9.5", "", { "dependencies": { "undici-types": ">=7.24.0 <7.24.7" } }, "sha512-OScDchr2fwuUmWdf4kZ9h7PcJiYDVInhJizG/biAq3cAvqwYktuy/TYGGdZNMtNTFUP7rnb0NU4TUdm82kt4Rg=="], + "@types/node": ["@types/node@26.4.0", "", { "dependencies": { "undici-types": "~8.3.0" } }, "sha512-faiGnoIrLH/V8cibOMEAZ8pMw6oXqSukl29ra4mN8GdaB2ZewzeaLj+INpV5N+Z1eKWzY+IzaIZH2EIR6YZRNQ=="], "@types/react": ["@types/react@19.2.18", "", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-AnzbBERsrLKtk2XSfTbYRLjQPdy116Sty4q+T+Bp3IC4l6jNBvreVPAHmpq9qhXQM7CXZPjLVmGMw9sy+hxQ3w=="], @@ -1380,29 +1376,69 @@ "@typescript-eslint/types": ["@typescript-eslint/types@8.64.0", "", {}, "sha512-qjhfuTfLXjA4IOzXvz0rTjT01BqEiIgPoUeMwiEjnaHKJMTNo8rH5pYW1a2L/0Dnux2fPC85AeyJoWaGa8WxTA=="], + "@typescript/typescript-aix-ppc64": ["@typescript/typescript-aix-ppc64@7.0.2", "", { "os": "aix", "cpu": "ppc64" }, "sha512-MTKKkWB7p/0E9xi1d1tHtZ5PiLkGEMIq88pK2CubZjOsLtYTLqhgIgi6zepFa+9GHZ6h05NMCkQxGKiPXMxXtQ=="], + + "@typescript/typescript-darwin-arm64": ["@typescript/typescript-darwin-arm64@7.0.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-gowzar9MwS/aRWp6f3a4KUqzRjAZjOsmGNCM6LcTgXum+dBfgsBVMN+AgvOCCbguXyick6LJhpBszxMebJ8syA=="], + + "@typescript/typescript-darwin-x64": ["@typescript/typescript-darwin-x64@7.0.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-SZ9xZInqApNlNGc9s0W1VSsktYSOe9cFqNOIqmN1Gs8SmkjKZYFt017G4VwPxASInODuAdbTW7sXiFUf893RgA=="], + + "@typescript/typescript-freebsd-arm64": ["@typescript/typescript-freebsd-arm64@7.0.2", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-W5NH4y/J0plIIS5b2xvTEkU7JFxyqdMAOgf+Ilhl0vHQXKO5dZoxd+C/jEtq56c4F3wk71RB4BMRQ2XdI+bwYQ=="], + + "@typescript/typescript-freebsd-x64": ["@typescript/typescript-freebsd-x64@7.0.2", "", { "os": "freebsd", "cpu": "x64" }, "sha512-UMGDx5sTpzNw3WiPebH7l90IWfJggEd+egHt/q6p7/Cm3zqoV7VxkGXt+3DxPIw8CcmvAB0j3sVVfbhX+M4Tpw=="], + + "@typescript/typescript-linux-arm": ["@typescript/typescript-linux-arm@7.0.2", "", { "os": "linux", "cpu": "arm" }, "sha512-gffT3xPz9sR7j/YJExkyPntrI0P2EP9XbOyWzth2/Gs0RstK+90RBcO0ncXoXy/beYll1SXw846Nf2zdnEz0QQ=="], + + "@typescript/typescript-linux-arm64": ["@typescript/typescript-linux-arm64@7.0.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-Qh4eU4/y3yDjnfjjyPYihMj5/ODIlmt+Bzu17OI+fiSRDW57QmU5SiN63exPRNJPKUzcc1INa1NXdrJ+MqHjUQ=="], + + "@typescript/typescript-linux-loong64": ["@typescript/typescript-linux-loong64@7.0.2", "", { "os": "linux", "cpu": "none" }, "sha512-uEHck9i8hoAzXPiYRib1O7miOnz23SxIeVl6F4LXox+qov1K35jHcEW6VHKvZI+pyvl7fZEP4MCU5LYvIq1GuQ=="], + + "@typescript/typescript-linux-mips64el": ["@typescript/typescript-linux-mips64el@7.0.2", "", { "os": "linux", "cpu": "none" }, "sha512-R4KvAMnE43W5Qeqb0Ly56O3mWMWIAgsMyz36DCaycd5nbg/9kzm0liw3JocfRqyJY0KPmzFjbswozXyW0DnIYA=="], + + "@typescript/typescript-linux-ppc64": ["@typescript/typescript-linux-ppc64@7.0.2", "", { "os": "linux", "cpu": "ppc64" }, "sha512-DORx5b3sd/4S7eayxm4FQv+A7CrkUIGRaHiwI8oiHTAI1fAPWhF4J0vAlkC8biAlHSVVwxMQ3tjZ2/DVbnQiiA=="], + + "@typescript/typescript-linux-riscv64": ["@typescript/typescript-linux-riscv64@7.0.2", "", { "os": "linux", "cpu": "none" }, "sha512-wf0jqEDOjrPRnKwYRyyJDRo11KMbvMFrU+q4zqKyChODBzvlkbhNQfKvLxQCcwTpdDaXSHZTVuh0JoCrKCUMHQ=="], + + "@typescript/typescript-linux-s390x": ["@typescript/typescript-linux-s390x@7.0.2", "", { "os": "linux", "cpu": "s390x" }, "sha512-IkwJc3L7yhytWd/ewjyxNDfOmswCm9GWMJT/ue/dU4aZNbwZeYAetq42VyLmsmSjvoX7z74X6ZaYCtzAr0EuGw=="], + + "@typescript/typescript-linux-x64": ["@typescript/typescript-linux-x64@7.0.2", "", { "os": "linux", "cpu": "x64" }, "sha512-EYdf2cNg7rgCWJnxCdJ+F3V39O8ihb37eHAu1LK8oAFizgTQbPOK7zHHXbPt8rX24COqODXeI3sIf0fCXG7H/A=="], + + "@typescript/typescript-netbsd-arm64": ["@typescript/typescript-netbsd-arm64@7.0.2", "", { "os": "none", "cpu": "arm64" }, "sha512-+polYF4MF04aPpO5FTkHran9yUQDSXqy5GiSDKpsll5jy3l3+g9QLhpf39T+ePtefhXLOGrLl0QIjkQP6VnelA=="], + + "@typescript/typescript-netbsd-x64": ["@typescript/typescript-netbsd-x64@7.0.2", "", { "os": "none", "cpu": "x64" }, "sha512-8YIT0EHM/3dq10ZOVF/A7pc/YSMtbcecct4rWtexrnSCHOPcpC2KTLXfTCR6vDpnSiY12heNb1GiN/wu+T/FyA=="], + + "@typescript/typescript-openbsd-arm64": ["@typescript/typescript-openbsd-arm64@7.0.2", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-APT8+ClYnuYm1u9+kgGXoMj2VzWzcymwh2gNSQVySHfkRDGOTVkoWLjCmOQSaO+PoqQ57B0flRp9SA+7GnnkzQ=="], + + "@typescript/typescript-openbsd-x64": ["@typescript/typescript-openbsd-x64@7.0.2", "", { "os": "openbsd", "cpu": "x64" }, "sha512-yX7s+Q0Dln0Dt9tEzZsAjXXR/+ytBM7AlglaqyeMPxQszJ1JhlJdZ6jLA+IzldHtflX81em7lDao1xXu+aRRkg=="], + + "@typescript/typescript-sunos-x64": ["@typescript/typescript-sunos-x64@7.0.2", "", { "os": "sunos", "cpu": "x64" }, "sha512-dLJDGaLZ1D4HPQn62u1n8mBDkJREwMsAkCdkwd4Ieqw+x3TUyTsqY0YiBCtE6H6OzzgGk3iuZ3vFWRS+E8/d1g=="], + + "@typescript/typescript-win32-arm64": ["@typescript/typescript-win32-arm64@7.0.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-Gyl1Vy6OsWesLzmq+EP0Fb7b4Nid5232AvcA2SFcdYreldpNtYFFofPjnt62y9hQy7VTaZp65ICJjuAQRaVcIQ=="], + + "@typescript/typescript-win32-x64": ["@typescript/typescript-win32-x64@7.0.2", "", { "os": "win32", "cpu": "x64" }, "sha512-0BQ3HkAHHlKLSp1qRvf3SUhGpGsDuhB/jgFw75guyqbxJqEaS0Cw/VFO8i2nHglJUzQCRtMMR/IBAKE3ETMC4g=="], + "@ungap/structured-clone": ["@ungap/structured-clone@1.3.3", "", {}, "sha512-60YRaenCQcVjYEKOcG824+DRGGIQ3VKErcBoAEDJZz5bKIs2ZG+X/H9Nk+Q6EVkwJk5QNApxbrc5QtBSwtrXAg=="], "@upsetjs/venn.js": ["@upsetjs/venn.js@2.0.0", "", { "optionalDependencies": { "d3-selection": "^3.0.0", "d3-transition": "^3.0.1" } }, "sha512-WbBhLrooyePuQ1VZxrJjtLvTc4NVfpOyKx0sKqioq9bX1C1m7Jgykkn8gLrtwumBioXIqam8DLxp88Adbue6Hw=="], "@vitejs/plugin-react": ["@vitejs/plugin-react@6.0.5", "", { "dependencies": { "@rolldown/pluginutils": "^1.0.1" }, "peerDependencies": { "@rolldown/plugin-babel": "^0.1.7 || ^0.2.0", "babel-plugin-react-compiler": "^1.0.0", "vite": "^8.0.0" }, "optionalPeers": ["@rolldown/plugin-babel", "babel-plugin-react-compiler"] }, "sha512-BOVzne/NL162sMdResB25mUv+vWMF5NoAjNf09TeGlE7ZpszZWSD3winycicLJw72yeVsoCn/2kOhEuCvEShMA=="], - "@vitest/browser": ["@vitest/browser@4.1.10", "", { "dependencies": { "@blazediff/core": "1.9.1", "@vitest/mocker": "4.1.10", "@vitest/utils": "4.1.10", "magic-string": "^0.30.21", "pngjs": "^7.0.0", "sirv": "^3.0.2", "tinyrainbow": "^3.1.0", "ws": "^8.19.0" }, "peerDependencies": { "vitest": "4.1.10" } }, "sha512-UDwuWGwXj646CBx/bQHOaJSX7np0I8JL/UKQYa1e4QrVHH8VdWtx8eaOuf8sy0ShwDgR6NjJAsp5eF6vjF6qng=="], + "@vitest/browser": ["@vitest/browser@4.1.11", "", { "dependencies": { "@blazediff/core": "1.9.1", "@vitest/mocker": "4.1.11", "@vitest/utils": "4.1.11", "magic-string": "^0.30.21", "pngjs": "^7.0.0", "sirv": "^3.0.2", "tinyrainbow": "^3.1.0", "ws": "^8.19.0" }, "peerDependencies": { "vitest": "4.1.11" } }, "sha512-bwMovvAeuTFOK5kIFevw4VEf+1gVEICv4SYK4k3knJOxl6b1zEWud8mYKD73e1B0odAn174h1MofURy2TPWf3w=="], - "@vitest/browser-preview": ["@vitest/browser-preview@4.1.10", "", { "dependencies": { "@testing-library/dom": "^10.4.1", "@testing-library/user-event": "^14.6.1", "@vitest/browser": "4.1.10" }, "peerDependencies": { "vitest": "4.1.10" } }, "sha512-14MJrL59ZFkqXLjwfSk6RzTDy5Czf9UG4+8q8L6Gxjs2aPjEce/cVNYV14bXAc2BvMjUNu904+ZEZA1Xc1wtvQ=="], + "@vitest/browser-preview": ["@vitest/browser-preview@4.1.11", "", { "dependencies": { "@testing-library/dom": "^10.4.1", "@testing-library/user-event": "^14.6.1", "@vitest/browser": "4.1.11" }, "peerDependencies": { "vitest": "4.1.11" } }, "sha512-iPKSE6Ibayey6HFgK1V1/aHgyhx7HSRk1YMi+lnBZGmlIiNV5Uc7xRkD9Su8RDylTxDECK23t7kTHdRKoqSYDQ=="], - "@vitest/expect": ["@vitest/expect@4.1.10", "", { "dependencies": { "@standard-schema/spec": "^1.1.0", "@types/chai": "^5.2.2", "@vitest/spy": "4.1.10", "@vitest/utils": "4.1.10", "chai": "^6.2.2", "tinyrainbow": "^3.1.0" } }, "sha512-YsCn+qAk1GWjQOWFEsEcL2gNQ0zmVmQu3T03qP6UyjhtmdtwtbuI+DASn/7iQB3HGTXkdBwGddzxPlmiql5vlA=="], + "@vitest/expect": ["@vitest/expect@4.1.11", "", { "dependencies": { "@standard-schema/spec": "^1.1.0", "@types/chai": "^5.2.2", "@vitest/spy": "4.1.11", "@vitest/utils": "4.1.11", "chai": "^6.2.2", "tinyrainbow": "^3.1.0" } }, "sha512-VX2x5vNJXET47KAFzwERI+KRMtTTCSWTfSMKsW7JsUsXV4psq++e3DvZpuTDOpHcxytiDs6p2nhVb2tVDiiUYw=="], - "@vitest/mocker": ["@vitest/mocker@4.1.10", "", { "dependencies": { "@vitest/spy": "4.1.10", "estree-walker": "^3.0.3", "magic-string": "^0.30.21" }, "peerDependencies": { "msw": "^2.4.9", "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" }, "optionalPeers": ["msw", "vite"] }, "sha512-v0xaezt+DKEmKfaxg133ldzADrwLGd7Ze1MfQQTYfvs8OqZIwbxyxaYURivwV7sWy5fqn3rH5uOrSp07bp44Ow=="], + "@vitest/mocker": ["@vitest/mocker@4.1.11", "", { "dependencies": { "@vitest/spy": "4.1.11", "estree-walker": "^3.0.3", "magic-string": "^0.30.21" }, "peerDependencies": { "msw": "^2.4.9", "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" }, "optionalPeers": ["msw", "vite"] }, "sha512-2XJVD55d1o5AZous5CCGKS74g/riOj9odEt2bQpCVZeblHyHdnMeFl4jl0XjU21stf4mbjUkew2eXQZt65g5CQ=="], - "@vitest/pretty-format": ["@vitest/pretty-format@4.1.10", "", { "dependencies": { "tinyrainbow": "^3.1.0" } }, "sha512-W1HsjSH4MXQ9YfmmhLAoIYf1HRfekQCGngeIgcei6MP5QQGWUe0gkopdZQaVCFO+JDJMrAJGwa5pRpNpvy4P8Q=="], + "@vitest/pretty-format": ["@vitest/pretty-format@4.1.11", "", { "dependencies": { "tinyrainbow": "^3.1.0" } }, "sha512-yiZzPbGTS9Sr/JpFl8zHrcIkAofNbFV6k21vIgQN/cY/oxZeXhJv5sc/MBJ5jFKWmWs+oJHw0UXLZjmf931+Vw=="], - "@vitest/runner": ["@vitest/runner@4.1.10", "", { "dependencies": { "@vitest/utils": "4.1.10", "pathe": "^2.0.3" } }, "sha512-IKI6kpIH+LmpROplyLwBBaCfMgOZOMsygVa6BARD6ahA04VRuJSa6OaVG7kRvSEMD870Vd91rSSw0eegtWyLGg=="], + "@vitest/runner": ["@vitest/runner@4.1.11", "", { "dependencies": { "@vitest/utils": "4.1.11", "pathe": "^2.0.3" } }, "sha512-LztvUgdwMNJMIkj3hQnnxiC2Xy1zNxq928W/xhjCLaNCzqTZOudjwbQf6v9IntZGPw132i2Lq2rgTRZHD3JHNw=="], - "@vitest/snapshot": ["@vitest/snapshot@4.1.10", "", { "dependencies": { "@vitest/pretty-format": "4.1.10", "@vitest/utils": "4.1.10", "magic-string": "^0.30.21", "pathe": "^2.0.3" } }, "sha512-xRkfOT1qpTAi/Ti4Y1LtfRc3kEuqxGw59eN2jN9pRWMtS/XDevekhcFSqvQqjUNGksfjMJu3Y+oJ+4Ypn2OaJw=="], + "@vitest/snapshot": ["@vitest/snapshot@4.1.11", "", { "dependencies": { "@vitest/pretty-format": "4.1.11", "@vitest/utils": "4.1.11", "magic-string": "^0.30.21", "pathe": "^2.0.3" } }, "sha512-pN7ikn1ON7h8ee4gIAp4AzyK+zBtJPzVbqOgu5LCEh4VaJVbPQcgYQYJIMGQPXVeJJq1fnfazis7a5pFNPahog=="], - "@vitest/spy": ["@vitest/spy@4.1.10", "", {}, "sha512-PLf/Ugvoq5wO/b4rwYCR1h2PSIdXz7wnkQFMiUpLdtM7l6pqVFcQIBEHyT1+l+cj7mNwAfZHzqXqDyjvOuwbDw=="], + "@vitest/spy": ["@vitest/spy@4.1.11", "", {}, "sha512-apNa/prQy2qCeywhnixOHPRCgGNhvg7T4Dapfl1GahLp/R+uhBm5cPyFoNVyqsNd2h1nJxL6BqqdIjiABL60YA=="], - "@vitest/utils": ["@vitest/utils@4.1.10", "", { "dependencies": { "@vitest/pretty-format": "4.1.10", "convert-source-map": "^2.0.0", "tinyrainbow": "^3.1.0" } }, "sha512-fy9am/HWxbaGt/Sawrp90vt6Y6jQwf1RX77cz3uwoJwJVMli/e1IEwRPnMNJ7vKfPTwo0diXifkpPvwH9v7nGA=="], + "@vitest/utils": ["@vitest/utils@4.1.11", "", { "dependencies": { "@vitest/pretty-format": "4.1.11", "convert-source-map": "^2.0.0", "tinyrainbow": "^3.1.0" } }, "sha512-zTCVGpyFsGWBhllOyKlTw/vnr6D9qxsfSDyfbyZmTyjHw5N/VuvzHpHoQjm2ZJzn4RJgx5w4r7V0er69CmLgPQ=="], "@voidzero-dev/vite-plus-core": ["@voidzero-dev/vite-plus-core@0.1.24", "", { "dependencies": { "@oxc-project/runtime": "=0.133.0", "@oxc-project/types": "=0.133.0", "lightningcss": "^1.30.2", "postcss": "^8.5.6" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@arethetypeswrong/core": "^0.18.1", "@tsdown/css": "0.22.1", "@tsdown/exe": "0.22.1", "@types/node": "^20.19.0 || >=22.12.0", "@vitejs/devtools": "^0.1.18", "esbuild": "^0.27.0 || ^0.28.0", "jiti": ">=1.21.0", "less": "^4.0.0", "publint": "^0.3.8", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "typescript": "^5.0.0 || ^6.0.0", "unplugin-unused": "^0.5.0", "unrun": "*", "yaml": "^2.4.2" }, "optionalPeers": ["@arethetypeswrong/core", "@tsdown/css", "@tsdown/exe", "@types/node", "@vitejs/devtools", "esbuild", "jiti", "less", "publint", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "typescript", "unplugin-unused", "unrun", "yaml"] }, "sha512-iXPGBABnQnrDMx89H6MOCGcTZp+QW+3rY4YMVKdE6ydchSvPk2O3MI2vgaRVfOtWJ2IjnxSnf1n2yjP67ZBRFQ=="], @@ -1556,7 +1592,7 @@ "buffer-from": ["buffer-from@1.1.2", "", {}, "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ=="], - "bun-types": ["bun-types@1.3.14", "", { "dependencies": { "@types/node": "*" } }, "sha512-4N0ig0fEomHt5R0KCFWjovxow98rIoRwKolrYdCcknNwMekCXRnWEUvgu5soYV8QXtVsrUD8B95MBOZGPvr6KQ=="], + "bun-types": ["bun-types@1.4.0", "", { "dependencies": { "@types/node": "*" } }, "sha512-iIKw23BspnQQYd3prITOBxeUsxBHnwzX6YJfGMuNOZzeNcMmVqzIIVGRm1l69ogaPQmb4wB6BN8mA5bE9YuC5Q=="], "bytes": ["bytes@3.1.2", "", {}, "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg=="], @@ -2294,31 +2330,31 @@ "openapi-types": ["openapi-types@12.1.3", "", {}, "sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw=="], - "opencode-ai": ["opencode-ai@1.18.4", "", { "optionalDependencies": { "opencode-darwin-arm64": "1.18.4", "opencode-darwin-x64": "1.18.4", "opencode-darwin-x64-baseline": "1.18.4", "opencode-linux-arm64": "1.18.4", "opencode-linux-arm64-musl": "1.18.4", "opencode-linux-x64": "1.18.4", "opencode-linux-x64-baseline": "1.18.4", "opencode-linux-x64-baseline-musl": "1.18.4", "opencode-linux-x64-musl": "1.18.4", "opencode-windows-arm64": "1.18.4", "opencode-windows-x64": "1.18.4", "opencode-windows-x64-baseline": "1.18.4" }, "os": [ "linux", "win32", "darwin", ], "cpu": [ "x64", "arm64", ], "bin": { "opencode": "bin/opencode.exe" } }, "sha512-B8pFAs1g158ZU+C6eSiJz/5ZhG7Vr2mH7Z7ruYEHLElAlX9tehrRTnzTTgjSxHY2vwZ/5VplwR3odFU+Dai8jg=="], + "opencode-ai": ["opencode-ai@1.18.23", "", { "optionalDependencies": { "opencode-darwin-arm64": "1.18.23", "opencode-darwin-x64": "1.18.23", "opencode-darwin-x64-baseline": "1.18.23", "opencode-linux-arm64": "1.18.23", "opencode-linux-arm64-musl": "1.18.23", "opencode-linux-x64": "1.18.23", "opencode-linux-x64-baseline": "1.18.23", "opencode-linux-x64-baseline-musl": "1.18.23", "opencode-linux-x64-musl": "1.18.23", "opencode-windows-arm64": "1.18.23", "opencode-windows-x64": "1.18.23", "opencode-windows-x64-baseline": "1.18.23" }, "os": [ "linux", "win32", "darwin", ], "cpu": [ "x64", "arm64", ], "bin": { "opencode": "bin/opencode.exe" } }, "sha512-3NkT0XINL7d0HYkTyGV1SPChHXhvRgKqNaTgKRTGb0TXUWszXA7MW/y3zMZw29y1AQuUDAzRvVYmQ9KGRQhroA=="], - "opencode-darwin-arm64": ["opencode-darwin-arm64@1.18.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-/GdcUv3axBFYmpdCY3yMGRCBq3fwAJayflnn9stXtIHKuckc7ZaUYZmyudIUpmzkZP5W8XsAVMJomR4Rbr2+Ig=="], + "opencode-darwin-arm64": ["opencode-darwin-arm64@1.18.23", "", { "os": "darwin", "cpu": "arm64" }, "sha512-QP9PjwpHtZoLVXw2WvUmPZecz7mWbQkT4t3K36B//fCaDG+zWa+SsztIeaW5azujNwwtUemLA5icE/zINng48Q=="], - "opencode-darwin-x64": ["opencode-darwin-x64@1.18.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-0i/AmdFw3CerwIggKqtaDlWDCBfSgzXT+KPSvks/Dx6l9NfYHihLnbV+vKbr0lwzco1lsDo4JHE4j/zz7JZUKQ=="], + "opencode-darwin-x64": ["opencode-darwin-x64@1.18.23", "", { "os": "darwin", "cpu": "x64" }, "sha512-R9nWP3edz/0FnEfwmuxtiWBB7bS4NtZCyCffJyiMlrbwdDC+bIXYrWxWXVrzaP1mJujs6g2MAwTCUSx/qpBhDw=="], - "opencode-darwin-x64-baseline": ["opencode-darwin-x64-baseline@1.18.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-1v/keJ/m+3UplGU2XdP5YfK25rTlGi7Kf5BVSV/ICObk6PiJWBAWOrB8bAQWDLhN/Tn7UfogBAgxaiRRfLPuEA=="], + "opencode-darwin-x64-baseline": ["opencode-darwin-x64-baseline@1.18.23", "", { "os": "darwin", "cpu": "x64" }, "sha512-QGx6I/nFYur7qJ/Nx2L3fC4XYQt44cyDsm7p8twNA+cdjGX3ndnPbMdAl5ikdZyAfSMUGYK8VWY2JMxv0rmfjw=="], - "opencode-linux-arm64": ["opencode-linux-arm64@1.18.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-3gBw9weE76jloNPGmPvzl3GJR8b6scwSAENxUKRdJ3WCkC7qcjJm49KFyfqFjO5++1H1Q2jbmqqxAgt5upqoMQ=="], + "opencode-linux-arm64": ["opencode-linux-arm64@1.18.23", "", { "os": "linux", "cpu": "arm64" }, "sha512-g1zDFhuE9FOYwjSGderlu69wfd4GQzS0xsDiIY11QUciuBmM6DrHqLvmhlLFsUVHYVnCPY1YeN1pq5ewE3x72Q=="], - "opencode-linux-arm64-musl": ["opencode-linux-arm64-musl@1.18.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-kno1QJz+JoY1YqGlrxcwGKqOh2/OFKdQ4UGUCN5K2evQRIhd9EfB4JvZkjzLxTzglda8lpP+PJltjCA4DHJAJw=="], + "opencode-linux-arm64-musl": ["opencode-linux-arm64-musl@1.18.23", "", { "os": "linux", "cpu": "arm64" }, "sha512-VyDkzUJfJgkx9h9RhazTW9xeTgSXBmVFPblsbPGBW9tR612f6gjQxfOfu4cpHnQHK1qjuW9ClzLKHWqv3EcJTA=="], - "opencode-linux-x64": ["opencode-linux-x64@1.18.4", "", { "os": "linux", "cpu": "x64" }, "sha512-mCiZuKQBqRHVhtp20YFDfNf5cUwTt6/I98MFJQDJdDTWggnkAVOnitQTmSt2cQwoPJ94VrRwDhZyY3pIY3+CQg=="], + "opencode-linux-x64": ["opencode-linux-x64@1.18.23", "", { "os": "linux", "cpu": "x64" }, "sha512-5x9d1Cm/YtqzR6lAlNbgVprTQ3R3hx7qGTWCzm5l5u6lBNkhYTrhy2s8k25dxKKuxqZ9Kngqz9JYWvSVHy2Lmw=="], - "opencode-linux-x64-baseline": ["opencode-linux-x64-baseline@1.18.4", "", { "os": "linux", "cpu": "x64" }, "sha512-LlVIv54gpM8I6QJVAoW5uvcaVO4z+y5DswfUCsxgaD0CWgjEbULaiAAZIS/L+N5rRw/jHeqcHSXiKinFZt0xww=="], + "opencode-linux-x64-baseline": ["opencode-linux-x64-baseline@1.18.23", "", { "os": "linux", "cpu": "x64" }, "sha512-yUhBOXfTQour2JCdAkwD3DDqSnyxB0grefwdPqEhYmJHIkYxfJIIzyy6V//pyouvkE0XMouFtiuZXw8S6Wo0iQ=="], - "opencode-linux-x64-baseline-musl": ["opencode-linux-x64-baseline-musl@1.18.4", "", { "os": "linux", "cpu": "x64" }, "sha512-hM/uPMEuxLbwXueOmHsf3jjmfJ238/3r6FZMPumKVMR7jJHutp3ZWuOIL+ZzByxzsUK1f3RGbKaFULOMIieRSw=="], + "opencode-linux-x64-baseline-musl": ["opencode-linux-x64-baseline-musl@1.18.23", "", { "os": "linux", "cpu": "x64" }, "sha512-c1DPxauhzAurlIBhJBr/rokDpc65l084T4qTl36gDDT9Xzc/Nk5Q5yMDaPm1DDI3WeHKDt11MDlxT5AjQW5gtw=="], - "opencode-linux-x64-musl": ["opencode-linux-x64-musl@1.18.4", "", { "os": "linux", "cpu": "x64" }, "sha512-AlojHLyv7Cgn2g5Rj5QeHckEWyA20qdbEla2d1R9mXSaqlP6sX7izNti8Tzr/vMG53Uwmppyjb37uFB2fK+YTQ=="], + "opencode-linux-x64-musl": ["opencode-linux-x64-musl@1.18.23", "", { "os": "linux", "cpu": "x64" }, "sha512-t/5mlnTBZKdZpqKHwdwxlWqGakntauvMSmXtyJc17M7XJRmZaaGHtNSaSefbbYFIL4agoQCXTvIkhhyxOvr7zQ=="], - "opencode-windows-arm64": ["opencode-windows-arm64@1.18.4", "", { "os": "win32", "cpu": "arm64" }, "sha512-B6m7N7ZPj/E6xx1ZaSjNW0gHTd0b3NTSf9mQiN3W89zji3oH9VOHLQdrP10rAd/2uZ5ZttxSkRqj/iwgAW+3TQ=="], + "opencode-windows-arm64": ["opencode-windows-arm64@1.18.23", "", { "os": "win32", "cpu": "arm64" }, "sha512-QtJQcLU0yPz6on3jjks3f/EHgZuIDFw7FvAKu3wsHhL09NYDh7GczfRXDPRHa3NgqnU8dkB8p9mhqgaPRPogoQ=="], - "opencode-windows-x64": ["opencode-windows-x64@1.18.4", "", { "os": "win32", "cpu": "x64" }, "sha512-TumOcMOvZ6vfs348LgGLK9eFEBLsh+wI/UXmQ8BhXW95YV1Wd26TutSzQi4L/wLftsDfl8Q6bKQ2trWtGBzBvQ=="], + "opencode-windows-x64": ["opencode-windows-x64@1.18.23", "", { "os": "win32", "cpu": "x64" }, "sha512-mMaIITuXzkNfjdcYL8uZaZuMDjulFyH/UCq9bxblam2mUZf9uWisoi5J6CXFsS/mkN7CZfTAt6PttSp4n3PH4g=="], - "opencode-windows-x64-baseline": ["opencode-windows-x64-baseline@1.18.4", "", { "os": "win32", "cpu": "x64" }, "sha512-4EpZ97uI50vVv9UT5sDeK3ff+sFzqmbBT4TNGkMF9uwNTKJYqoSVuOIZhIfLTLNlrEK2hd2/sf/4GQhIkM6iTw=="], + "opencode-windows-x64-baseline": ["opencode-windows-x64-baseline@1.18.23", "", { "os": "win32", "cpu": "x64" }, "sha512-AqXsTKaPcDx3rrid5bLUwJbQ/3vr9rJ6fvOStIznTzwrbOgP8wy5G4jCoIzu6KB/WxGx/d1MrV4cGaJ73qnjBA=="], "optionator": ["optionator@0.9.4", "", { "dependencies": { "deep-is": "^0.1.3", "fast-levenshtein": "^2.0.6", "levn": "^0.4.1", "prelude-ls": "^1.2.1", "type-check": "^0.4.0", "word-wrap": "^1.2.5" } }, "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g=="], @@ -2656,7 +2692,7 @@ "undici": ["undici@7.28.0", "", {}, "sha512-cRZYrTDwWznlnRiPjggAGxZXanty6M8RV1ff8Wm4LWXBp7/IG8v5DnOm74DtUBp9OONpK75YlPnIjQqX0dBDtA=="], - "undici-types": ["undici-types@7.24.6", "", {}, "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg=="], + "undici-types": ["undici-types@8.3.0", "", {}, "sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ=="], "unenv": ["unenv@2.0.0-rc.24", "", { "dependencies": { "pathe": "^2.0.3" } }, "sha512-i7qRCmY42zmCwnYlh9H2SvLEypEFGye5iRmEMKjcGi7zk9UquigRjFtTLz0TYqr0ZGLZhaMHl/foy1bZR+Cwlw=="], @@ -2698,7 +2734,7 @@ "vary": ["vary@1.1.2", "", {}, "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg=="], - "vestig": ["vestig@0.23.0", "", {}, "sha512-Jo9HAym5MyXn3zY4L1T/e7zGwP3Okf5VWtSWeSvjpeAbDD6iparFRxrlRSpwZLCxJY1DSlBFiHRLX4+wYVNtiA=="], + "vestig": ["vestig@0.24.1", "", {}, "sha512-sQTGvU3VdPgtM3PE3Eweo7AqJJPeoDl3Wke2OEVJRA5lZclZ5CmgDHLKCfvOJ/S82SyXtyzwCeQNvTxSbPttWg=="], "vfile": ["vfile@6.0.3", "", { "dependencies": { "@types/unist": "^3.0.0", "vfile-message": "^4.0.0" } }, "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q=="], @@ -2710,7 +2746,7 @@ "vite-plus": ["vite-plus@0.1.24", "", { "dependencies": { "@oxc-project/types": "=0.133.0", "@oxlint/plugins": "=1.61.0", "@voidzero-dev/vite-plus-core": "0.1.24", "@voidzero-dev/vite-plus-test": "0.1.24", "oxfmt": "=0.52.0", "oxlint": "=1.67.0", "oxlint-tsgolint": "=0.23.0" }, "optionalDependencies": { "@voidzero-dev/vite-plus-darwin-arm64": "0.1.24", "@voidzero-dev/vite-plus-darwin-x64": "0.1.24", "@voidzero-dev/vite-plus-linux-arm64-gnu": "0.1.24", "@voidzero-dev/vite-plus-linux-arm64-musl": "0.1.24", "@voidzero-dev/vite-plus-linux-x64-gnu": "0.1.24", "@voidzero-dev/vite-plus-linux-x64-musl": "0.1.24", "@voidzero-dev/vite-plus-win32-arm64-msvc": "0.1.24", "@voidzero-dev/vite-plus-win32-x64-msvc": "0.1.24" }, "bin": { "vp": "bin/vp", "oxfmt": "bin/oxfmt", "oxlint": "bin/oxlint" } }, "sha512-b3fr6WtCiEhetjuzW/4KcEMOAMuZxoxZATWaXKmPzOLf1upG+pzKJOFZTb94D6wiPBlwcjxoaUtF7C3uAN+VjQ=="], - "vitest": ["vitest@4.1.10", "", { "dependencies": { "@vitest/expect": "4.1.10", "@vitest/mocker": "4.1.10", "@vitest/pretty-format": "4.1.10", "@vitest/runner": "4.1.10", "@vitest/snapshot": "4.1.10", "@vitest/spy": "4.1.10", "@vitest/utils": "4.1.10", "es-module-lexer": "^2.0.0", "expect-type": "^1.3.0", "magic-string": "^0.30.21", "obug": "^2.1.1", "pathe": "^2.0.3", "picomatch": "^4.0.3", "std-env": "^4.0.0-rc.1", "tinybench": "^2.9.0", "tinyexec": "^1.0.2", "tinyglobby": "^0.2.15", "tinyrainbow": "^3.1.0", "vite": "^6.0.0 || ^7.0.0 || ^8.0.0", "why-is-node-running": "^2.3.0" }, "peerDependencies": { "@edge-runtime/vm": "*", "@opentelemetry/api": "^1.9.0", "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", "@vitest/browser-playwright": "4.1.10", "@vitest/browser-preview": "4.1.10", "@vitest/browser-webdriverio": "4.1.10", "@vitest/coverage-istanbul": "4.1.10", "@vitest/coverage-v8": "4.1.10", "@vitest/ui": "4.1.10", "happy-dom": "*", "jsdom": "*" }, "optionalPeers": ["@edge-runtime/vm", "@opentelemetry/api", "@types/node", "@vitest/browser-playwright", "@vitest/browser-preview", "@vitest/browser-webdriverio", "@vitest/coverage-istanbul", "@vitest/coverage-v8", "@vitest/ui", "happy-dom", "jsdom"], "bin": { "vitest": "./vitest.mjs" } }, "sha512-R9jUTe5S4Qb0HCd4TNqpC7oGcrMssMRGXLW80ubjWsW9VH5GF8y1Y0SFLY9AbqSk6nt0PnOx4H4WNJYZ13GUPw=="], + "vitest": ["vitest@4.1.11", "", { "dependencies": { "@vitest/expect": "4.1.11", "@vitest/mocker": "4.1.11", "@vitest/pretty-format": "4.1.11", "@vitest/runner": "4.1.11", "@vitest/snapshot": "4.1.11", "@vitest/spy": "4.1.11", "@vitest/utils": "4.1.11", "es-module-lexer": "^2.0.0", "expect-type": "^1.3.0", "magic-string": "^0.30.21", "obug": "^2.1.1", "pathe": "^2.0.3", "picomatch": "^4.0.3", "std-env": "^4.0.0-rc.1", "tinybench": "^2.9.0", "tinyexec": "^1.0.2", "tinyglobby": "^0.2.15", "tinyrainbow": "^3.1.0", "vite": "^6.0.0 || ^7.0.0 || ^8.0.0", "why-is-node-running": "^2.3.0" }, "peerDependencies": { "@edge-runtime/vm": "*", "@opentelemetry/api": "^1.9.0", "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", "@vitest/browser-playwright": "4.1.11", "@vitest/browser-preview": "4.1.11", "@vitest/browser-webdriverio": "4.1.11", "@vitest/coverage-istanbul": "4.1.11", "@vitest/coverage-v8": "4.1.11", "@vitest/ui": "4.1.11", "happy-dom": "*", "jsdom": "*" }, "optionalPeers": ["@edge-runtime/vm", "@opentelemetry/api", "@types/node", "@vitest/browser-playwright", "@vitest/browser-preview", "@vitest/browser-webdriverio", "@vitest/coverage-istanbul", "@vitest/coverage-v8", "@vitest/ui", "happy-dom", "jsdom"], "bin": { "vitest": "./vitest.mjs" } }, "sha512-fhACrNXUidIbGSBr5FlbuBkO7VWC1ZyLl0DO4CU2DrQoAPxX84Ysxs+HeGQpii5lZWV1Q4gBZTTu49mF+A6Edw=="], "vscode-jsonrpc": ["vscode-jsonrpc@8.2.0", "", {}, "sha512-C+r0eKJUIfiDIfwJhria30+TYWPtuHJXHtI7J0YlOmKAo7ogxP20T0zxB7HZQIFhIyvoBPwWskjxrvAtfjyZfA=="], @@ -2828,13 +2864,21 @@ "@modelcontextprotocol/sdk/hono": ["hono@4.12.30", "", {}, "sha512-emn+JoJjrN9YTpRDS5it/UI2SO9BAE37T6I3d963RxcZ81G9A4pr2SZTEiiaiKbzx+NKRg5BZ89fCL7gCJCUog=="], - "@mosoo/agent-driver/vite-plus": ["vite-plus@0.2.5", "", { "dependencies": { "@oxc-project/types": "=0.139.0", "@oxlint/plugins": "=1.73.0", "@vitest/browser": "4.1.10", "@vitest/browser-preview": "4.1.10", "@vitest/expect": "4.1.10", "@vitest/mocker": "4.1.10", "@vitest/pretty-format": "4.1.10", "@vitest/runner": "4.1.10", "@vitest/snapshot": "4.1.10", "@vitest/spy": "4.1.10", "@vitest/utils": "4.1.10", "@voidzero-dev/vite-plus-core": "0.2.5", "oxfmt": "=0.58.0", "oxlint": "=1.73.0", "oxlint-tsgolint": "=0.24.0", "vitest": "4.1.10" }, "optionalDependencies": { "@voidzero-dev/vite-plus-darwin-arm64": "0.2.5", "@voidzero-dev/vite-plus-darwin-x64": "0.2.5", "@voidzero-dev/vite-plus-linux-arm64-gnu": "0.2.5", "@voidzero-dev/vite-plus-linux-arm64-musl": "0.2.5", "@voidzero-dev/vite-plus-linux-x64-gnu": "0.2.5", "@voidzero-dev/vite-plus-linux-x64-musl": "0.2.5", "@voidzero-dev/vite-plus-win32-arm64-msvc": "0.2.5", "@voidzero-dev/vite-plus-win32-x64-msvc": "0.2.5" }, "peerDependencies": { "@vitest/browser-playwright": "4.1.10", "@vitest/browser-webdriverio": "4.1.10" }, "optionalPeers": ["@vitest/browser-playwright", "@vitest/browser-webdriverio"], "bin": { "vp": "bin/vp", "vpr": "bin/vpr", "oxfmt": "bin/oxfmt", "oxlint": "bin/oxlint" } }, "sha512-QNJ0FnN8rfs5u8lZKZ1uR2Tegjg3VkT0AGTxSLGHg4fYmGxRNfAV0YX1parZrVa4VybSI56SWCoo7wQ6D7pMew=="], + "@mosoo/agent-driver/typescript": ["typescript@7.0.2", "", { "optionalDependencies": { "@typescript/typescript-aix-ppc64": "7.0.2", "@typescript/typescript-darwin-arm64": "7.0.2", "@typescript/typescript-darwin-x64": "7.0.2", "@typescript/typescript-freebsd-arm64": "7.0.2", "@typescript/typescript-freebsd-x64": "7.0.2", "@typescript/typescript-linux-arm": "7.0.2", "@typescript/typescript-linux-arm64": "7.0.2", "@typescript/typescript-linux-loong64": "7.0.2", "@typescript/typescript-linux-mips64el": "7.0.2", "@typescript/typescript-linux-ppc64": "7.0.2", "@typescript/typescript-linux-riscv64": "7.0.2", "@typescript/typescript-linux-s390x": "7.0.2", "@typescript/typescript-linux-x64": "7.0.2", "@typescript/typescript-netbsd-arm64": "7.0.2", "@typescript/typescript-netbsd-x64": "7.0.2", "@typescript/typescript-openbsd-arm64": "7.0.2", "@typescript/typescript-openbsd-x64": "7.0.2", "@typescript/typescript-sunos-x64": "7.0.2", "@typescript/typescript-win32-arm64": "7.0.2", "@typescript/typescript-win32-x64": "7.0.2" }, "bin": { "tsc": "bin/tsc" } }, "sha512-8FYau96o3NKOhbjKi/qNvG/W5jhzxkbdm5sj9AbZ/5T5sWqn3hJgLfGx27sRKZWTvyzCP8dLRBTf5tBTSRVUNA=="], - "@poppinss/colors/kleur": ["kleur@4.1.5", "", {}, "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ=="], + "@mosoo/agent-driver/vite-plus": ["vite-plus@0.3.0", "", { "dependencies": { "@oxc-project/types": "=0.146.0", "@oxlint/plugins": "=1.79.0", "@vitest/browser": "4.1.11", "@vitest/browser-preview": "4.1.11", "@vitest/expect": "4.1.11", "@vitest/mocker": "4.1.11", "@vitest/pretty-format": "4.1.11", "@vitest/runner": "4.1.11", "@vitest/snapshot": "4.1.11", "@vitest/spy": "4.1.11", "@vitest/utils": "4.1.11", "@voidzero-dev/vite-plus-core": "0.3.0", "oxfmt": "=0.64.0", "oxlint": "=1.79.0", "oxlint-tsgolint": "=7.0.2001", "vitest": "4.1.11" }, "optionalDependencies": { "@voidzero-dev/vite-plus-darwin-arm64": "0.3.0", "@voidzero-dev/vite-plus-darwin-x64": "0.3.0", "@voidzero-dev/vite-plus-linux-arm64-gnu": "0.3.0", "@voidzero-dev/vite-plus-linux-arm64-musl": "0.3.0", "@voidzero-dev/vite-plus-linux-x64-gnu": "0.3.0", "@voidzero-dev/vite-plus-linux-x64-musl": "0.3.0", "@voidzero-dev/vite-plus-win32-arm64-msvc": "0.3.0", "@voidzero-dev/vite-plus-win32-x64-msvc": "0.3.0" }, "peerDependencies": { "@vitest/browser-playwright": "4.1.11", "@vitest/browser-webdriverio": "4.1.11" }, "optionalPeers": ["@vitest/browser-playwright", "@vitest/browser-webdriverio"], "bin": { "oxfmt": "./bin/oxfmt", "oxlint": "./bin/oxlint", "vp": "./bin/vp", "vpr": "./bin/vpr" } }, "sha512-GNWbWuWD37frCSFrz6MLzUo62bTv5IOJozHEgZYOkxsLkuQtTwm4TowzpfoGrSsfwhAAtfPd/sK1Y0+v1SwhZA=="], + + "@mosoo/api/@types/node": ["@types/node@25.9.5", "", { "dependencies": { "undici-types": ">=7.24.0 <7.24.7" } }, "sha512-OScDchr2fwuUmWdf4kZ9h7PcJiYDVInhJizG/biAq3cAvqwYktuy/TYGGdZNMtNTFUP7rnb0NU4TUdm82kt4Rg=="], + + "@mosoo/db/@types/node": ["@types/node@25.9.5", "", { "dependencies": { "undici-types": ">=7.24.0 <7.24.7" } }, "sha512-OScDchr2fwuUmWdf4kZ9h7PcJiYDVInhJizG/biAq3cAvqwYktuy/TYGGdZNMtNTFUP7rnb0NU4TUdm82kt4Rg=="], + + "@mosoo/e2e/@types/node": ["@types/node@25.9.5", "", { "dependencies": { "undici-types": ">=7.24.0 <7.24.7" } }, "sha512-OScDchr2fwuUmWdf4kZ9h7PcJiYDVInhJizG/biAq3cAvqwYktuy/TYGGdZNMtNTFUP7rnb0NU4TUdm82kt4Rg=="], + + "@mosoo/observability/vestig": ["vestig@0.23.0", "", {}, "sha512-Jo9HAym5MyXn3zY4L1T/e7zGwP3Okf5VWtSWeSvjpeAbDD6iparFRxrlRSpwZLCxJY1DSlBFiHRLX4+wYVNtiA=="], - "@rolldown/binding-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.11.1", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.2", "tslib": "^2.4.0" } }, "sha512-RSvbQmHzdKzNsLYa/wHrbc3KN4sYLKAdPZxqiM2HATqv/SBk2/ENSHpvXGaLOMcsAyz0poEGqkmmKYG3OWiJEQ=="], + "@mosoo/web/@types/node": ["@types/node@25.9.5", "", { "dependencies": { "undici-types": ">=7.24.0 <7.24.7" } }, "sha512-OScDchr2fwuUmWdf4kZ9h7PcJiYDVInhJizG/biAq3cAvqwYktuy/TYGGdZNMtNTFUP7rnb0NU4TUdm82kt4Rg=="], - "@rolldown/binding-wasm32-wasi/@emnapi/runtime": ["@emnapi/runtime@1.11.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw=="], + "@poppinss/colors/kleur": ["kleur@4.1.5", "", {}, "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ=="], "@tailwindcss/node/lightningcss": ["lightningcss@1.32.0", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-android-arm64": "1.32.0", "lightningcss-darwin-arm64": "1.32.0", "lightningcss-darwin-x64": "1.32.0", "lightningcss-freebsd-x64": "1.32.0", "lightningcss-linux-arm-gnueabihf": "1.32.0", "lightningcss-linux-arm64-gnu": "1.32.0", "lightningcss-linux-arm64-musl": "1.32.0", "lightningcss-linux-x64-gnu": "1.32.0", "lightningcss-linux-x64-musl": "1.32.0", "lightningcss-win32-arm64-msvc": "1.32.0", "lightningcss-win32-x64-msvc": "1.32.0" } }, "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ=="], @@ -2944,8 +2988,6 @@ "vitest/es-module-lexer": ["es-module-lexer@2.3.1", "", {}, "sha512-shc1dbU90Yl/xq1QrC7QRtfcwURZuVRfPhZbDoldJ1cn1gzDvBaBWlv0eFolj5+0znnPJz5TXLxsN77X/12KTA=="], - "vitest/vite": ["vite@8.1.5", "", { "dependencies": { "lightningcss": "^1.32.0", "picomatch": "^4.0.5", "postcss": "^8.5.17", "rolldown": "~1.1.5", "tinyglobby": "^0.2.17" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "@vitejs/devtools": "^0.3.0", "esbuild": "^0.27.0 || ^0.28.0", "jiti": ">=1.21.0", "less": "^4.0.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "@vitejs/devtools", "esbuild", "jiti", "less", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-7ULLwsCdYx/nRyrpiEwvqb5TFHrMVZyBt+rg/OAXT7rgj/z+DtTDyKFeLAdDkubDVDKD8jOsndmy7m55XcfUsw=="], - "wrap-ansi/string-width": ["string-width@8.2.2", "", { "dependencies": { "get-east-asian-width": "^1.5.0", "strip-ansi": "^7.1.2" } }, "sha512-GaPUh5gfdrYzqeVNZvUfT23vYYxXzKYidUcnMtJg/3rxRV63EFZy3k6xfKlmfeJD0176lnUV/Usr3XcwSvFzpg=="], "@esbuild-kit/core-utils/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.18.20", "", { "os": "android", "cpu": "arm" }, "sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw=="], @@ -2996,33 +3038,41 @@ "@graphql-codegen/cli/@graphql-codegen/client-preset/@graphql-codegen/visitor-plugin-common": ["@graphql-codegen/visitor-plugin-common@7.2.0", "", { "dependencies": { "@graphql-codegen/plugin-helpers": "^7.1.0", "@graphql-tools/optimize": "^2.0.0", "@graphql-tools/relay-operation-optimizer": "^7.1.1", "@graphql-tools/utils": "^11.2.0", "auto-bind": "^5.0.0", "change-case-all": "^2.1.0", "dependency-graph": "^1.0.0", "graphql-tag": "^2.11.0", "parse-filepath": "^1.0.2", "tslib": "^2.8.0" }, "peerDependencies": { "graphql": "^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" } }, "sha512-qqtTY8taONuxlR0HvD8z+zgWC3CfJ47M2rATx+geWNIN8v8Itc5TflHYxjkWAtNL6E3q7WZRY5P7se+2S3EjBg=="], - "@mosoo/agent-driver/vite-plus/@oxc-project/types": ["@oxc-project/types@0.139.0", "", {}, "sha512-r9gHphtCs+1M7J0pw6Sn/hh/Wpa/iQrOOkrNAlVLF/gHq+/CJmHIWKKUUhdWjcD6CIa8idarspCsASiXCXvFUw=="], + "@mosoo/agent-driver/vite-plus/@oxc-project/types": ["@oxc-project/types@0.146.0", "", {}, "sha512-XC0QsnnhVe7sLIWmYmdPw7x5P0h4W8vUU3Nv1ySgWXtvCz8NizoAEpGXA0sOYoJQV2Rl13LgURAHQ5cI5ILCSA=="], + + "@mosoo/agent-driver/vite-plus/@oxlint/plugins": ["@oxlint/plugins@1.79.0", "", {}, "sha512-S0uyoxakDINJ4DPgqxGlEEvrdSMeQb7Z2lKVjxoY2gwsbZbfg2Xr8Klfeo5ZeraHmmdBCELFUHkSe6KEmBpMvg=="], + + "@mosoo/agent-driver/vite-plus/@voidzero-dev/vite-plus-core": ["@voidzero-dev/vite-plus-core@0.3.0", "", { "dependencies": { "@oxc-project/runtime": "=0.146.0", "@oxc-project/types": "=0.146.0", "lightningcss": "^1.33.0", "postcss": "^8.5.6", "yuku-codegen": "^0.5.44", "yuku-parser": "^0.5.44" }, "optionalDependencies": { "@voidzero-dev/vite-plus-darwin-arm64": "0.3.0", "@voidzero-dev/vite-plus-darwin-x64": "0.3.0", "@voidzero-dev/vite-plus-linux-arm64-gnu": "0.3.0", "@voidzero-dev/vite-plus-linux-arm64-musl": "0.3.0", "@voidzero-dev/vite-plus-linux-x64-gnu": "0.3.0", "@voidzero-dev/vite-plus-linux-x64-musl": "0.3.0", "@voidzero-dev/vite-plus-win32-arm64-msvc": "0.3.0", "@voidzero-dev/vite-plus-win32-x64-msvc": "0.3.0", "fsevents": "~2.3.3" }, "peerDependencies": { "@arethetypeswrong/core": "^0.18.1", "@types/node": "^20.19.0 || >=22.12.0", "@vitejs/devtools": "^0.4.0 || ^0.5.0", "esbuild": "^0.27.0 || ^0.28.0", "jiti": ">=1.21.0", "less": "^4.0.0", "publint": "^0.3.8", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "typescript": "^5.0.0 || ^6.0.0 || ^7.0.0", "unplugin-unused": "^0.5.0", "unrun": "*", "yaml": "^2.4.2" }, "optionalPeers": ["@arethetypeswrong/core", "@types/node", "@vitejs/devtools", "esbuild", "jiti", "less", "publint", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "typescript", "unplugin-unused", "unrun", "yaml"] }, "sha512-aOqoqIWaF+Q/geDU48pC2rVFEVSvLV1GGj/NdvhUiBhCZntoFNbwI+hjUeG8BMaPG67sOV6ey+/sgkdmGmKqaw=="], + + "@mosoo/agent-driver/vite-plus/@voidzero-dev/vite-plus-darwin-arm64": ["@voidzero-dev/vite-plus-darwin-arm64@0.3.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-9ADr1egZ8T4tJOqrpQLhoDl95Y74R95+bsvjmin0gy1C0eQVhpmcNnBfb07KFNhJioJp9MMO7F7Dx4fQL5SKsw=="], - "@mosoo/agent-driver/vite-plus/@oxlint/plugins": ["@oxlint/plugins@1.73.0", "", {}, "sha512-OhgMQeMmZA0dcFcX4/priaJZWdFECxiClgq6mRX6aatZEcV9PbKC3P3/v8U1hVjviT1i5U+vR8lAtBV6m4FXAA=="], + "@mosoo/agent-driver/vite-plus/@voidzero-dev/vite-plus-darwin-x64": ["@voidzero-dev/vite-plus-darwin-x64@0.3.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-GegasVCwNeDOkNyvhLOuwU1+T2JkjY/Tq+SOvwphUpVcqQ6OOAUq9LlpoXviO2QL/Kq2NbMYjiAfPKVSTLUFQw=="], - "@mosoo/agent-driver/vite-plus/@voidzero-dev/vite-plus-core": ["@voidzero-dev/vite-plus-core@0.2.5", "", { "dependencies": { "@oxc-project/runtime": "=0.139.0", "@oxc-project/types": "=0.139.0", "lightningcss": "^1.32.0", "postcss": "^8.5.6", "yuku-codegen": "^0.5.44", "yuku-parser": "^0.5.44" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@arethetypeswrong/core": "^0.18.1", "@types/node": "^20.19.0 || >=22.12.0", "@vitejs/devtools": "^0.3.0", "esbuild": "^0.27.0 || ^0.28.0", "jiti": ">=1.21.0", "less": "^4.0.0", "publint": "^0.3.8", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "typescript": "^5.0.0 || ^6.0.0 || ^7.0.0", "unplugin-unused": "^0.5.0", "unrun": "*", "yaml": "^2.4.2" }, "optionalPeers": ["@arethetypeswrong/core", "@types/node", "@vitejs/devtools", "esbuild", "jiti", "less", "publint", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "typescript", "unplugin-unused", "unrun", "yaml"] }, "sha512-fxMGImIOyOipwCX6udOD1S9Q1xXfaimv6kcRgLWBxLsy7oryAyXqVfoYr7bmmAdSDlIutHRgvA6eiqfJjARTHA=="], + "@mosoo/agent-driver/vite-plus/@voidzero-dev/vite-plus-linux-arm64-gnu": ["@voidzero-dev/vite-plus-linux-arm64-gnu@0.3.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-nYI3KNYXkXjRPsSdR4Lr7J2xMxfR1+TplWlG/dV37qVXWAjbyHpoAlbULjZBAVJMyXRNlcADhBrEwXe4g6s48A=="], - "@mosoo/agent-driver/vite-plus/@voidzero-dev/vite-plus-darwin-arm64": ["@voidzero-dev/vite-plus-darwin-arm64@0.2.5", "", { "os": "darwin", "cpu": "arm64" }, "sha512-M62R3gmoHZbhL+UHHTevJi9a3aJyY+Eid8GAOtxEsRMkHmJ8IwOSOBERXM3C4CULvEa/ORYKiUQnqo5ewF44Fw=="], + "@mosoo/agent-driver/vite-plus/@voidzero-dev/vite-plus-linux-arm64-musl": ["@voidzero-dev/vite-plus-linux-arm64-musl@0.3.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-HRlVA3AOcuGXmOdHhQ+Zv5XAaKbYF9si5rRHoOsKl0UyBo4txA3OoJfmP0WjanfLUNmu85JyO2dO1ptL4C6wgg=="], - "@mosoo/agent-driver/vite-plus/@voidzero-dev/vite-plus-darwin-x64": ["@voidzero-dev/vite-plus-darwin-x64@0.2.5", "", { "os": "darwin", "cpu": "x64" }, "sha512-a1h1dv/7QcnlqlN6yZBIPjgHSxbeyY/IcTxepTAOpPB7eAi1RPb1+cCkwo7c3MnDPJb3iZzwD0rm2+fOUoZp0w=="], + "@mosoo/agent-driver/vite-plus/@voidzero-dev/vite-plus-linux-x64-gnu": ["@voidzero-dev/vite-plus-linux-x64-gnu@0.3.0", "", { "os": "linux", "cpu": "x64" }, "sha512-9A+dFScPfwcrzF/rRR0zH8++2hOf6xtFmN/5LyzyfUywtw9MILXcC72IMcOeL6QRJwKUMsudi1rFeDE59azNvw=="], - "@mosoo/agent-driver/vite-plus/@voidzero-dev/vite-plus-linux-arm64-gnu": ["@voidzero-dev/vite-plus-linux-arm64-gnu@0.2.5", "", { "os": "linux", "cpu": "arm64" }, "sha512-t8bS8fA2a3OSAEaHdgJFmzd0TkWh9yAxIoKAprsOleIcUEmzDxhH8drTj9TPyTrChKpv0aJTsK5ZK3RzcCUkdg=="], + "@mosoo/agent-driver/vite-plus/@voidzero-dev/vite-plus-linux-x64-musl": ["@voidzero-dev/vite-plus-linux-x64-musl@0.3.0", "", { "os": "linux", "cpu": "x64" }, "sha512-KfIV3qaPdaOOE8JQMRHRE34FtZocl9O86XLTP6JMjDUlcx8FPgf8/fz/HFqJ8g232vM+JsgLI/YTVeXP8LkTKw=="], - "@mosoo/agent-driver/vite-plus/@voidzero-dev/vite-plus-linux-arm64-musl": ["@voidzero-dev/vite-plus-linux-arm64-musl@0.2.5", "", { "os": "linux", "cpu": "arm64" }, "sha512-vnsjQI3LEUYFMR3LCMqtAxaZav8BNypSAf8YzcFu9+Qtd1dcCrAUz9RrEBCIIqEiw0p0O+SrX2CcMHSSnzWbKA=="], + "@mosoo/agent-driver/vite-plus/@voidzero-dev/vite-plus-win32-arm64-msvc": ["@voidzero-dev/vite-plus-win32-arm64-msvc@0.3.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-KRhdy5K13AYx9KBfCVHRrK7zSZU+bMW9CL6gTai+UkJgAmDJi1kjdSNboZOjO8mrzUnTCrELgMI2tnstcxSTuA=="], - "@mosoo/agent-driver/vite-plus/@voidzero-dev/vite-plus-linux-x64-gnu": ["@voidzero-dev/vite-plus-linux-x64-gnu@0.2.5", "", { "os": "linux", "cpu": "x64" }, "sha512-3xlXrxIz8UKGcGefifkhoMpsTIMdgqikwQuDUqgG5O7/b2tetpK9aoT4C9b2fQGkhYUpCJwdD83AtywQ4EhoWA=="], + "@mosoo/agent-driver/vite-plus/@voidzero-dev/vite-plus-win32-x64-msvc": ["@voidzero-dev/vite-plus-win32-x64-msvc@0.3.0", "", { "os": "win32", "cpu": "x64" }, "sha512-7+G+GxGmxdpQO0zjiGnkZFXKGqm0CrVduebRsJd6ccuOuxCQYPxLcoHq4WOaGrh56SrAGS7XjhnQCrXRkzKUVQ=="], - "@mosoo/agent-driver/vite-plus/@voidzero-dev/vite-plus-linux-x64-musl": ["@voidzero-dev/vite-plus-linux-x64-musl@0.2.5", "", { "os": "linux", "cpu": "x64" }, "sha512-XylGiayBoD7vt1/SKfmh5FoBNdKI5EWlIb5Sd9A1oTQW8DLi97VcEgVZ7vh/8kG1OEe+9z1lyRis6RDql2KDUw=="], + "@mosoo/agent-driver/vite-plus/oxfmt": ["oxfmt@0.64.0", "", { "dependencies": { "tinypool": "2.1.0" }, "optionalDependencies": { "@oxfmt/binding-android-arm-eabi": "0.64.0", "@oxfmt/binding-android-arm64": "0.64.0", "@oxfmt/binding-darwin-arm64": "0.64.0", "@oxfmt/binding-darwin-x64": "0.64.0", "@oxfmt/binding-freebsd-x64": "0.64.0", "@oxfmt/binding-linux-arm-gnueabihf": "0.64.0", "@oxfmt/binding-linux-arm-musleabihf": "0.64.0", "@oxfmt/binding-linux-arm64-gnu": "0.64.0", "@oxfmt/binding-linux-arm64-musl": "0.64.0", "@oxfmt/binding-linux-ppc64-gnu": "0.64.0", "@oxfmt/binding-linux-riscv64-gnu": "0.64.0", "@oxfmt/binding-linux-riscv64-musl": "0.64.0", "@oxfmt/binding-linux-s390x-gnu": "0.64.0", "@oxfmt/binding-linux-x64-gnu": "0.64.0", "@oxfmt/binding-linux-x64-musl": "0.64.0", "@oxfmt/binding-openharmony-arm64": "0.64.0", "@oxfmt/binding-win32-arm64-msvc": "0.64.0", "@oxfmt/binding-win32-ia32-msvc": "0.64.0", "@oxfmt/binding-win32-x64-msvc": "0.64.0" }, "peerDependencies": { "svelte": "^5.0.0", "vite-plus": "*" }, "optionalPeers": ["svelte", "vite-plus"], "bin": { "oxfmt": "bin/oxfmt" } }, "sha512-XZ4GFBN/PLbXKq+0zrgpQfPKYuJlUuj+nzZJY7UpIbFMNyefNLCdN9EwViycNqnYcv0wrn0jXcQLlqJp8RCKBg=="], - "@mosoo/agent-driver/vite-plus/@voidzero-dev/vite-plus-win32-arm64-msvc": ["@voidzero-dev/vite-plus-win32-arm64-msvc@0.2.5", "", { "os": "win32", "cpu": "arm64" }, "sha512-eN1zvUAqXVXOC72WOVu9gz/jxr9tBrga/5lCsDjHeZDJ/bzhDVJ4eYZUDZzasPE1QCbAhmuIJsv0WzEUxdGAzA=="], + "@mosoo/agent-driver/vite-plus/oxlint": ["oxlint@1.79.0", "", { "optionalDependencies": { "@oxlint/binding-android-arm-eabi": "1.79.0", "@oxlint/binding-android-arm64": "1.79.0", "@oxlint/binding-darwin-arm64": "1.79.0", "@oxlint/binding-darwin-x64": "1.79.0", "@oxlint/binding-freebsd-x64": "1.79.0", "@oxlint/binding-linux-arm-gnueabihf": "1.79.0", "@oxlint/binding-linux-arm-musleabihf": "1.79.0", "@oxlint/binding-linux-arm64-gnu": "1.79.0", "@oxlint/binding-linux-arm64-musl": "1.79.0", "@oxlint/binding-linux-ppc64-gnu": "1.79.0", "@oxlint/binding-linux-riscv64-gnu": "1.79.0", "@oxlint/binding-linux-riscv64-musl": "1.79.0", "@oxlint/binding-linux-s390x-gnu": "1.79.0", "@oxlint/binding-linux-x64-gnu": "1.79.0", "@oxlint/binding-linux-x64-musl": "1.79.0", "@oxlint/binding-openharmony-arm64": "1.79.0", "@oxlint/binding-win32-arm64-msvc": "1.79.0", "@oxlint/binding-win32-ia32-msvc": "1.79.0", "@oxlint/binding-win32-x64-msvc": "1.79.0" }, "peerDependencies": { "oxlint-tsgolint": ">=7.0.2001", "vite-plus": "*" }, "optionalPeers": ["oxlint-tsgolint", "vite-plus"], "bin": { "oxlint": "bin/oxlint" } }, "sha512-hVJ9hq9m2unPS+Of4eJJgCPdIeCC+3DHEUX3tkmrPJr3OK2hz7PhXwgC+ZP71ZcYu8cCDEtQrqLxWNvxBppBVg=="], - "@mosoo/agent-driver/vite-plus/@voidzero-dev/vite-plus-win32-x64-msvc": ["@voidzero-dev/vite-plus-win32-x64-msvc@0.2.5", "", { "os": "win32", "cpu": "x64" }, "sha512-jgXVYK8crlR5cQ07vX5Qw2K+boNLKMxarPgE3/AyPPRUtGC8iCpZz0RCPoJHDmTh7848Ra5Fnt4LEfbcUv1ByQ=="], + "@mosoo/agent-driver/vite-plus/oxlint-tsgolint": ["oxlint-tsgolint@7.0.2001", "", { "optionalDependencies": { "@oxlint-tsgolint/darwin-arm64": "7.0.2001", "@oxlint-tsgolint/darwin-x64": "7.0.2001", "@oxlint-tsgolint/linux-arm64": "7.0.2001", "@oxlint-tsgolint/linux-x64": "7.0.2001", "@oxlint-tsgolint/win32-arm64": "7.0.2001", "@oxlint-tsgolint/win32-x64": "7.0.2001" }, "bin": { "tsgolint": "./bin/tsgolint.js" } }, "sha512-KjK/XLcXr1DSyonKhsuFqJRiuKqcyG9j3LJ8nkOsrLzGvodBPqzHOKauy10asLMDI0sUpvb+1sxlzff3udZvfg=="], - "@mosoo/agent-driver/vite-plus/oxfmt": ["oxfmt@0.58.0", "", { "dependencies": { "tinypool": "2.1.0" }, "optionalDependencies": { "@oxfmt/binding-android-arm-eabi": "0.58.0", "@oxfmt/binding-android-arm64": "0.58.0", "@oxfmt/binding-darwin-arm64": "0.58.0", "@oxfmt/binding-darwin-x64": "0.58.0", "@oxfmt/binding-freebsd-x64": "0.58.0", "@oxfmt/binding-linux-arm-gnueabihf": "0.58.0", "@oxfmt/binding-linux-arm-musleabihf": "0.58.0", "@oxfmt/binding-linux-arm64-gnu": "0.58.0", "@oxfmt/binding-linux-arm64-musl": "0.58.0", "@oxfmt/binding-linux-ppc64-gnu": "0.58.0", "@oxfmt/binding-linux-riscv64-gnu": "0.58.0", "@oxfmt/binding-linux-riscv64-musl": "0.58.0", "@oxfmt/binding-linux-s390x-gnu": "0.58.0", "@oxfmt/binding-linux-x64-gnu": "0.58.0", "@oxfmt/binding-linux-x64-musl": "0.58.0", "@oxfmt/binding-openharmony-arm64": "0.58.0", "@oxfmt/binding-win32-arm64-msvc": "0.58.0", "@oxfmt/binding-win32-ia32-msvc": "0.58.0", "@oxfmt/binding-win32-x64-msvc": "0.58.0" }, "peerDependencies": { "svelte": "^5.0.0", "vite-plus": "*" }, "optionalPeers": ["svelte", "vite-plus"], "bin": { "oxfmt": "bin/oxfmt" } }, "sha512-8feG/7NVEHDVwc1OUpP6Pks+TnaDFUw2jLLFIMi5bcmmwxAX2wBQvjSzj62RRTYBf2Op1Wt8xbkmagmPTR5ETg=="], + "@mosoo/api/@types/node/undici-types": ["undici-types@7.24.6", "", {}, "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg=="], - "@mosoo/agent-driver/vite-plus/oxlint": ["oxlint@1.73.0", "", { "optionalDependencies": { "@oxlint/binding-android-arm-eabi": "1.73.0", "@oxlint/binding-android-arm64": "1.73.0", "@oxlint/binding-darwin-arm64": "1.73.0", "@oxlint/binding-darwin-x64": "1.73.0", "@oxlint/binding-freebsd-x64": "1.73.0", "@oxlint/binding-linux-arm-gnueabihf": "1.73.0", "@oxlint/binding-linux-arm-musleabihf": "1.73.0", "@oxlint/binding-linux-arm64-gnu": "1.73.0", "@oxlint/binding-linux-arm64-musl": "1.73.0", "@oxlint/binding-linux-ppc64-gnu": "1.73.0", "@oxlint/binding-linux-riscv64-gnu": "1.73.0", "@oxlint/binding-linux-riscv64-musl": "1.73.0", "@oxlint/binding-linux-s390x-gnu": "1.73.0", "@oxlint/binding-linux-x64-gnu": "1.73.0", "@oxlint/binding-linux-x64-musl": "1.73.0", "@oxlint/binding-openharmony-arm64": "1.73.0", "@oxlint/binding-win32-arm64-msvc": "1.73.0", "@oxlint/binding-win32-ia32-msvc": "1.73.0", "@oxlint/binding-win32-x64-msvc": "1.73.0" }, "peerDependencies": { "oxlint-tsgolint": ">=0.24.0", "vite-plus": "*" }, "optionalPeers": ["oxlint-tsgolint", "vite-plus"], "bin": { "oxlint": "bin/oxlint" } }, "sha512-u91G9TJzU6yqKWNZUYprQB07W7YvntZXaRxQ6CkoytepYhLWUXWsr1M8zUJ34VatNPuUAr3Z8GH+O2A331CluQ=="], + "@mosoo/db/@types/node/undici-types": ["undici-types@7.24.6", "", {}, "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg=="], - "@mosoo/agent-driver/vite-plus/oxlint-tsgolint": ["oxlint-tsgolint@0.24.0", "", { "optionalDependencies": { "@oxlint-tsgolint/darwin-arm64": "0.24.0", "@oxlint-tsgolint/darwin-x64": "0.24.0", "@oxlint-tsgolint/linux-arm64": "0.24.0", "@oxlint-tsgolint/linux-x64": "0.24.0", "@oxlint-tsgolint/win32-arm64": "0.24.0", "@oxlint-tsgolint/win32-x64": "0.24.0" }, "bin": { "tsgolint": "bin/tsgolint.js" } }, "sha512-giCk5sEvG02d5tzPmFMX3hem8ndzEEu1xvGYS5OwNfO2WGl6ZVxt5LjE0yiMDoz94INI7XkXwgFAQiydPvVHDw=="], + "@mosoo/e2e/@types/node/undici-types": ["undici-types@7.24.6", "", {}, "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg=="], + + "@mosoo/web/@types/node/undici-types": ["undici-types@7.24.6", "", {}, "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg=="], "@tailwindcss/node/lightningcss/lightningcss-android-arm64": ["lightningcss-android-arm64@1.32.0", "", { "os": "android", "cpu": "arm64" }, "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg=="], @@ -3170,182 +3220,94 @@ "vite-plus/oxlint/@oxlint/binding-win32-x64-msvc": ["@oxlint/binding-win32-x64-msvc@1.67.0", "", { "os": "win32", "cpu": "x64" }, "sha512-bw24y+/1MHS4QDkons3YyHkPT9uCMoLHHgQhb+mb8NOjTYwub1CZ+K9Ngr8aO5DMrDrkqHwTzlTwFP2vS8Y/ZQ=="], - "vitest/vite/lightningcss": ["lightningcss@1.32.0", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-android-arm64": "1.32.0", "lightningcss-darwin-arm64": "1.32.0", "lightningcss-darwin-x64": "1.32.0", "lightningcss-freebsd-x64": "1.32.0", "lightningcss-linux-arm-gnueabihf": "1.32.0", "lightningcss-linux-arm64-gnu": "1.32.0", "lightningcss-linux-arm64-musl": "1.32.0", "lightningcss-linux-x64-gnu": "1.32.0", "lightningcss-linux-x64-musl": "1.32.0", "lightningcss-win32-arm64-msvc": "1.32.0", "lightningcss-win32-x64-msvc": "1.32.0" } }, "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ=="], - - "vitest/vite/postcss": ["postcss@8.5.19", "", { "dependencies": { "nanoid": "^3.3.12", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-Mz8SaolMd8nB+G13WkORcxQKHZ/NE4xXevtkJHVuG+guo9/wYKlIMTKAqGdEmYOXR2ijPjTYNHssizdaVSUNdQ=="], - - "vitest/vite/rolldown": ["rolldown@1.1.5", "", { "dependencies": { "@oxc-project/types": "=0.139.0", "@rolldown/pluginutils": "^1.0.0" }, "optionalDependencies": { "@rolldown/binding-android-arm64": "1.1.5", "@rolldown/binding-darwin-arm64": "1.1.5", "@rolldown/binding-darwin-x64": "1.1.5", "@rolldown/binding-freebsd-x64": "1.1.5", "@rolldown/binding-linux-arm-gnueabihf": "1.1.5", "@rolldown/binding-linux-arm64-gnu": "1.1.5", "@rolldown/binding-linux-arm64-musl": "1.1.5", "@rolldown/binding-linux-ppc64-gnu": "1.1.5", "@rolldown/binding-linux-s390x-gnu": "1.1.5", "@rolldown/binding-linux-x64-gnu": "1.1.5", "@rolldown/binding-linux-x64-musl": "1.1.5", "@rolldown/binding-openharmony-arm64": "1.1.5", "@rolldown/binding-wasm32-wasi": "1.1.5", "@rolldown/binding-win32-arm64-msvc": "1.1.5", "@rolldown/binding-win32-x64-msvc": "1.1.5" }, "bin": { "rolldown": "./bin/cli.mjs" } }, "sha512-t9z29cJjXf/vxQ8dyhCSpt6H6aSwHTk8cT5I3iy6SMXuFpk5mB6PL6XfC8PCwrPTx93udwKUm9HRteAlTGBLiA=="], - - "@mosoo/agent-driver/vite-plus/@voidzero-dev/vite-plus-core/@oxc-project/runtime": ["@oxc-project/runtime@0.139.0", "", {}, "sha512-WnuGdceWtRdqD7f3alOIDXN6bnGuGtFjtQf/dHjzgn2im7eKaYRJTEl2T1kFEWPhBWCDk+UDYgsTLUE5L6jc0w=="], - - "@mosoo/agent-driver/vite-plus/@voidzero-dev/vite-plus-core/lightningcss": ["lightningcss@1.32.0", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-android-arm64": "1.32.0", "lightningcss-darwin-arm64": "1.32.0", "lightningcss-darwin-x64": "1.32.0", "lightningcss-freebsd-x64": "1.32.0", "lightningcss-linux-arm-gnueabihf": "1.32.0", "lightningcss-linux-arm64-gnu": "1.32.0", "lightningcss-linux-arm64-musl": "1.32.0", "lightningcss-linux-x64-gnu": "1.32.0", "lightningcss-linux-x64-musl": "1.32.0", "lightningcss-win32-arm64-msvc": "1.32.0", "lightningcss-win32-x64-msvc": "1.32.0" } }, "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ=="], - - "@mosoo/agent-driver/vite-plus/@voidzero-dev/vite-plus-core/postcss": ["postcss@8.5.19", "", { "dependencies": { "nanoid": "^3.3.12", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-Mz8SaolMd8nB+G13WkORcxQKHZ/NE4xXevtkJHVuG+guo9/wYKlIMTKAqGdEmYOXR2ijPjTYNHssizdaVSUNdQ=="], - - "@mosoo/agent-driver/vite-plus/oxfmt/@oxfmt/binding-android-arm-eabi": ["@oxfmt/binding-android-arm-eabi@0.58.0", "", { "os": "android", "cpu": "arm" }, "sha512-Uz62sHduGGPftXtILGyxdSW4PX82rUg+rfdNqhsgxe881g4rIoXlIqmZQ6HVKcF4f+F8qMhdD03Bx5u7gmeTdg=="], - - "@mosoo/agent-driver/vite-plus/oxfmt/@oxfmt/binding-android-arm64": ["@oxfmt/binding-android-arm64@0.58.0", "", { "os": "android", "cpu": "arm64" }, "sha512-rD0lRaJp1b+9vw6X4A2dJWKukd6X8yxiicN4JxXcXayolmUypRZxk+lKR+fVOu5q/iYc0fh5fR4bgmfOfVlbaA=="], - - "@mosoo/agent-driver/vite-plus/oxfmt/@oxfmt/binding-darwin-arm64": ["@oxfmt/binding-darwin-arm64@0.58.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-uzbPPk7O6M+w2K65vcQ1woga3wgP8zghjL1KOG5b6qJ8dvYHZJ1VShaslg2KOK6yQIwCQtcMCXqLBM6sqXUNTg=="], - - "@mosoo/agent-driver/vite-plus/oxfmt/@oxfmt/binding-darwin-x64": ["@oxfmt/binding-darwin-x64@0.58.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-L0nKYDxU32oxeQqJj21W9SlIMnf81VZEhyah6iDvFhf5q0oynq498Fopth7blErUJVBpVtxQ98RMCfMPqpJX6w=="], - - "@mosoo/agent-driver/vite-plus/oxfmt/@oxfmt/binding-freebsd-x64": ["@oxfmt/binding-freebsd-x64@0.58.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-woNwfD58dC5PGS9LSLSD5JYfo/EFK5iG9vhDWkcCg3q78ag7KC8bpDqgvPHrMoXpx83OLXxoSOhu6z8FsVTHlg=="], - - "@mosoo/agent-driver/vite-plus/oxfmt/@oxfmt/binding-linux-arm-gnueabihf": ["@oxfmt/binding-linux-arm-gnueabihf@0.58.0", "", { "os": "linux", "cpu": "arm" }, "sha512-Sqs8nMLxuQpY21NKJ1u4stPDmO5hskBCNNh2E3AdCfI1QqWtf4m+Qn4mGEIUO4KGmuq3SWc/SZ80uy5IiwTCDw=="], - - "@mosoo/agent-driver/vite-plus/oxfmt/@oxfmt/binding-linux-arm-musleabihf": ["@oxfmt/binding-linux-arm-musleabihf@0.58.0", "", { "os": "linux", "cpu": "arm" }, "sha512-Vd4exzBI5B5hB9m22JiTQzIL23WvHo/Pe+sNXPNeBLXSP9swCBPKCEBRwKpmpQzYhlgYaCgfPcGXPKAJBRIiZQ=="], - - "@mosoo/agent-driver/vite-plus/oxfmt/@oxfmt/binding-linux-arm64-gnu": ["@oxfmt/binding-linux-arm64-gnu@0.58.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-bUWi5mHV+4Vi56RLHE1h6q/HHfwAIT3XoB9vJAVeRzfu5NriXM8y6eeJu0vlKa0C9kq2rq1sOWRClhdLHPocrg=="], - - "@mosoo/agent-driver/vite-plus/oxfmt/@oxfmt/binding-linux-arm64-musl": ["@oxfmt/binding-linux-arm64-musl@0.58.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-2ZHxemzgHcjtktAuVUwSoyXmGo/t+aF5tS1ciPpPei4rhSyrz3JOqDosXXrmhN/yLUSzJjtuW7ToTWqfQpCj2w=="], - - "@mosoo/agent-driver/vite-plus/oxfmt/@oxfmt/binding-linux-ppc64-gnu": ["@oxfmt/binding-linux-ppc64-gnu@0.58.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-AwKkVwjVmFQ3bcO7j0McGYAqCKH2a326fswfofng/E8VewCT/raeeGQr4huVhY704deK8AWASSTlxzMj0eZc6Q=="], - - "@mosoo/agent-driver/vite-plus/oxfmt/@oxfmt/binding-linux-riscv64-gnu": ["@oxfmt/binding-linux-riscv64-gnu@0.58.0", "", { "os": "linux", "cpu": "none" }, "sha512-xsRpTxfUnJF8D3AUKko/qyWdjw4GZVHlCVFuGlzSCTeewLmykKINW8em1+wx+axsDVtJJcMtvsiaXggXxrlHgw=="], - - "@mosoo/agent-driver/vite-plus/oxfmt/@oxfmt/binding-linux-riscv64-musl": ["@oxfmt/binding-linux-riscv64-musl@0.58.0", "", { "os": "linux", "cpu": "none" }, "sha512-Z4AYOTcy7nYEIiXwD62PlerimyYRcfJOgUbQAEBjXz098kxKuERBlRntofGy69HHhe9E0TLVNMl1yspVNu+efw=="], - - "@mosoo/agent-driver/vite-plus/oxfmt/@oxfmt/binding-linux-s390x-gnu": ["@oxfmt/binding-linux-s390x-gnu@0.58.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-A3nhhtZPC/TKVWOPj9q/H3p2znJDCcHWYlJBhWL8hGq/bFmBaNBHC8Np6E581yVq1w9Mi3rMDNzDalWvtUfJtQ=="], - - "@mosoo/agent-driver/vite-plus/oxfmt/@oxfmt/binding-linux-x64-gnu": ["@oxfmt/binding-linux-x64-gnu@0.58.0", "", { "os": "linux", "cpu": "x64" }, "sha512-2g+tVkgwqphw8R4hgo+kF4oz8+P5RwVOtr9+irsC7uwEp0e9j7Crw8kDGKL20uYlLPD7g02DqA61mC/UNYx98A=="], - - "@mosoo/agent-driver/vite-plus/oxfmt/@oxfmt/binding-linux-x64-musl": ["@oxfmt/binding-linux-x64-musl@0.58.0", "", { "os": "linux", "cpu": "x64" }, "sha512-rc15P6AbyyB7426aN8AakLd02Trb3a6ML/mmfAQeVHJEfVofWLcWIrBdy6zDEY+DIaL/s8E4GGPboVw+oP3+EA=="], - - "@mosoo/agent-driver/vite-plus/oxfmt/@oxfmt/binding-openharmony-arm64": ["@oxfmt/binding-openharmony-arm64@0.58.0", "", { "os": "none", "cpu": "arm64" }, "sha512-ZWoTM27/HYPOh9iq86DAbhPu9nXb8qKvvGU/h8OfliyVUFAMMNTLDkGsWDKKnDqIkqvZ9+dXlgUOsH1LYO3O7g=="], - - "@mosoo/agent-driver/vite-plus/oxfmt/@oxfmt/binding-win32-arm64-msvc": ["@oxfmt/binding-win32-arm64-msvc@0.58.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-LHZnqFXe2dEfkRI4XdZS/57nEOT/I4UCRX5IyM9v4GYW9XwQCjGe1IUK59SuKw3POwvcgWQ4pme2cYXmNqTNPg=="], - - "@mosoo/agent-driver/vite-plus/oxfmt/@oxfmt/binding-win32-ia32-msvc": ["@oxfmt/binding-win32-ia32-msvc@0.58.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-mZKpg20TpheCJym1rarcZCUJeW1sSruw8zAAaCYWvuVfwIUDN1CXdrPU/JgCWReXTCTrEfCB8Wyo3hh9jSZ2EA=="], - - "@mosoo/agent-driver/vite-plus/oxfmt/@oxfmt/binding-win32-x64-msvc": ["@oxfmt/binding-win32-x64-msvc@0.58.0", "", { "os": "win32", "cpu": "x64" }, "sha512-N/wUU4N5PZ2orBtI+Ko7MnMfYLfE7K91UrGMY/c/pYyHR3lA9kwst1XugkZx+92YcRh/Eo+iv2eTESSWXfiZPA=="], - - "@mosoo/agent-driver/vite-plus/oxlint/@oxlint/binding-android-arm-eabi": ["@oxlint/binding-android-arm-eabi@1.73.0", "", { "os": "android", "cpu": "arm" }, "sha512-HZQRN/UMBu+Ut+/9MiAChkbP4qZqrNOWBcNI45vOT40GVhbGR0JgHB87L48D4iAqFQIdVmeQYtV9RF89AjTKkg=="], - - "@mosoo/agent-driver/vite-plus/oxlint/@oxlint/binding-android-arm64": ["@oxlint/binding-android-arm64@1.73.0", "", { "os": "android", "cpu": "arm64" }, "sha512-Gp+KJRylv2aW7thRpG5p1KTxZq4ZJFbWowrKzufNq9d3ssl3r3JviYV45/+p+7CN1Nv0zDd1e8Ex0b/HUDq4TQ=="], - - "@mosoo/agent-driver/vite-plus/oxlint/@oxlint/binding-darwin-arm64": ["@oxlint/binding-darwin-arm64@1.73.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-3de96NdtXhxERMjIz7wsp2HYMY6pMQycGxFWac2mFecAx6VeARF/IqFb1QIaqiCRIdfzBwzTed+pCTCoiS+CYA=="], - - "@mosoo/agent-driver/vite-plus/oxlint/@oxlint/binding-darwin-x64": ["@oxlint/binding-darwin-x64@1.73.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-5zx/uPW32TiaOeVY1dQ/H5iOf0K1HOdFKOJhLqGl4o63+i1fpzoqqu/mKtd7OFgFjNCdhlyTGgjVkQTZm1ELcg=="], - - "@mosoo/agent-driver/vite-plus/oxlint/@oxlint/binding-freebsd-x64": ["@oxlint/binding-freebsd-x64@1.73.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-qNe4gKHaGnLuZJ8toUg90JAa0S2vTVvDw+0bRi3q1avXZXDT4u5mMeECf3nD4HYrbdn1O7dXqWut4onY/yx/Xg=="], - - "@mosoo/agent-driver/vite-plus/oxlint/@oxlint/binding-linux-arm-gnueabihf": ["@oxlint/binding-linux-arm-gnueabihf@1.73.0", "", { "os": "linux", "cpu": "arm" }, "sha512-cCehYh5hTbfShm/fxTD6wwrGUWIpvX+N5OxmAMhFhDeTGXvw+BeNj889tpxsFQ9ZLatQ6wImuY8tsKLZ+FMz7w=="], - - "@mosoo/agent-driver/vite-plus/oxlint/@oxlint/binding-linux-arm-musleabihf": ["@oxlint/binding-linux-arm-musleabihf@1.73.0", "", { "os": "linux", "cpu": "arm" }, "sha512-d5j5GDU/2dMgjVhw7TQT9ITrsIr1Y02KEXKyVGIXUkD+KiaxE9TP65FS2ZdgTBemQvoRL+gSBdbrIm3cQIeacg=="], - - "@mosoo/agent-driver/vite-plus/oxlint/@oxlint/binding-linux-arm64-gnu": ["@oxlint/binding-linux-arm64-gnu@1.73.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-Eyf1SrP3+yR1DI3OJgOY2Pvrr9dWP9TK37xPaDYycwTtlGlI45erJAVIfH5/m/xosDt6BupJYEFi47bvbTuuyw=="], - - "@mosoo/agent-driver/vite-plus/oxlint/@oxlint/binding-linux-arm64-musl": ["@oxlint/binding-linux-arm64-musl@1.73.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-IlT/OJApEDKaMmCooHuncgJZbbCe7T5QIWmTZBEtYscWvzPQuuEinVcid6kwQRVQOUdb7PUCz4jQHnaYXdfJXw=="], - - "@mosoo/agent-driver/vite-plus/oxlint/@oxlint/binding-linux-ppc64-gnu": ["@oxlint/binding-linux-ppc64-gnu@1.73.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-L+JYcb/vdg5fmcH08V6o0YYLU28cTH1SPNulwJdvK9NK49aXSkYy6oNpKBmddArVOXYqNepriDGiZ04G54kh1Q=="], - - "@mosoo/agent-driver/vite-plus/oxlint/@oxlint/binding-linux-riscv64-gnu": ["@oxlint/binding-linux-riscv64-gnu@1.73.0", "", { "os": "linux", "cpu": "none" }, "sha512-Qtk0g3bKV6OwWjIm7R8kQN1uOZRKQt/MODK2a8QfkwhTpXBD53ozx5XLVWLGDQAVyp2otLW4D2wB98XfAfMPGA=="], - - "@mosoo/agent-driver/vite-plus/oxlint/@oxlint/binding-linux-riscv64-musl": ["@oxlint/binding-linux-riscv64-musl@1.73.0", "", { "os": "linux", "cpu": "none" }, "sha512-wX0NQKZVxltkAOVmzFcpOaMpdaUvsq1Eqpx9tkAfl71UdkTlSo1R4AdAnGccR1Fm2+TzFgZ22CyyGuZ41RDr/A=="], - - "@mosoo/agent-driver/vite-plus/oxlint/@oxlint/binding-linux-s390x-gnu": ["@oxlint/binding-linux-s390x-gnu@1.73.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-vPe7UGBMWyiLTtnqS4xxgMQFSFGmtQwhwCxuiw6lXygaO6bVt0D8dFVg8Xv05eaiN3ybC0HXXHUAohFMFvqoCQ=="], - - "@mosoo/agent-driver/vite-plus/oxlint/@oxlint/binding-linux-x64-gnu": ["@oxlint/binding-linux-x64-gnu@1.73.0", "", { "os": "linux", "cpu": "x64" }, "sha512-2CwIWr9cemFC/CbRBWZvuk5mffz6ObmfFkfcC/9rTQ7f+icNhYr2kOjf9Rt8lLvugvkdGDOmkoVoFFHh6ClCTw=="], - - "@mosoo/agent-driver/vite-plus/oxlint/@oxlint/binding-linux-x64-musl": ["@oxlint/binding-linux-x64-musl@1.73.0", "", { "os": "linux", "cpu": "x64" }, "sha512-nDadfJgg7NBBxG0N560wOe7LLX5QiYp6qBaI7viuk5EUORFBktU/NfV0MbTqU3gTqQDCh4VyxKdo5VADxk9w8Q=="], - - "@mosoo/agent-driver/vite-plus/oxlint/@oxlint/binding-openharmony-arm64": ["@oxlint/binding-openharmony-arm64@1.73.0", "", { "os": "none", "cpu": "arm64" }, "sha512-wGjJC+NLH9xP+IKGn9RDW94ojJR/wPbg5WCnQjj/oReaOtCQthr8ws1zICe77JFmo4ouUdeTHHZL/ESGiF6Pmw=="], - - "@mosoo/agent-driver/vite-plus/oxlint/@oxlint/binding-win32-arm64-msvc": ["@oxlint/binding-win32-arm64-msvc@1.73.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-I7X47GPGljw225YUQ5SbC/rb1Kkdrd0yQf0x+hYxeKS6DpfjMbo9ccQPQ6LNY6BoJQ1sHhgDUGuMn5Vg5gHT6w=="], - - "@mosoo/agent-driver/vite-plus/oxlint/@oxlint/binding-win32-ia32-msvc": ["@oxlint/binding-win32-ia32-msvc@1.73.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-5lWj+3h+74Fm1jYOO9qkJA4xkAlZA099DkXppuXsk7UpnpZLttsefrZU469vChGaG6hcSqrkKXQOvMTZtbjeNg=="], - - "@mosoo/agent-driver/vite-plus/oxlint/@oxlint/binding-win32-x64-msvc": ["@oxlint/binding-win32-x64-msvc@1.73.0", "", { "os": "win32", "cpu": "x64" }, "sha512-WaNRvh4f6zY9CvUQk2YoA1O90ieWrIklI84+HXFr9Isjz9CSESrdqo/RtIYt4Dll/cAchqGDMehfaZd0vqEFZw=="], - - "@mosoo/agent-driver/vite-plus/oxlint-tsgolint/@oxlint-tsgolint/darwin-arm64": ["@oxlint-tsgolint/darwin-arm64@0.24.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-C2uMmwK5Bc4ri4ysZ6sA8Rcu+A5zBQTp6ml2u0CLLbRZp4kMFPV3yWk8B5DK9Aw7y9bbjogIm75tUwGLFzlsYQ=="], + "@mosoo/agent-driver/vite-plus/@voidzero-dev/vite-plus-core/@oxc-project/runtime": ["@oxc-project/runtime@0.146.0", "", {}, "sha512-lbXHIpZ1MmK6zuw5txlMdIZ2waLVUIU5Gnm3sEuwJOiqDfQfbtjeHscatmeBoxbv8+If9LFM6PGh/3DcDWYIYw=="], - "@mosoo/agent-driver/vite-plus/oxlint-tsgolint/@oxlint-tsgolint/darwin-x64": ["@oxlint-tsgolint/darwin-x64@0.24.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-Wgvt/1lRbDxmoNqWQKKcL+UIiqLmdJ+EWLpQa1qzoNVAfNB0PJpa82/8dH1twT/3rSs4zrP5TXPWl4juB71WuQ=="], + "@mosoo/agent-driver/vite-plus/oxfmt/@oxfmt/binding-android-arm-eabi": ["@oxfmt/binding-android-arm-eabi@0.64.0", "", { "os": "android", "cpu": "arm" }, "sha512-o6uzh/jTOQeAY5TdkAeXdqv7MBRcPxiRA08zrcBtkKj5cSu/FMu0Hl7Q6Fi1KCKyCWZ6lJVjBzdsJvsKltUsGQ=="], - "@mosoo/agent-driver/vite-plus/oxlint-tsgolint/@oxlint-tsgolint/linux-arm64": ["@oxlint-tsgolint/linux-arm64@0.24.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-PB1rxII7KV83+ASY4sSkXtqvpij6ME66+QCRL49uksi/ofs2Rf/UVboYr095n0Rkbl2wgvlsHGl6DHC361jQUQ=="], + "@mosoo/agent-driver/vite-plus/oxfmt/@oxfmt/binding-android-arm64": ["@oxfmt/binding-android-arm64@0.64.0", "", { "os": "android", "cpu": "arm64" }, "sha512-jRGSUeeP7p3Gynw2YaCVtjBIA6ZxY6bEB/ES5i54OhqmRTyuVg7ZgstEtzgq6GOAJd+2QZ5pvf+bFfmW5Mp9cw=="], - "@mosoo/agent-driver/vite-plus/oxlint-tsgolint/@oxlint-tsgolint/linux-x64": ["@oxlint-tsgolint/linux-x64@0.24.0", "", { "os": "linux", "cpu": "x64" }, "sha512-xcz3CxKmjTQLREtE/UShh+ruWmm9nAb7UM9zKcD65BStiuYgOakAKkPHl4YS5DztpVcDrE0+HqbOolTlRKYWmw=="], + "@mosoo/agent-driver/vite-plus/oxfmt/@oxfmt/binding-darwin-arm64": ["@oxfmt/binding-darwin-arm64@0.64.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-JINwtU2lW7nOFSqi+H2qplipNUqah9Gc1jgGmB82kTD4UnZrZIVxCJ9qEmFiKfjNq27gYLFhrUb0to86aCwMjw=="], - "@mosoo/agent-driver/vite-plus/oxlint-tsgolint/@oxlint-tsgolint/win32-arm64": ["@oxlint-tsgolint/win32-arm64@0.24.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-A2i6ZGBec3i20S7RaxkgHc6r3HYtD5Mn7j/mb22NkTz14u0JuudvTu6JggAnbGMcv8+dBKQI//EasxSPJLD8pw=="], + "@mosoo/agent-driver/vite-plus/oxfmt/@oxfmt/binding-darwin-x64": ["@oxfmt/binding-darwin-x64@0.64.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-gCmuswrgrOSajV4HCRFkVCGIruPq8bjYuPYgSE2WQB3mD6XrdyZ3JMSRZCkQ8zCxOyGWriBo6QoZ5nmMHQ1BfA=="], - "@mosoo/agent-driver/vite-plus/oxlint-tsgolint/@oxlint-tsgolint/win32-x64": ["@oxlint-tsgolint/win32-x64@0.24.0", "", { "os": "win32", "cpu": "x64" }, "sha512-0ZbGd9qRB6zs82moekaKdEvncRANq49EAwfNX62JpTS46feXUhKAuoyVDvZMj6Rywejylrmmu79Wo6faYCo4Ew=="], + "@mosoo/agent-driver/vite-plus/oxfmt/@oxfmt/binding-freebsd-x64": ["@oxfmt/binding-freebsd-x64@0.64.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-Ab8g7a38pT0MMImjh7anRSTve6buWBIlcXIFBYa5xl4s6UxEgKSc2xOOhbGtLwvXnEi2PsEDGoJh3oUU7xkehQ=="], - "vitest/vite/lightningcss/lightningcss-android-arm64": ["lightningcss-android-arm64@1.32.0", "", { "os": "android", "cpu": "arm64" }, "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg=="], + "@mosoo/agent-driver/vite-plus/oxfmt/@oxfmt/binding-linux-arm-gnueabihf": ["@oxfmt/binding-linux-arm-gnueabihf@0.64.0", "", { "os": "linux", "cpu": "arm" }, "sha512-BgvS3CoQ+Xy2deoZqEN8JVKabcCZi2RxA3yant8G9OAv9KuPJ9TCjHkqigzdHUVwErZxEP5d2bzLIEyKYyBDLg=="], - "vitest/vite/lightningcss/lightningcss-darwin-arm64": ["lightningcss-darwin-arm64@1.32.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ=="], + "@mosoo/agent-driver/vite-plus/oxfmt/@oxfmt/binding-linux-arm-musleabihf": ["@oxfmt/binding-linux-arm-musleabihf@0.64.0", "", { "os": "linux", "cpu": "arm" }, "sha512-QXpNxwoMj0YvnceCNZadNSden3bIcnvjn/sDp/rwZhRoZoZYGpHvtPyhGsdJz9uvT9GkaMW7SsLddurU56dt8w=="], - "vitest/vite/lightningcss/lightningcss-darwin-x64": ["lightningcss-darwin-x64@1.32.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w=="], + "@mosoo/agent-driver/vite-plus/oxfmt/@oxfmt/binding-linux-arm64-gnu": ["@oxfmt/binding-linux-arm64-gnu@0.64.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-BBgH3I1ppDsI5pZ4Pdhw0ceYxwVCfbU/bZEBCeZ6caRS9x0ZabErxubP7riGUn11PXZBhe8DYdjkDKP1FlVQ5w=="], - "vitest/vite/lightningcss/lightningcss-freebsd-x64": ["lightningcss-freebsd-x64@1.32.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig=="], + "@mosoo/agent-driver/vite-plus/oxfmt/@oxfmt/binding-linux-arm64-musl": ["@oxfmt/binding-linux-arm64-musl@0.64.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-v19HSjC/BGXdt26qEvKZtwAHgGmQ2Agcap2kQP+KIqoRZqivVzYth3ui2dJA1i+6/fjpjga85lIOaJJjQ/bOOw=="], - "vitest/vite/lightningcss/lightningcss-linux-arm-gnueabihf": ["lightningcss-linux-arm-gnueabihf@1.32.0", "", { "os": "linux", "cpu": "arm" }, "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw=="], + "@mosoo/agent-driver/vite-plus/oxfmt/@oxfmt/binding-linux-ppc64-gnu": ["@oxfmt/binding-linux-ppc64-gnu@0.64.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-PElLnOo4xFTBZrxPhgTIj0eHqZXwEBQoNWtb7facUV170T0B0FRET0iNbb3LUeLWTybkUW+vsdyv4ihOdyXGyw=="], - "vitest/vite/lightningcss/lightningcss-linux-arm64-gnu": ["lightningcss-linux-arm64-gnu@1.32.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ=="], + "@mosoo/agent-driver/vite-plus/oxfmt/@oxfmt/binding-linux-riscv64-gnu": ["@oxfmt/binding-linux-riscv64-gnu@0.64.0", "", { "os": "linux", "cpu": "none" }, "sha512-Qzsg15n4F5CH+MorcRW4MkAEMiLzXmeG+DiDSbP/bBTqCmWOH3K9DHryNrve+JHlV0txS+B6Z9P5Xz+cmWeL+g=="], - "vitest/vite/lightningcss/lightningcss-linux-arm64-musl": ["lightningcss-linux-arm64-musl@1.32.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg=="], + "@mosoo/agent-driver/vite-plus/oxfmt/@oxfmt/binding-linux-riscv64-musl": ["@oxfmt/binding-linux-riscv64-musl@0.64.0", "", { "os": "linux", "cpu": "none" }, "sha512-/GZ358wnQ/Ez4UVnCcZIi56JkY0sOdZ+B108pqXKqZz3jLS59F4KEAB1Qv3fRlObrFEk+3L2vUQ/xoPx+3vjXw=="], - "vitest/vite/lightningcss/lightningcss-linux-x64-gnu": ["lightningcss-linux-x64-gnu@1.32.0", "", { "os": "linux", "cpu": "x64" }, "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA=="], + "@mosoo/agent-driver/vite-plus/oxfmt/@oxfmt/binding-linux-s390x-gnu": ["@oxfmt/binding-linux-s390x-gnu@0.64.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-/C9We3DXegowfLXtVCYHeNiU9azwCDr5cQkEtCVlc74vyn+lLQSPApJ1CZmxAduqeq/Oi3gQ+IVptyhCaTMtkQ=="], - "vitest/vite/lightningcss/lightningcss-linux-x64-musl": ["lightningcss-linux-x64-musl@1.32.0", "", { "os": "linux", "cpu": "x64" }, "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg=="], + "@mosoo/agent-driver/vite-plus/oxfmt/@oxfmt/binding-linux-x64-gnu": ["@oxfmt/binding-linux-x64-gnu@0.64.0", "", { "os": "linux", "cpu": "x64" }, "sha512-91KM2CeRWscIEHlj1NsW2WSnzGeq1Ehq+39bfDowTdkn+fcvK/x4Y1RcyqT7glyBjZio0ldkeCG6Usj3v7ASog=="], - "vitest/vite/lightningcss/lightningcss-win32-arm64-msvc": ["lightningcss-win32-arm64-msvc@1.32.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw=="], + "@mosoo/agent-driver/vite-plus/oxfmt/@oxfmt/binding-linux-x64-musl": ["@oxfmt/binding-linux-x64-musl@0.64.0", "", { "os": "linux", "cpu": "x64" }, "sha512-gw7uEk9I+7zoT1EYLra1eWArIzNcz8e3jkv+Noo2+o2T7wPvsNSQbfoa4DSfZlvn1i6mJ05RiZ4/omaXPDNhQg=="], - "vitest/vite/lightningcss/lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.32.0", "", { "os": "win32", "cpu": "x64" }, "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q=="], + "@mosoo/agent-driver/vite-plus/oxfmt/@oxfmt/binding-openharmony-arm64": ["@oxfmt/binding-openharmony-arm64@0.64.0", "", { "os": "none", "cpu": "arm64" }, "sha512-HYHFf616FHSPSO07c09mjmXBfQ73wIVM3m0txOiooa5XZkGoxFd6B14PVj0LB0DXIqJ6wAO/dDR/NX/5UUaqnw=="], - "vitest/vite/postcss/nanoid": ["nanoid@3.3.16", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q=="], + "@mosoo/agent-driver/vite-plus/oxfmt/@oxfmt/binding-win32-arm64-msvc": ["@oxfmt/binding-win32-arm64-msvc@0.64.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-uQjFp081IZSWD6VAofX2iO2z01awAdHmfC+NrieWIPKrT2hZKQDyq/U18M7ifC0sm0Wz8aHY/p6+FDYIzs/CrQ=="], - "vitest/vite/rolldown/@oxc-project/types": ["@oxc-project/types@0.139.0", "", {}, "sha512-r9gHphtCs+1M7J0pw6Sn/hh/Wpa/iQrOOkrNAlVLF/gHq+/CJmHIWKKUUhdWjcD6CIa8idarspCsASiXCXvFUw=="], + "@mosoo/agent-driver/vite-plus/oxfmt/@oxfmt/binding-win32-ia32-msvc": ["@oxfmt/binding-win32-ia32-msvc@0.64.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-lNM6byTAQ881jugzFu8juJTbNRgsUTlswMA6pJmwi1XDvmIqnnb49lcUAs5gz94fCJLrVN+/X3s3jOKqx23WIQ=="], - "vitest/vite/rolldown/@rolldown/binding-android-arm64": ["@rolldown/binding-android-arm64@1.1.5", "", { "os": "android", "cpu": "arm64" }, "sha512-lZg8fqIv2v7FF237bwMgzGZEJvGL79/s5knJ/i6FmsGF4XXlzccZ4jb+TrFIxtSSxFtIpdsgrPZeMk1I9AFcyQ=="], + "@mosoo/agent-driver/vite-plus/oxfmt/@oxfmt/binding-win32-x64-msvc": ["@oxfmt/binding-win32-x64-msvc@0.64.0", "", { "os": "win32", "cpu": "x64" }, "sha512-BtmbtL/QjMtF1a6C3CqoDluH2IfB6fJt62E+B9RFfUPtFk4Iz9PFS6+y/SzzOvSxc7aUk2Kphwg7Dh8lMbwu6g=="], - "vitest/vite/rolldown/@rolldown/binding-darwin-arm64": ["@rolldown/binding-darwin-arm64@1.1.5", "", { "os": "darwin", "cpu": "arm64" }, "sha512-51Bnx9pNiMRKSUNtBfySkNJ9vMU9Hh3I1ozDd6gyPPYzaXCfnptUcEZxXGYFn+ul2dtcMUiqGR1Yai2K10uoTw=="], + "@mosoo/agent-driver/vite-plus/oxlint/@oxlint/binding-android-arm-eabi": ["@oxlint/binding-android-arm-eabi@1.79.0", "", { "os": "android", "cpu": "arm" }, "sha512-TebFaaMklO/RXzTv7PucaCq9l3X6D1gA+C8H6K4njtjFOV+zWE9MKLpulcJZN9bzytbUbQIY0mZuz12nQ5Kv4Q=="], - "vitest/vite/rolldown/@rolldown/binding-darwin-x64": ["@rolldown/binding-darwin-x64@1.1.5", "", { "os": "darwin", "cpu": "x64" }, "sha512-Tm+gbfC0aHu1tBA/JvKQh32S0K6YgCHkiAF4/W6xX0K0RmNuc94VeK419dJoE65R5aRxmo+noZQSWrAMF6yb6g=="], + "@mosoo/agent-driver/vite-plus/oxlint/@oxlint/binding-android-arm64": ["@oxlint/binding-android-arm64@1.79.0", "", { "os": "android", "cpu": "arm64" }, "sha512-KqqnOtAVgNsPPF0YSodkFZA1O80jcKoCZCTu3bgsszxA+MrMP9TLzfXitKjEj1FmrPprKDMdRDMmY3weESO9sg=="], - "vitest/vite/rolldown/@rolldown/binding-freebsd-x64": ["@rolldown/binding-freebsd-x64@1.1.5", "", { "os": "freebsd", "cpu": "x64" }, "sha512-JMzDKCCXq93YccG5gz3hvOs1oXRKAf0XYpfOS88e+wZrC8Iugj6j68867vrYZkvpDDpKn/KoKORThmchMpF6TA=="], + "@mosoo/agent-driver/vite-plus/oxlint/@oxlint/binding-darwin-arm64": ["@oxlint/binding-darwin-arm64@1.79.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-BVC2nsMzqQzRDPc5RhixkZ+m1p7iH4bxRRvqkbwDXX0PlQKm1BPy8J8cRjnAFafOq2QzI+BfO3vE8w2GZ3CBag=="], - "vitest/vite/rolldown/@rolldown/binding-linux-arm-gnueabihf": ["@rolldown/binding-linux-arm-gnueabihf@1.1.5", "", { "os": "linux", "cpu": "arm" }, "sha512-uML21j2K5TfPGutKxub+M+nLjZIrWjXQ5Grx4lCe/nimTj9B4L63zHpjXLl4y0L3mcm2htEQIb06oCG/szerNw=="], + "@mosoo/agent-driver/vite-plus/oxlint/@oxlint/binding-darwin-x64": ["@oxlint/binding-darwin-x64@1.79.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-p6Lm+snmhGuLKL1+CpCV8L6ijkE/qJzK2H2jG9+eKJT0n31RbY4FLsdhexekgP3bLpw4Kgde+9DZuDZQ4yIInA=="], - "vitest/vite/rolldown/@rolldown/binding-linux-arm64-gnu": ["@rolldown/binding-linux-arm64-gnu@1.1.5", "", { "os": "linux", "cpu": "arm64" }, "sha512-navSiuTMogvnQoZoM/v+l3ZWo50/NTwSHSzheABx/RCnmUPaKwq9qSo4Br2OYRs21+Fz8uFqITZM3H4opOB0/Q=="], + "@mosoo/agent-driver/vite-plus/oxlint/@oxlint/binding-freebsd-x64": ["@oxlint/binding-freebsd-x64@1.79.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-qDMm0dXZnoHyRqSL4N4xUq82T4sqK5cbKSjvd/dF/YbMUXc2R1wEPf+vmA5S0qUmi0nwXfNbjXBtZaIqzQLIMg=="], - "vitest/vite/rolldown/@rolldown/binding-linux-arm64-musl": ["@rolldown/binding-linux-arm64-musl@1.1.5", "", { "os": "linux", "cpu": "arm64" }, "sha512-lAryqH7IteztmCXQXk0etKj4wBQ7Gx5S6LjKhsgp9zb8I5bsuvU/2llH1hDQcjsFeqIsovMVN339/8pUDDBXxA=="], + "@mosoo/agent-driver/vite-plus/oxlint/@oxlint/binding-linux-arm-gnueabihf": ["@oxlint/binding-linux-arm-gnueabihf@1.79.0", "", { "os": "linux", "cpu": "arm" }, "sha512-2od7s0nuKPzqyUZAWk9KkCyGg7eI9dwFPZg+20lB15fKFkVZ0c9ZFxqPfiBAyDTlTkh9stPI0t+JlPCqMbItVA=="], - "vitest/vite/rolldown/@rolldown/binding-linux-ppc64-gnu": ["@rolldown/binding-linux-ppc64-gnu@1.1.5", "", { "os": "linux", "cpu": "ppc64" }, "sha512-fsK/sNBnxzBlL4O1JNrZakVQxPspqpED5dLtNsZS9oOKmtSpdNIzxH2kkol5HYTWJN47sE20ztMJPxfZ89qGOg=="], + "@mosoo/agent-driver/vite-plus/oxlint/@oxlint/binding-linux-arm-musleabihf": ["@oxlint/binding-linux-arm-musleabihf@1.79.0", "", { "os": "linux", "cpu": "arm" }, "sha512-ZOQUjkzDnvlhSE3+tWC3YXx94MMl+sYMlwH+u1+YGApGHOJP/YAc8ZBRFOXZ6eOBmxtXAWuS/fBcdZr8qqNO1A=="], - "vitest/vite/rolldown/@rolldown/binding-linux-s390x-gnu": ["@rolldown/binding-linux-s390x-gnu@1.1.5", "", { "os": "linux", "cpu": "s390x" }, "sha512-gLYb4BIadlfTOYT5gO503n8zQjXflgzpD0FcyKh0Mzx3rqCZKnHoJWV9xe1KXUJ5lx2JfcSHr/mhzS0PC/McAA=="], + "@mosoo/agent-driver/vite-plus/oxlint/@oxlint/binding-linux-arm64-gnu": ["@oxlint/binding-linux-arm64-gnu@1.79.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-lu158FR4nGqGeRS3BQvtG85wRgU/Fy4MD5Cxp1hzJXizGiLo6u2742wJSCDKh8cFcZntvX7fcxlq4mMmfryH1g=="], - "vitest/vite/rolldown/@rolldown/binding-linux-x64-gnu": ["@rolldown/binding-linux-x64-gnu@1.1.5", "", { "os": "linux", "cpu": "x64" }, "sha512-FjcpEKUyJygHgs1o50VYNvkt5+7Le/VEdYt0AkRpkL33MnyQfwr8l5mXwMmfmTbyMPr5vJLC+8/Gd9gXnwU1QQ=="], + "@mosoo/agent-driver/vite-plus/oxlint/@oxlint/binding-linux-arm64-musl": ["@oxlint/binding-linux-arm64-musl@1.79.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-mbpKQeE2aflTjddaHK7MP8KP/OFbUM++lt5M635ENM8IyIdK0jm2t9pb+2v9mVVIvhF6TqA4l7F79Pll1mi+uw=="], - "vitest/vite/rolldown/@rolldown/binding-linux-x64-musl": ["@rolldown/binding-linux-x64-musl@1.1.5", "", { "os": "linux", "cpu": "x64" }, "sha512-Me+PfPI2TMeOQk0gYWfLQZtTktrmzbr8cDboqX83XKc7UrgAi55gF+2dUkWdxd19n55Essp2yeca+O9N5rBxHg=="], + "@mosoo/agent-driver/vite-plus/oxlint/@oxlint/binding-linux-ppc64-gnu": ["@oxlint/binding-linux-ppc64-gnu@1.79.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-WpGNua7gaxaHnpSDeog2ji8IDHn/QLPl9LPzwkR/FvVv58vT5BcXjRXnU+wbu3N75cpeha8CdC7ho/U2OIsB4g=="], - "vitest/vite/rolldown/@rolldown/binding-openharmony-arm64": ["@rolldown/binding-openharmony-arm64@1.1.5", "", { "os": "none", "cpu": "arm64" }, "sha512-yc5WrLzXks6zCQfn9Oxr8pORKyl/pF+QjHmW/Qx3qu0oyrrNC+y2JLTU1E2rcWYAmzlnqngWXHQjy51VzW70Vw=="], + "@mosoo/agent-driver/vite-plus/oxlint/@oxlint/binding-linux-riscv64-gnu": ["@oxlint/binding-linux-riscv64-gnu@1.79.0", "", { "os": "linux", "cpu": "none" }, "sha512-tK1E93A5LVzISg4ngpKJnfTs7EqtIUceGI7MQ4GyDjJiLi8wPCkEyKlj2xkyKWZ1yzkDJyLHTBJ5/iFWRdnJvg=="], - "vitest/vite/rolldown/@rolldown/binding-win32-arm64-msvc": ["@rolldown/binding-win32-arm64-msvc@1.1.5", "", { "os": "win32", "cpu": "arm64" }, "sha512-gHv82k63z4qpV5+Q1y/12KrK0ltWBukVDI8nZcbT7Tt/ZlOIVwppazneq0F93oDxTo3IgAMEDIoQh3E2n6mVsw=="], + "@mosoo/agent-driver/vite-plus/oxlint/@oxlint/binding-linux-riscv64-musl": ["@oxlint/binding-linux-riscv64-musl@1.79.0", "", { "os": "linux", "cpu": "none" }, "sha512-qhQvUIrngXivA2A9pQ+xPCychztn/5qUv7yS3gDwXv3w7Rag+eTeeXWmRyx+t7XsW5x6LuY/8AsTq36UgFIblg=="], - "vitest/vite/rolldown/@rolldown/binding-win32-x64-msvc": ["@rolldown/binding-win32-x64-msvc@1.1.5", "", { "os": "win32", "cpu": "x64" }, "sha512-tTZuDBPw85tEN5PQi1pnEBzDy0Z49HtScLAbD5t6hyeU92A95pRWaSMw1GZZi/RwgSgUIl0xrSlXIT/9QzvYSA=="], + "@mosoo/agent-driver/vite-plus/oxlint/@oxlint/binding-linux-s390x-gnu": ["@oxlint/binding-linux-s390x-gnu@1.79.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-sv6AaVgU/eE6u+6WFiQVDcPPwTxP6IJMSB9k701W2r/r6Tx465e8vPvVyRxquNH4Vy6KwRNu90mVbxXJN8+5gg=="], - "@mosoo/agent-driver/vite-plus/@voidzero-dev/vite-plus-core/lightningcss/lightningcss-android-arm64": ["lightningcss-android-arm64@1.32.0", "", { "os": "android", "cpu": "arm64" }, "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg=="], + "@mosoo/agent-driver/vite-plus/oxlint/@oxlint/binding-linux-x64-gnu": ["@oxlint/binding-linux-x64-gnu@1.79.0", "", { "os": "linux", "cpu": "x64" }, "sha512-iFZL02deziHslb3jEX9KdqlAkYoo4fGyotchKDzdfK1f5mxlIBeiQeHhvK3iFpuEJSB4ma/qeFn9oxPiwnhUPQ=="], - "@mosoo/agent-driver/vite-plus/@voidzero-dev/vite-plus-core/lightningcss/lightningcss-darwin-arm64": ["lightningcss-darwin-arm64@1.32.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ=="], + "@mosoo/agent-driver/vite-plus/oxlint/@oxlint/binding-linux-x64-musl": ["@oxlint/binding-linux-x64-musl@1.79.0", "", { "os": "linux", "cpu": "x64" }, "sha512-3DtZR2raqObnh7wXZoFYFd0Fw7skBvcb3f7A+/lkEiDuh8hrE6vv9b/62Qxao1a9/OeHLw/FcXlXzgsW9wTRFg=="], - "@mosoo/agent-driver/vite-plus/@voidzero-dev/vite-plus-core/lightningcss/lightningcss-darwin-x64": ["lightningcss-darwin-x64@1.32.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w=="], + "@mosoo/agent-driver/vite-plus/oxlint/@oxlint/binding-openharmony-arm64": ["@oxlint/binding-openharmony-arm64@1.79.0", "", { "os": "none", "cpu": "arm64" }, "sha512-Oatt4GuA1WJkqzk2ozx4HrWROOi7opV3AKDw/U8qDIqeTqzsjn5K2x3REJMNjU3/KU/Bkq96Zi3CknaiDTaC/Q=="], - "@mosoo/agent-driver/vite-plus/@voidzero-dev/vite-plus-core/lightningcss/lightningcss-freebsd-x64": ["lightningcss-freebsd-x64@1.32.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig=="], + "@mosoo/agent-driver/vite-plus/oxlint/@oxlint/binding-win32-arm64-msvc": ["@oxlint/binding-win32-arm64-msvc@1.79.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-NAgZr9Qp8nIA9rpo0JEvwiabTF/2UVqBNnupBG9X4kxXcQoScJUTi+qHhvabb9s/thgj5wQ4XcIaJvb+ZMgoKw=="], - "@mosoo/agent-driver/vite-plus/@voidzero-dev/vite-plus-core/lightningcss/lightningcss-linux-arm-gnueabihf": ["lightningcss-linux-arm-gnueabihf@1.32.0", "", { "os": "linux", "cpu": "arm" }, "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw=="], + "@mosoo/agent-driver/vite-plus/oxlint/@oxlint/binding-win32-ia32-msvc": ["@oxlint/binding-win32-ia32-msvc@1.79.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-+KyXjIvcpaXmWW/j9NNY5yWjrIVxaX18VyIheQy3jwc2GSYgpCr7MGI/HxIGQ/shAL5IWEKbhsqoMpAO5Stiog=="], - "@mosoo/agent-driver/vite-plus/@voidzero-dev/vite-plus-core/lightningcss/lightningcss-linux-arm64-gnu": ["lightningcss-linux-arm64-gnu@1.32.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ=="], + "@mosoo/agent-driver/vite-plus/oxlint/@oxlint/binding-win32-x64-msvc": ["@oxlint/binding-win32-x64-msvc@1.79.0", "", { "os": "win32", "cpu": "x64" }, "sha512-mEelcCMMBS57sIXh2veGMNy+pQwuGtcMxHxGIZWQ5Ba9pJ5jCCUFOZB9E2JhBaxGsURe+WGe0zJp4RVre52gpQ=="], - "@mosoo/agent-driver/vite-plus/@voidzero-dev/vite-plus-core/lightningcss/lightningcss-linux-arm64-musl": ["lightningcss-linux-arm64-musl@1.32.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg=="], + "@mosoo/agent-driver/vite-plus/oxlint-tsgolint/@oxlint-tsgolint/darwin-arm64": ["@oxlint-tsgolint/darwin-arm64@7.0.2001", "", { "os": "darwin", "cpu": "arm64" }, "sha512-CUJEdbSZ54+Xy9OXqOhWLTKZKV0BBiV7C2i/ygyVmXtkUNXx5YCzN8DpSSshTAKktoL7S+tnQ/ftFG/i7X896w=="], - "@mosoo/agent-driver/vite-plus/@voidzero-dev/vite-plus-core/lightningcss/lightningcss-linux-x64-gnu": ["lightningcss-linux-x64-gnu@1.32.0", "", { "os": "linux", "cpu": "x64" }, "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA=="], + "@mosoo/agent-driver/vite-plus/oxlint-tsgolint/@oxlint-tsgolint/darwin-x64": ["@oxlint-tsgolint/darwin-x64@7.0.2001", "", { "os": "darwin", "cpu": "x64" }, "sha512-pXfBb5BqONCcgrXQNUZWXgiYmRSWJzd97S8i41VVOh6ut0tyo+cJ5FKFpczDHxiVNfj/3e7c9B4MtztNdpIVCw=="], - "@mosoo/agent-driver/vite-plus/@voidzero-dev/vite-plus-core/lightningcss/lightningcss-linux-x64-musl": ["lightningcss-linux-x64-musl@1.32.0", "", { "os": "linux", "cpu": "x64" }, "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg=="], + "@mosoo/agent-driver/vite-plus/oxlint-tsgolint/@oxlint-tsgolint/linux-arm64": ["@oxlint-tsgolint/linux-arm64@7.0.2001", "", { "os": "linux", "cpu": "arm64" }, "sha512-roP7zujb/QDPzDwEKsFFpzNHHy91/Y7oX9vQXk78ekyZtcQj1QXDIMH33gjDdHBfRl4K9pZ36xhRgrP4Zr+R8A=="], - "@mosoo/agent-driver/vite-plus/@voidzero-dev/vite-plus-core/lightningcss/lightningcss-win32-arm64-msvc": ["lightningcss-win32-arm64-msvc@1.32.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw=="], + "@mosoo/agent-driver/vite-plus/oxlint-tsgolint/@oxlint-tsgolint/linux-x64": ["@oxlint-tsgolint/linux-x64@7.0.2001", "", { "os": "linux", "cpu": "x64" }, "sha512-UDezNqdECVmngu2TPnjaS1YoAmcTaBoI5lV9vk3VahBxoi+I5r9k3iJTT7qZoYWOXTD/7T7bNcwRgrocR6BscQ=="], - "@mosoo/agent-driver/vite-plus/@voidzero-dev/vite-plus-core/lightningcss/lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.32.0", "", { "os": "win32", "cpu": "x64" }, "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q=="], + "@mosoo/agent-driver/vite-plus/oxlint-tsgolint/@oxlint-tsgolint/win32-arm64": ["@oxlint-tsgolint/win32-arm64@7.0.2001", "", { "os": "win32", "cpu": "arm64" }, "sha512-uJZhqB6pdXLuN+AD1F5082byyQti/NPmJA77GtcFlmT2HzRelqbNls3SaIqxpjdFgvSBF9g0yOKGBkGFg7kX8Q=="], - "@mosoo/agent-driver/vite-plus/@voidzero-dev/vite-plus-core/postcss/nanoid": ["nanoid@3.3.16", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q=="], + "@mosoo/agent-driver/vite-plus/oxlint-tsgolint/@oxlint-tsgolint/win32-x64": ["@oxlint-tsgolint/win32-x64@7.0.2001", "", { "os": "win32", "cpu": "x64" }, "sha512-FkDRm8hx9OwzGQqyWG1tO5QrTLRApff9DzSgpz9QZau37BR8d1VYKOxMLGf6shPZntJFoTwIIJYT68VndYDCog=="], } } diff --git a/pkgs/ag-ui-session/src/ag-ui-session-compaction.ts b/pkgs/ag-ui-session/src/ag-ui-session-compaction.ts index 811e40b6..6353e7a8 100644 --- a/pkgs/ag-ui-session/src/ag-ui-session-compaction.ts +++ b/pkgs/ag-ui-session/src/ag-ui-session-compaction.ts @@ -1,5 +1,8 @@ import type { AgUiSessionEvent } from "./ag-ui-session-events"; -import { REPLACEABLE_CUSTOM_EVENT_NAMES } from "./custom-event-registry"; +import { + GENERATION_REPLACEABLE_CUSTOM_EVENT_NAMES, + REPLACEABLE_CUSTOM_EVENT_NAMES, +} from "./custom-event-registry"; interface CompactAgUiSessionEventsOptions { skipToolCallArgs?: boolean; @@ -8,6 +11,9 @@ interface CompactAgUiSessionEventsOptions { type ReplaceableCustomEvent = Extract; const replaceableCustomEventNames = new Set(REPLACEABLE_CUSTOM_EVENT_NAMES); +const generationReplaceableCustomEventNames = new Set( + GENERATION_REPLACEABLE_CUSTOM_EVENT_NAMES, +); function isReplaceableCustomEvent(event: AgUiSessionEvent): event is ReplaceableCustomEvent { return ( @@ -17,6 +23,17 @@ function isReplaceableCustomEvent(event: AgUiSessionEvent): event is Replaceable ); } +function getReplaceableCustomEventKey(event: ReplaceableCustomEvent): string { + if ( + generationReplaceableCustomEventNames.has(event.name) && + event.name === "mosoo.session.tasks.replaced" + ) { + return JSON.stringify([event.name, event.value.runId, event.value.driverInstanceId]); + } + + return event.name; +} + export function isAgUiSessionEventBufferable(event: AgUiSessionEvent): boolean { if ( event.type === "REASONING_MESSAGE_CONTENT" || @@ -175,15 +192,15 @@ function appendEventToCompactedEvents( } function findLatestReplaceableEventIndexes(events: AgUiSessionEvent[]): Set { - const indexesByName = new Map(); + const indexesByKey = new Map(); events.forEach((event, index) => { if (isReplaceableCustomEvent(event)) { - indexesByName.set(event.name, index); + indexesByKey.set(getReplaceableCustomEventKey(event), index); } }); - return new Set(indexesByName.values()); + return new Set(indexesByKey.values()); } export function compactAgUiSessionEvents( diff --git a/pkgs/ag-ui-session/src/ag-ui-session-events.ts b/pkgs/ag-ui-session/src/ag-ui-session-events.ts index 6f541843..412ed66e 100644 --- a/pkgs/ag-ui-session/src/ag-ui-session-events.ts +++ b/pkgs/ag-ui-session/src/ag-ui-session-events.ts @@ -25,6 +25,7 @@ import type { MosooSessionRuntimeTimelineValue, MosooSessionRunUpdatedValue, MosooSessionStoppedValue, + MosooSessionTasksReplacedValue, MosooSessionSyncRequestValue, MosooSessionUsageUpdatedValue, } from "./custom-event-values"; @@ -145,6 +146,7 @@ export interface MosooCustomEventValueByName { [CUSTOM_EVENT_REGISTRY.sessionRuntimeTimelineUpdated.name]: MosooSessionRuntimeTimelineValue; [CUSTOM_EVENT_REGISTRY.sessionRunUpdated.name]: MosooSessionRunUpdatedValue; [CUSTOM_EVENT_REGISTRY.sessionStopped.name]: MosooSessionStoppedValue; + [CUSTOM_EVENT_REGISTRY.sessionTasksReplaced.name]: MosooSessionTasksReplacedValue; [CUSTOM_EVENT_REGISTRY.sessionSyncRequest.name]: MosooSessionSyncRequestValue; [CUSTOM_EVENT_REGISTRY.sessionUsageUpdated.name]: MosooSessionUsageUpdatedValue; } diff --git a/pkgs/ag-ui-session/src/custom-event-registry.ts b/pkgs/ag-ui-session/src/custom-event-registry.ts index b577dd8c..85eabb88 100644 --- a/pkgs/ag-ui-session/src/custom-event-registry.ts +++ b/pkgs/ag-ui-session/src/custom-event-registry.ts @@ -4,7 +4,7 @@ interface MosooCustomEventRegistration< TName extends string, TDirection extends MosooCustomEventDirection, > { - readonly coalescing?: "replace"; + readonly coalescing?: "replace" | "replace_generation"; readonly direction: TDirection; readonly name: TName; readonly visibility?: "all_consumers" | "owner_debug"; @@ -88,6 +88,11 @@ export const MOSOO_CUSTOM_EVENT = { direction: "server", name: "mosoo.session.stopped", }, + sessionTasksReplaced: { + coalescing: "replace_generation", + direction: "server", + name: "mosoo.session.tasks.replaced", + }, sessionSyncRequest: { direction: "viewer", name: "mosoo.session.sync.request", @@ -116,7 +121,7 @@ export type MosooViewerEventName = Extract< >["name"]; export type ReplaceableCustomEventName = Extract< MosooCustomEventRegistrationValue, - { coalescing: "replace" } + { coalescing: "replace" | "replace_generation" } >["name"]; export type OwnerDebugCustomEventName = Extract< MosooCustomEventRegistrationValue, @@ -128,7 +133,10 @@ const customEventName = (event: MosooCustomEventRegistrationValue): MosooCustomE event.name; export const REPLACEABLE_CUSTOM_EVENT_NAMES = customEventRegistrations - .filter((event) => "coalescing" in event && event.coalescing === "replace") + .filter((event) => "coalescing" in event) + .map(customEventName); +export const GENERATION_REPLACEABLE_CUSTOM_EVENT_NAMES = customEventRegistrations + .filter((event) => "coalescing" in event && event.coalescing === "replace_generation") .map(customEventName); export const OWNER_DEBUG_CUSTOM_EVENT_NAMES = customEventRegistrations .filter((event) => "visibility" in event && event.visibility === "owner_debug") diff --git a/pkgs/ag-ui-session/src/custom-event-schema.ts b/pkgs/ag-ui-session/src/custom-event-schema.ts index 84091178..7780d124 100644 --- a/pkgs/ag-ui-session/src/custom-event-schema.ts +++ b/pkgs/ag-ui-session/src/custom-event-schema.ts @@ -1,3 +1,4 @@ +import { AgentTaskSnapshot } from "@mosoo/contracts/session"; import { type } from "arktype"; import { NullableString, OptionalNullableString } from "./ag-ui-session-schema-primitives"; @@ -204,6 +205,7 @@ export const MosooServerCustomEventSchema = type.or( name: eventNameLiteral(MOSOO_CUSTOM_EVENT.sessionRunUpdated.name), type: '"CUSTOM"', value: { + driverInstanceId: NullableString, lifecycle: '"IDLE" | "RUNNING" | "RESCHEDULING" | "TERMINATED"', run: SessionRunViewSchema, }, @@ -218,6 +220,11 @@ export const MosooServerCustomEventSchema = type.or( reason: "string", }, }), + type({ + name: eventNameLiteral(MOSOO_CUSTOM_EVENT.sessionTasksReplaced.name), + type: '"CUSTOM"', + value: AgentTaskSnapshot, + }), type({ name: eventNameLiteral(MOSOO_CUSTOM_EVENT.sessionUsageUpdated.name), type: '"CUSTOM"', diff --git a/pkgs/ag-ui-session/src/custom-event-values.ts b/pkgs/ag-ui-session/src/custom-event-values.ts index ec8ba2b5..4135e442 100644 --- a/pkgs/ag-ui-session/src/custom-event-values.ts +++ b/pkgs/ag-ui-session/src/custom-event-values.ts @@ -1,3 +1,5 @@ +import type { AgentTaskSnapshot } from "@mosoo/contracts/session"; + import type { SessionCommandOption, SessionConfigOption, @@ -12,10 +14,13 @@ import type { } from "./live-state"; export interface MosooSessionRunUpdatedValue { + driverInstanceId: string | null; lifecycle: SessionLifecycleStatus; run: SessionRunView; } +export type MosooSessionTasksReplacedValue = AgentTaskSnapshot; + export interface MosooSessionSyncRequestValue { reason: "manual" | "reconnect"; } diff --git a/pkgs/ag-ui-session/src/live-state-custom.reducer.ts b/pkgs/ag-ui-session/src/live-state-custom.reducer.ts index a8c5502a..6441b7ec 100644 --- a/pkgs/ag-ui-session/src/live-state-custom.reducer.ts +++ b/pkgs/ag-ui-session/src/live-state-custom.reducer.ts @@ -163,13 +163,47 @@ function updateRunState( } const run = mergeSessionRunUpdate(state.run, event.value.run); + const driverInstanceId = + event.value.lifecycle !== "RUNNING" || isTerminalRunStatus(run.status) + ? null + : (event.value.driverInstanceId ?? + (state.run.id === run.id ? state.infra.driverInstanceId : null)); return touchSessionLiveState({ ...state, - infra: updateInfraForRun(state, run), + infra: { + ...updateInfraForRun(state, run), + driverInstanceId, + }, lifecycle: event.value.lifecycle, permissionRequests: isTerminalRunStatus(run.status) ? [] : state.permissionRequests, run, + taskSnapshot: + event.value.lifecycle !== "RUNNING" || + isTerminalRunStatus(run.status) || + state.taskSnapshot?.runId !== run.id || + state.taskSnapshot.driverInstanceId !== driverInstanceId + ? null + : state.taskSnapshot, + }); +} + +function replaceAgentTasks( + state: SessionLiveState, + event: CustomEventByName, +): SessionLiveState { + if ( + state.lifecycle !== "RUNNING" || + isTerminalRunStatus(state.run.status) || + state.run.id !== event.value.runId || + state.infra.driverInstanceId !== event.value.driverInstanceId + ) { + return state; + } + + return touchSessionLiveState({ + ...state, + taskSnapshot: event.value, }); } @@ -187,6 +221,7 @@ function updateInfraForRescheduling( reconnecting: true, }, lifecycle: "RESCHEDULING", + taskSnapshot: null, }); } @@ -198,12 +233,14 @@ function updateInfraForAgentChange( ...state, infra: { ...state.infra, + driverInstanceId: null, lastFailureMessage: null, lastFailureReason: `agent.${event.value.operation}`, lastSeen: currentIsoTimestamp(), reconnecting: true, }, lifecycle: "RESCHEDULING", + taskSnapshot: null, }); } @@ -232,12 +269,14 @@ function updateInfraForReady( ...state, infra: { ...state.infra, + driverInstanceId: null, lastFailureMessage: null, lastFailureReason: null, lastSeen: event.value.readyAt, reconnecting: false, }, lifecycle: "IDLE", + taskSnapshot: null, }); } @@ -253,6 +292,7 @@ function stopSession( ...terminalState, infra: { ...terminalState.infra, + driverInstanceId: null, lastFailureMessage: message, lastFailureReason: event.value.reason, lastSeen, @@ -260,6 +300,7 @@ function stopSession( }, lifecycle: "TERMINATED", permissionRequests: [], + taskSnapshot: null, run: { ...terminalState.run, completedAt: terminalState.run.completedAt ?? currentIsoTimestamp(), @@ -316,6 +357,10 @@ function updateRuntimeCustomState( return updateRunState(state, event); } + case CUSTOM_EVENT_REGISTRY.sessionTasksReplaced.name: { + return replaceAgentTasks(state, event); + } + case CUSTOM_EVENT_REGISTRY.sessionInfraRescheduling.name: { return updateInfraForRescheduling(state, event); } diff --git a/pkgs/ag-ui-session/src/live-state.reducer-core.ts b/pkgs/ag-ui-session/src/live-state.reducer-core.ts index 0c7267ad..7a42588a 100644 --- a/pkgs/ag-ui-session/src/live-state.reducer-core.ts +++ b/pkgs/ag-ui-session/src/live-state.reducer-core.ts @@ -15,6 +15,7 @@ export function touchSessionLiveState(state: SessionLiveState): SessionLiveState export function defaultInfraState(): SessionLiveState["infra"] { return { + driverInstanceId: null, lastFailureMessage: null, lastFailureReason: null, lastSeen: null, diff --git a/pkgs/ag-ui-session/src/live-state.reducer.ts b/pkgs/ag-ui-session/src/live-state.reducer.ts index abeeba80..3f9e5b49 100644 --- a/pkgs/ag-ui-session/src/live-state.reducer.ts +++ b/pkgs/ag-ui-session/src/live-state.reducer.ts @@ -85,6 +85,13 @@ function normalizeSessionLiveStateShape(state: SessionLiveState): SessionLiveSta lifecycle: terminalState.lifecycle, permissionRequests: isTerminalRunStatus(run.status) ? [] : terminalState.permissionRequests, readiness: terminalState.readiness ?? null, + taskSnapshot: + terminalState.lifecycle !== "RUNNING" || + isTerminalRunStatus(terminalState.run.status) || + terminalState.taskSnapshot?.runId !== terminalState.run.id || + terminalState.taskSnapshot?.driverInstanceId !== terminalState.infra.driverInstanceId + ? null + : terminalState.taskSnapshot, }; } @@ -211,12 +218,15 @@ function applyEvent(state: SessionLiveState, event: AgUiEvent): SessionLiveState ...currentState, infra: { ...currentState.infra, + driverInstanceId: + currentState.run.id === event.runId ? currentState.infra.driverInstanceId : null, lastFailureMessage: null, lastFailureReason: null, reconnecting: false, }, lifecycle: "RUNNING", permissionRequests: [], + taskSnapshot: state.run.id === event.runId ? state.taskSnapshot : null, run: { ...state.run, completedAt: null, @@ -237,12 +247,14 @@ function applyEvent(state: SessionLiveState, event: AgUiEvent): SessionLiveState ...finishedState, infra: { ...finishedState.infra, + driverInstanceId: null, lastFailureMessage: null, lastFailureReason: null, reconnecting: false, }, lifecycle: "IDLE", permissionRequests: [], + taskSnapshot: null, run: { ...finishedState.run, completedAt: currentIsoTimestamp(), @@ -262,12 +274,14 @@ function applyEvent(state: SessionLiveState, event: AgUiEvent): SessionLiveState ...failedState, infra: { ...failedState.infra, + driverInstanceId: null, lastFailureMessage: event.message, lastFailureReason: event.code ?? "runtime.error", reconnecting: false, }, lifecycle: "IDLE", permissionRequests: [], + taskSnapshot: null, run: { ...failedState.run, completedAt: currentIsoTimestamp(), @@ -343,6 +357,7 @@ export function createInitialSessionLiveState(input: { }, sessionId: input.sessionId, title: input.title, + taskSnapshot: null, updatedAt: now, usage: null, viewerId: input.viewerId, diff --git a/pkgs/ag-ui-session/src/live-state.ts b/pkgs/ag-ui-session/src/live-state.ts index d0207a43..76217e59 100644 --- a/pkgs/ag-ui-session/src/live-state.ts +++ b/pkgs/ag-ui-session/src/live-state.ts @@ -1,3 +1,5 @@ +import type { AgentTaskSnapshot } from "@mosoo/contracts/session"; + export interface SessionViewPlanEntry { content: string; priority: "high" | "medium" | "low"; @@ -74,6 +76,7 @@ export interface SessionReadinessSnapshotView { } export interface SessionInfraState { + driverInstanceId: string | null; lastFailureReason: string | null; lastFailureMessage: string | null; lastSeen: string | null; @@ -173,6 +176,7 @@ export interface SessionLiveState { run: SessionRunView; sessionId: string; title: string | null; + taskSnapshot: AgentTaskSnapshot | null; updatedAt: string | null; usage: SessionUsageSummary | null; viewerId: string; diff --git a/pkgs/ag-ui-session/src/session-live-state-schema.ts b/pkgs/ag-ui-session/src/session-live-state-schema.ts index 3614c606..a288845f 100644 --- a/pkgs/ag-ui-session/src/session-live-state-schema.ts +++ b/pkgs/ag-ui-session/src/session-live-state-schema.ts @@ -1,3 +1,4 @@ +import { AgentTaskSnapshot } from "@mosoo/contracts/session"; import { type } from "arktype"; import { @@ -152,6 +153,7 @@ export const SessionRunViewSchema = type({ }); export const SessionInfraStateSchema = type({ + driverInstanceId: NullableString, lastFailureMessage: NullableString, lastFailureReason: NullableString, lastSeen: NullableString, @@ -172,6 +174,7 @@ export const SessionLiveStateSchema = type({ run: SessionRunViewSchema, sessionId: "string", title: NullableString, + taskSnapshot: type("null").or(AgentTaskSnapshot), updatedAt: NullableString, usage: type("null").or(SessionUsageSummarySchema), viewerId: "string", diff --git a/pkgs/ag-ui-session/tests/ag-ui-session-codec.test.ts b/pkgs/ag-ui-session/tests/ag-ui-session-codec.test.ts index 60a2ced9..899df2e9 100644 --- a/pkgs/ag-ui-session/tests/ag-ui-session-codec.test.ts +++ b/pkgs/ag-ui-session/tests/ag-ui-session-codec.test.ts @@ -12,6 +12,10 @@ function validStateSnapshot(): SessionLiveState { return { ...state, + infra: { + ...state.infra, + driverInstanceId: "driver-1", + }, lifecycle: "RUNNING", run: { ...state.run, @@ -56,6 +60,20 @@ describe("AG-UI session codec boundary", () => { expect(() => parseAgUiSessionEventJson(JSON.stringify(event))).toThrow(); }); + test("requires the expected driver fence in state snapshots", () => { + const { driverInstanceId: _driverInstanceId, ...infraWithoutDriver } = + validStateSnapshot().infra; + const event = { + snapshot: { + ...validStateSnapshot(), + infra: infraWithoutDriver, + }, + type: "STATE_SNAPSHOT", + }; + + expect(() => parseAgUiSessionEventJson(JSON.stringify(event))).toThrow(); + }); + test("parses standard AG-UI text content events", () => { expect( parseAgUiSessionEventJson( @@ -89,6 +107,31 @@ describe("AG-UI session codec boundary", () => { expect(() => parseAgUiSessionEventJson(JSON.stringify(event))).toThrow(); }); + test("enforces the canonical task snapshot bounds at the AG-UI boundary", () => { + const event = { + name: "mosoo.session.tasks.replaced", + type: "CUSTOM", + value: { + driverInstanceId: "driver-1", + runId: "run-1", + tasks: Array.from({ length: 257 }, (_, index) => ({ taskId: `task-${index}` })), + }, + }; + + expect(() => parseAgUiSessionEventJson(JSON.stringify(event))).toThrow(); + expect(() => + parseAgUiSessionEventJson( + JSON.stringify({ + ...event, + value: { + ...event.value, + tasks: [{ taskId: "🙂".repeat(65) }], + }, + }), + ), + ).toThrow(); + }); + test("rejects nullable numeric fields when the custom contract requires numbers", () => { const event = { name: "mosoo.session.runtime.timing", diff --git a/pkgs/ag-ui-session/tests/ag-ui-session-compaction.test.ts b/pkgs/ag-ui-session/tests/ag-ui-session-compaction.test.ts index 3671affb..536dc26c 100644 --- a/pkgs/ag-ui-session/tests/ag-ui-session-compaction.test.ts +++ b/pkgs/ag-ui-session/tests/ag-ui-session-compaction.test.ts @@ -82,6 +82,45 @@ describe("AG-UI session event compaction", () => { ]); }); + test("replaces task snapshots only within the same run and driver generation", () => { + const first = createServerCustomEvent("mosoo.session.tasks.replaced", { + driverInstanceId: "driver-2", + runId: "run-2", + tasks: [{ taskId: "first" }], + }); + const latest = createServerCustomEvent("mosoo.session.tasks.replaced", { + driverInstanceId: "driver-2", + runId: "run-2", + tasks: [{ taskId: "latest" }], + }); + + expect(compactAgUiSessionEvents([first, latest])).toEqual([latest]); + }); + + test("preserves task snapshots across run and driver generations", () => { + const oldRun = createServerCustomEvent("mosoo.session.tasks.replaced", { + driverInstanceId: "driver-1", + runId: "run-1", + tasks: [{ taskId: "old-run" }], + }); + const oldDriver = createServerCustomEvent("mosoo.session.tasks.replaced", { + driverInstanceId: "driver-1", + runId: "run-2", + tasks: [{ taskId: "old-driver" }], + }); + const current = createServerCustomEvent("mosoo.session.tasks.replaced", { + driverInstanceId: "driver-2", + runId: "run-2", + tasks: [{ taskId: "current" }], + }); + + expect(compactAgUiSessionEvents([oldRun, oldDriver, current])).toEqual([ + oldRun, + oldDriver, + current, + ]); + }); + test("appends compacted batches using full compaction semantics", () => { const first = createServerCustomEvent("mosoo.session.info.updated", { title: "Draft", @@ -129,6 +168,15 @@ describe("AG-UI session event compaction", () => { type: EventType.RUN_STARTED, }), ).toBe(false); + expect( + isAgUiSessionEventBufferable( + createServerCustomEvent("mosoo.session.tasks.replaced", { + driverInstanceId: "driver-1", + runId: "run-1", + tasks: [], + }), + ), + ).toBe(true); expect( getAgUiSessionEventDeltaLength({ runId: "run-1", diff --git a/pkgs/ag-ui-session/tests/live-state.reducer.test.ts b/pkgs/ag-ui-session/tests/live-state.reducer.test.ts index b14543ef..6619c32e 100644 --- a/pkgs/ag-ui-session/tests/live-state.reducer.test.ts +++ b/pkgs/ag-ui-session/tests/live-state.reducer.test.ts @@ -29,6 +29,18 @@ function runView( }; } +function runningRunUpdatedEvent(runId: string, driverInstanceId: string): AgUiEvent { + return { + name: MOSOO_CUSTOM_EVENT.sessionRunUpdated.name, + type: "CUSTOM", + value: { + driverInstanceId, + lifecycle: "RUNNING", + run: runView({ id: runId, status: "running" }), + }, + }; +} + describe("session live-state transcript reducer", () => { test("replaces live state when a state snapshot arrives", () => { const userMessage = createSessionLiveStateMessage({ @@ -408,6 +420,7 @@ describe("session live-state transcript reducer", () => { name: "mosoo.session.run.completed", type: "CUSTOM", value: { + driverInstanceId: null, lifecycle: "IDLE", run: runView({ completedAt: "2026-04-30T00:00:03.000Z", @@ -438,6 +451,7 @@ describe("session live-state transcript reducer", () => { name: MOSOO_CUSTOM_EVENT.sessionRunUpdated.name, type: "CUSTOM", value: { + driverInstanceId: null, lifecycle: "IDLE", run: runView({ completedAt: "2026-04-30T00:00:03.000Z", @@ -484,6 +498,7 @@ describe("session live-state transcript reducer", () => { name: MOSOO_CUSTOM_EVENT.sessionRunUpdated.name, type: "CUSTOM", value: { + driverInstanceId: null, lifecycle: "IDLE", run: runView({ completedAt: "2026-04-30T00:00:03.000Z", @@ -598,6 +613,323 @@ describe("session live-state transcript reducer", () => { expect(nextState.permissionRequests).toEqual([]); }); + test("atomically replaces and explicitly empties the current run task snapshot", () => { + const nextState = applyAgUiEventsToSessionLiveState(baseState(), [ + runningRunUpdatedEvent("run-1", "driver-1"), + { + name: MOSOO_CUSTOM_EVENT.sessionTasksReplaced.name, + type: "CUSTOM", + value: { + driverInstanceId: "driver-1", + runId: "run-1", + tasks: [{ taskId: "task-1", title: "First" }], + }, + }, + { + name: MOSOO_CUSTOM_EVENT.sessionTasksReplaced.name, + type: "CUSTOM", + value: { + driverInstanceId: "driver-1", + runId: "run-1", + tasks: [{ taskId: "task-2", taskType: "review" }], + }, + }, + { + name: MOSOO_CUSTOM_EVENT.sessionTasksReplaced.name, + type: "CUSTOM", + value: { + driverInstanceId: "driver-1", + runId: "run-1", + tasks: [], + }, + }, + ]); + + expect(nextState.taskSnapshot).toEqual({ + driverInstanceId: "driver-1", + runId: "run-1", + tasks: [], + }); + }); + + test("rejects stale run and driver task snapshots", () => { + const nextState = applyAgUiEventsToSessionLiveState(baseState(), [ + runningRunUpdatedEvent("run-2", "driver-2"), + { + name: MOSOO_CUSTOM_EVENT.sessionTasksReplaced.name, + type: "CUSTOM", + value: { + driverInstanceId: "driver-2", + runId: "run-2", + tasks: [{ taskId: "current" }], + }, + }, + { + name: MOSOO_CUSTOM_EVENT.sessionTasksReplaced.name, + type: "CUSTOM", + value: { + driverInstanceId: "driver-1", + runId: "run-1", + tasks: [{ taskId: "old-run" }], + }, + }, + { + name: MOSOO_CUSTOM_EVENT.sessionTasksReplaced.name, + type: "CUSTOM", + value: { + driverInstanceId: "driver-1", + runId: "run-2", + tasks: [{ taskId: "old-driver" }], + }, + }, + ]); + + expect(nextState.taskSnapshot?.tasks).toEqual([{ taskId: "current" }]); + }); + + test("does not restore a delayed task snapshot after agent replacement starts", () => { + const nextState = applyAgUiEventsToSessionLiveState(baseState(), [ + runningRunUpdatedEvent("run-1", "driver-1"), + { + name: MOSOO_CUSTOM_EVENT.sessionTasksReplaced.name, + type: "CUSTOM", + value: { + driverInstanceId: "driver-1", + runId: "run-1", + tasks: [{ taskId: "before-reschedule" }], + }, + }, + { + name: MOSOO_CUSTOM_EVENT.agentUpdating.name, + type: "CUSTOM", + value: { + agentId: "agent-1", + operation: "restartDriver", + startedAt: "2026-05-26T00:00:01.000Z", + }, + }, + { + name: MOSOO_CUSTOM_EVENT.sessionTasksReplaced.name, + type: "CUSTOM", + value: { + driverInstanceId: "driver-1", + runId: "run-1", + tasks: [{ taskId: "delayed-old-driver" }], + }, + }, + ]); + + expect(nextState.lifecycle).toBe("RESCHEDULING"); + expect(nextState.taskSnapshot).toBeNull(); + }); + + test("normalizes state snapshots to hide tasks outside the running lifecycle", () => { + const snapshot: SessionLiveState = { + ...baseState(), + infra: { + ...baseState().infra, + driverInstanceId: "driver-1", + }, + lifecycle: "RESCHEDULING", + run: { + ...baseState().run, + id: "run-1", + status: "running", + }, + taskSnapshot: { + driverInstanceId: "driver-1", + runId: "run-1", + tasks: [{ taskId: "stale" }], + }, + }; + + const nextState = applyAgUiEventsToSessionLiveState(baseState(), [ + { snapshot, type: "STATE_SNAPSHOT" }, + ]); + + expect(nextState.taskSnapshot).toBeNull(); + }); + + test.each([ + ["old first", ["driver-1", "driver-2"]], + ["new first", ["driver-2", "driver-1"]], + ] as const)("fences replacement driver snapshots when %s", (_label, arrivalOrder) => { + const stateBeforeReplacement = applyAgUiEventsToSessionLiveState(baseState(), [ + runningRunUpdatedEvent("run-1", "driver-1"), + { + name: MOSOO_CUSTOM_EVENT.sessionTasksReplaced.name, + type: "CUSTOM", + value: { + driverInstanceId: "driver-1", + runId: "run-1", + tasks: [{ taskId: "before-reschedule" }], + }, + }, + ]); + const replacementEvents: AgUiEvent[] = [ + runningRunUpdatedEvent("run-1", "driver-2"), + ...arrivalOrder.map( + (driverInstanceId) => + ({ + name: MOSOO_CUSTOM_EVENT.sessionTasksReplaced.name, + type: "CUSTOM", + value: { + driverInstanceId, + runId: "run-1", + tasks: [ + { + taskId: + driverInstanceId === "driver-2" ? "replacement-driver" : "delayed-old-driver", + }, + ], + }, + }) satisfies AgUiEvent, + ), + ]; + const nextState = applyAgUiEventsToSessionLiveState(stateBeforeReplacement, replacementEvents); + + expect(nextState.taskSnapshot).toEqual({ + driverInstanceId: "driver-2", + runId: "run-1", + tasks: [{ taskId: "replacement-driver" }], + }); + }); + + test("keeps the expected driver across a same-run API lifecycle update", () => { + const nextState = applyAgUiEventsToSessionLiveState(baseState(), [ + runningRunUpdatedEvent("run-1", "driver-1"), + { + name: MOSOO_CUSTOM_EVENT.sessionTasksReplaced.name, + type: "CUSTOM", + value: { + driverInstanceId: "driver-1", + runId: "run-1", + tasks: [{ taskId: "task-1" }], + }, + }, + { + name: MOSOO_CUSTOM_EVENT.sessionRunUpdated.name, + type: "CUSTOM", + value: { + driverInstanceId: null, + lifecycle: "RUNNING", + run: runView({ id: "run-1", status: "running" }), + }, + }, + ]); + + expect(nextState.infra.driverInstanceId).toBe("driver-1"); + expect(nextState.taskSnapshot?.tasks).toEqual([{ taskId: "task-1" }]); + }); + + test("accepts the same driver again after a websocket reconnect", () => { + const nextState = applyAgUiEventsToSessionLiveState(baseState(), [ + runningRunUpdatedEvent("run-1", "driver-1"), + { + name: MOSOO_CUSTOM_EVENT.sessionTasksReplaced.name, + type: "CUSTOM", + value: { + driverInstanceId: "driver-1", + runId: "run-1", + tasks: [{ taskId: "before-reconnect" }], + }, + }, + { + name: MOSOO_CUSTOM_EVENT.sessionInfraRescheduling.name, + type: "CUSTOM", + value: { + lastSeen: "2026-05-26T00:00:01.000Z", + reason: "websocket.closed", + rescheduleStartedAt: "2026-05-26T00:00:01.000Z", + }, + }, + { + name: MOSOO_CUSTOM_EVENT.sessionInfraRunning.name, + type: "CUSTOM", + value: { resumedAt: "2026-05-26T00:00:02.000Z" }, + }, + { + name: MOSOO_CUSTOM_EVENT.sessionTasksReplaced.name, + type: "CUSTOM", + value: { + driverInstanceId: "driver-1", + runId: "run-1", + tasks: [{ taskId: "after-reconnect" }], + }, + }, + ]); + + expect(nextState.taskSnapshot?.tasks).toEqual([{ taskId: "after-reconnect" }]); + }); + + test.each([ + ["run terminal", { runId: "run-1", threadId: "session-1", type: "RUN_FINISHED" } as const], + [ + "rescheduling", + { + name: MOSOO_CUSTOM_EVENT.sessionInfraRescheduling.name, + type: "CUSTOM", + value: { + lastSeen: "2026-05-26T00:00:01.000Z", + reason: "websocket.closed", + rescheduleStartedAt: "2026-05-26T00:00:01.000Z", + }, + } as const, + ], + [ + "agent replacement", + { + name: MOSOO_CUSTOM_EVENT.agentUpdating.name, + type: "CUSTOM", + value: { + agentId: "agent-1", + operation: "restartDriver", + startedAt: "2026-05-26T00:00:01.000Z", + }, + } as const, + ], + [ + "agent ready", + { + name: MOSOO_CUSTOM_EVENT.agentReady.name, + type: "CUSTOM", + value: { + agentId: "agent-1", + operation: "restartDriver", + readyAt: "2026-05-26T00:00:01.000Z", + }, + } as const, + ], + [ + "session stop", + { + name: MOSOO_CUSTOM_EVENT.sessionStopped.name, + type: "CUSTOM", + value: { reason: "session.stopped" }, + } as const, + ], + ])("clears task snapshots on %s", (label, boundaryEvent) => { + const stateWithTasks = applyAgUiEventsToSessionLiveState(baseState(), [ + runningRunUpdatedEvent("run-1", "driver-1"), + { + name: MOSOO_CUSTOM_EVENT.sessionTasksReplaced.name, + type: "CUSTOM", + value: { + driverInstanceId: "driver-1", + runId: "run-1", + tasks: [{ taskId: "task-1" }], + }, + }, + ]); + + const nextState = applyAgUiEventsToSessionLiveState(stateWithTasks, [boundaryEvent]); + + expect(nextState.taskSnapshot).toBeNull(); + if (label === "agent ready") { + expect(nextState.infra.driverInstanceId).toBeNull(); + } + }); + test("permission resolution clears pending approvals and returns the run to running", () => { const waitingState: SessionLiveState = { ...baseState(), diff --git a/pkgs/contracts/src/session/session.contract.ts b/pkgs/contracts/src/session/session.contract.ts index e23a50f9..0d925552 100644 --- a/pkgs/contracts/src/session/session.contract.ts +++ b/pkgs/contracts/src/session/session.contract.ts @@ -1,3 +1,5 @@ +import { type } from "arktype"; + import type { AgentBuiltInToolConfig, AgentKind } from "../agent/agent.contract"; import type { FileUploadSummary } from "../file/file.contract"; import type { @@ -491,10 +493,110 @@ export interface AgentSessionActionCapability { status: AgentSessionActionCapabilityStatus; } +const AGENT_TASK_SNAPSHOT_MAX_TASKS = 256; +const AGENT_TASK_ID_MAX_UTF8_BYTES = 256; +const AGENT_TASK_TEXT_MAX_CODE_UNITS = 4096; +const AGENT_TASK_PAYLOAD_MAX_UTF8_BYTES = 1020 * 1024; + +function getUtf8ByteLength(value: string): number { + let bytes = 0; + + for (let index = 0; index < value.length; index += 1) { + const codePoint = value.codePointAt(index) ?? 0; + bytes += codePoint <= 0x7f ? 1 : codePoint <= 0x7ff ? 2 : codePoint <= 0xffff ? 3 : 4; + + if (codePoint > 0xffff) { + index += 1; + } + } + + return bytes; +} + +export const AgentTask = type({ + taskId: "string > 0", + "taskType?": "string > 0", + "title?": "string > 0", +}) + .onUndeclaredKey("reject") + .narrow((task, context) => { + if (getUtf8ByteLength(task.taskId) > AGENT_TASK_ID_MAX_UTF8_BYTES) { + return context.reject({ + actual: task.taskId, + expected: `a taskId of at most ${AGENT_TASK_ID_MAX_UTF8_BYTES} UTF-8 bytes`, + }); + } + + for (const value of [task.taskType, task.title]) { + if (value !== undefined && value.length > AGENT_TASK_TEXT_MAX_CODE_UNITS) { + return context.reject({ + actual: value, + expected: `task metadata of at most ${AGENT_TASK_TEXT_MAX_CODE_UNITS} UTF-16 code units`, + }); + } + } + + return true; + }); +export type AgentTask = typeof AgentTask.infer; + +const AgentTaskList = AgentTask.array().narrow((tasks, context) => { + if (tasks.length > AGENT_TASK_SNAPSHOT_MAX_TASKS) { + return context.reject({ + actual: String(tasks.length), + expected: `at most ${AGENT_TASK_SNAPSHOT_MAX_TASKS} tasks`, + }); + } + + if (new Set(tasks.map((task) => task.taskId)).size !== tasks.length) { + return context.reject({ + actual: tasks.map((task) => task.taskId).join(", "), + expected: "unique taskId values", + }); + } + + return true; +}); + +export const AgentTasksReplacedPayload = type({ + tasks: AgentTaskList, +}) + .onUndeclaredKey("reject") + .narrow((payload, context) => { + const byteLength = getUtf8ByteLength(JSON.stringify(payload)); + + return byteLength <= AGENT_TASK_PAYLOAD_MAX_UTF8_BYTES + ? true + : context.reject({ + actual: `${byteLength} UTF-8 bytes`, + expected: `at most ${AGENT_TASK_PAYLOAD_MAX_UTF8_BYTES} UTF-8 bytes`, + }); + }); +export type AgentTasksReplacedPayload = typeof AgentTasksReplacedPayload.infer; + +export const AgentTaskSnapshot = type({ + driverInstanceId: "string > 0", + runId: "string > 0", + tasks: AgentTaskList, +}) + .onUndeclaredKey("reject") + .narrow((snapshot, context) => { + const byteLength = getUtf8ByteLength(JSON.stringify({ tasks: snapshot.tasks })); + + return byteLength <= AGENT_TASK_PAYLOAD_MAX_UTF8_BYTES + ? true + : context.reject({ + actual: `${byteLength} UTF-8 bytes`, + expected: `at most ${AGENT_TASK_PAYLOAD_MAX_UTF8_BYTES} UTF-8 bytes of tasks`, + }); + }); +export type AgentTaskSnapshot = typeof AgentTaskSnapshot.infer; + export interface AgentSessionRetrieveResult { capabilities: AgentSessionActionCapability[]; recoverability: AgentSessionRecoverability; session: SessionSummary; + taskSnapshot: AgentTaskSnapshot | null; } export interface AgentSessionRetrieveConnection { diff --git a/pkgs/db/drizzle/0012_agent-task-snapshot-state.sql b/pkgs/db/drizzle/0012_agent-task-snapshot-state.sql new file mode 100644 index 00000000..11eaff7c --- /dev/null +++ b/pkgs/db/drizzle/0012_agent-task-snapshot-state.sql @@ -0,0 +1,9 @@ +CREATE TABLE `session_agent_task_snapshot` ( + `driver_instance_id` text CHECK ("driver_instance_id" = upper("driver_instance_id") AND length("driver_instance_id") = 26 AND substr("driver_instance_id", 1, 1) GLOB '[0-7]' AND "driver_instance_id" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*') NOT NULL, + `run_id` text CHECK ("run_id" = upper("run_id") AND length("run_id") = 26 AND substr("run_id", 1, 1) GLOB '[0-7]' AND "run_id" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*') NOT NULL, + `seq` integer NOT NULL, + `session_id` text CHECK ("session_id" = upper("session_id") AND length("session_id") = 26 AND substr("session_id", 1, 1) GLOB '[0-7]' AND "session_id" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*') PRIMARY KEY NOT NULL, + `tasks_json` text NOT NULL, + FOREIGN KEY (`run_id`) REFERENCES `session_run`(`id`) ON UPDATE no action ON DELETE cascade, + FOREIGN KEY (`session_id`) REFERENCES `session`(`id`) ON UPDATE no action ON DELETE cascade +); diff --git a/pkgs/db/drizzle/meta/0012_snapshot.json b/pkgs/db/drizzle/meta/0012_snapshot.json new file mode 100644 index 00000000..a37f9a85 --- /dev/null +++ b/pkgs/db/drizzle/meta/0012_snapshot.json @@ -0,0 +1,6596 @@ +{ + "version": "6", + "dialect": "sqlite", + "id": "57a9cb12-4c89-4d3e-a8f4-8b6fafe633a8", + "prevId": "c62b2b37-8e6b-4ae6-bd80-186a6e314043", + "tables": { + "agent_deployment_version": { + "name": "agent_deployment_version", + "columns": { + "agent_id": { + "name": "agent_id", + "type": "text CHECK (\"agent_id\" = upper(\"agent_id\") AND length(\"agent_id\") = 26 AND substr(\"agent_id\", 1, 1) GLOB '[0-7]' AND \"agent_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "config_json": { + "name": "config_json", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_by_account_id": { + "name": "created_by_account_id", + "type": "text CHECK (\"created_by_account_id\" = upper(\"created_by_account_id\") AND length(\"created_by_account_id\") = 26 AND substr(\"created_by_account_id\", 1, 1) GLOB '[0-7]' AND \"created_by_account_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "environment_id": { + "name": "environment_id", + "type": "text CHECK (\"environment_id\" = upper(\"environment_id\") AND length(\"environment_id\") = 26 AND substr(\"environment_id\", 1, 1) GLOB '[0-7]' AND \"environment_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "id": { + "name": "id", + "type": "text CHECK (\"id\" = upper(\"id\") AND length(\"id\") = 26 AND substr(\"id\", 1, 1) GLOB '[0-7]' AND \"id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "mcp_bindings_json": { + "name": "mcp_bindings_json", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "prompt": { + "name": "prompt", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "runtime_id": { + "name": "runtime_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "skills_json": { + "name": "skills_json", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "summary": { + "name": "summary", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "version_number": { + "name": "version_number", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "agent_deployment_version_agent_number_idx": { + "name": "agent_deployment_version_agent_number_idx", + "columns": ["agent_id", "version_number"], + "isUnique": true + }, + "agent_deployment_version_agent_created_idx": { + "name": "agent_deployment_version_agent_created_idx", + "columns": ["agent_id", "created_at"], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "agent_mcp_binding": { + "name": "agent_mcp_binding", + "columns": { + "agent_credential_id": { + "name": "agent_credential_id", + "type": "text CHECK (\"agent_credential_id\" = upper(\"agent_credential_id\") AND length(\"agent_credential_id\") = 26 AND substr(\"agent_credential_id\", 1, 1) GLOB '[0-7]' AND \"agent_credential_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "agent_id": { + "name": "agent_id", + "type": "text CHECK (\"agent_id\" = upper(\"agent_id\") AND length(\"agent_id\") = 26 AND substr(\"agent_id\", 1, 1) GLOB '[0-7]' AND \"agent_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "credential_mode": { + "name": "credential_mode", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'runtime_resolved'" + }, + "enabled": { + "name": "enabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "id": { + "name": "id", + "type": "text CHECK (\"id\" = upper(\"id\") AND length(\"id\") = 26 AND substr(\"id\", 1, 1) GLOB '[0-7]' AND \"id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "server_id": { + "name": "server_id", + "type": "text CHECK (\"server_id\" = upper(\"server_id\") AND length(\"server_id\") = 26 AND substr(\"server_id\", 1, 1) GLOB '[0-7]' AND \"server_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "agent_mcp_binding_agent_sort_idx": { + "name": "agent_mcp_binding_agent_sort_idx", + "columns": ["agent_id", "sort_order"], + "isUnique": true + }, + "agent_mcp_binding_server_idx": { + "name": "agent_mcp_binding_server_idx", + "columns": ["server_id"], + "isUnique": false + }, + "agent_mcp_binding_profile_server_idx": { + "name": "agent_mcp_binding_profile_server_idx", + "columns": ["agent_id", "server_id"], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": { + "agent_mcp_binding_agent_credential_shape_check": { + "name": "agent_mcp_binding_agent_credential_shape_check", + "value": "\n (\"agent_mcp_binding\".\"credential_mode\" = 'agent_bound' AND \"agent_mcp_binding\".\"agent_credential_id\" IS NOT NULL)\n OR (\"agent_mcp_binding\".\"credential_mode\" = 'runtime_resolved' AND \"agent_mcp_binding\".\"agent_credential_id\" IS NULL)\n " + } + } + }, + "agent_skill": { + "name": "agent_skill", + "columns": { + "agent_id": { + "name": "agent_id", + "type": "text CHECK (\"agent_id\" = upper(\"agent_id\") AND length(\"agent_id\") = 26 AND substr(\"agent_id\", 1, 1) GLOB '[0-7]' AND \"agent_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "skill_id": { + "name": "skill_id", + "type": "text CHECK (\"skill_id\" = upper(\"skill_id\") AND length(\"skill_id\") = 26 AND substr(\"skill_id\", 1, 1) GLOB '[0-7]' AND \"skill_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "agent_skill_agent_sort_idx": { + "name": "agent_skill_agent_sort_idx", + "columns": ["agent_id", "sort_order"], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "agent_skill_agent_id_skill_id_pk": { + "columns": ["agent_id", "skill_id"], + "name": "agent_skill_agent_id_skill_id_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "agent": { + "name": "agent", + "columns": { + "config_json": { + "name": "config_json", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "environment_id": { + "name": "environment_id", + "type": "text CHECK (\"environment_id\" = upper(\"environment_id\") AND length(\"environment_id\") = 26 AND substr(\"environment_id\", 1, 1) GLOB '[0-7]' AND \"environment_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "id": { + "name": "id", + "type": "text CHECK (\"id\" = upper(\"id\") AND length(\"id\") = 26 AND substr(\"id\", 1, 1) GLOB '[0-7]' AND \"id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'pet'" + }, + "live_deployment_version_id": { + "name": "live_deployment_version_id", + "type": "text CHECK (\"live_deployment_version_id\" = upper(\"live_deployment_version_id\") AND length(\"live_deployment_version_id\") = 26 AND substr(\"live_deployment_version_id\", 1, 1) GLOB '[0-7]' AND \"live_deployment_version_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "owner_account_id": { + "name": "owner_account_id", + "type": "text CHECK (\"owner_account_id\" = upper(\"owner_account_id\") AND length(\"owner_account_id\") = 26 AND substr(\"owner_account_id\", 1, 1) GLOB '[0-7]' AND \"owner_account_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "app_id": { + "name": "app_id", + "type": "text CHECK (\"app_id\" = upper(\"app_id\") AND length(\"app_id\") = 26 AND substr(\"app_id\", 1, 1) GLOB '[0-7]' AND \"app_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "prompt": { + "name": "prompt", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "runtime_id": { + "name": "runtime_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'draft'" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "visibility": { + "name": "visibility", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'private'" + } + }, + "indexes": { + "agent_app_owner_account_idx": { + "name": "agent_app_owner_account_idx", + "columns": ["app_id", "owner_account_id"], + "isUnique": false + }, + "agent_app_status_idx": { + "name": "agent_app_status_idx", + "columns": ["app_id", "status"], + "isUnique": false + }, + "agent_environment_idx": { + "name": "agent_environment_idx", + "columns": ["environment_id"], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": { + "agent_published_live_deployment_version_check": { + "name": "agent_published_live_deployment_version_check", + "value": "\"agent\".\"status\" <> 'published' OR \"agent\".\"live_deployment_version_id\" IS NOT NULL" + } + } + }, + "api_command": { + "name": "api_command", + "columns": { + "attempt_count": { + "name": "attempt_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "claim_expires_at": { + "name": "claim_expires_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "claim_owner": { + "name": "claim_owner", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "completed_at": { + "name": "completed_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "dedupe_key": { + "name": "dedupe_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "id": { + "name": "id", + "type": "text CHECK (\"id\" = upper(\"id\") AND length(\"id\") = 26 AND substr(\"id\", 1, 1) GLOB '[0-7]' AND \"id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "last_error_code": { + "name": "last_error_code", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_error_message": { + "name": "last_error_message", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "payload_json": { + "name": "payload_json", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "api_command_dedupe_idx": { + "name": "api_command_dedupe_idx", + "columns": ["dedupe_key"], + "isUnique": true + }, + "api_command_status_updated_idx": { + "name": "api_command_status_updated_idx", + "columns": ["status", "updated_at"], + "isUnique": false + }, + "api_command_claim_idx": { + "name": "api_command_claim_idx", + "columns": ["status", "claim_expires_at"], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "auth_account": { + "name": "auth_account", + "columns": { + "access_token": { + "name": "access_token", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "access_token_expires_at": { + "name": "access_token_expires_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "provider_account_id": { + "name": "provider_account_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "id_token": { + "name": "id_token", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "refresh_token": { + "name": "refresh_token", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "refresh_token_expires_at": { + "name": "refresh_token_expires_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "account_id": { + "name": "account_id", + "type": "text CHECK (\"account_id\" = upper(\"account_id\") AND length(\"account_id\") = 26 AND substr(\"account_id\", 1, 1) GLOB '[0-7]' AND \"account_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "auth_account_provider_account_idx": { + "name": "auth_account_provider_account_idx", + "columns": ["provider_id", "provider_account_id"], + "isUnique": true + }, + "auth_account_account_id_idx": { + "name": "auth_account_account_id_idx", + "columns": ["account_id"], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "auth_session": { + "name": "auth_session", + "columns": { + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "ip_address": { + "name": "ip_address", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "account_id": { + "name": "account_id", + "type": "text CHECK (\"account_id\" = upper(\"account_id\") AND length(\"account_id\") = 26 AND substr(\"account_id\", 1, 1) GLOB '[0-7]' AND \"account_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "auth_session_expires_at_idx": { + "name": "auth_session_expires_at_idx", + "columns": ["expires_at"], + "isUnique": false + }, + "auth_session_token_idx": { + "name": "auth_session_token_idx", + "columns": ["token"], + "isUnique": true + }, + "auth_session_account_id_idx": { + "name": "auth_session_account_id_idx", + "columns": ["account_id"], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "auth_verification": { + "name": "auth_verification", + "columns": { + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "identifier": { + "name": "identifier", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "auth_verification_expires_at_idx": { + "name": "auth_verification_expires_at_idx", + "columns": ["expires_at"], + "isUnique": false + }, + "auth_verification_identifier_idx": { + "name": "auth_verification_identifier_idx", + "columns": ["identifier"], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "cli_oauth_flow": { + "name": "cli_oauth_flow", + "columns": { + "account_id": { + "name": "account_id", + "type": "text CHECK (\"account_id\" = upper(\"account_id\") AND length(\"account_id\") = 26 AND substr(\"account_id\", 1, 1) GLOB '[0-7]' AND \"account_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "authorized_at": { + "name": "authorized_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "completed_at": { + "name": "completed_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "device_code_hash": { + "name": "device_code_hash", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "hostname": { + "name": "hostname", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "id": { + "name": "id", + "type": "text CHECK (\"id\" = upper(\"id\") AND length(\"id\") = 26 AND substr(\"id\", 1, 1) GLOB '[0-7]' AND \"id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_code": { + "name": "user_code", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "cli_oauth_flow_status_expires_idx": { + "name": "cli_oauth_flow_status_expires_idx", + "columns": ["status", "expires_at"], + "isUnique": false + }, + "cli_oauth_flow_device_code_hash_idx": { + "name": "cli_oauth_flow_device_code_hash_idx", + "columns": ["device_code_hash"], + "isUnique": true + }, + "cli_oauth_flow_user_code_idx": { + "name": "cli_oauth_flow_user_code_idx", + "columns": ["user_code"], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "personal_access_token": { + "name": "personal_access_token", + "columns": { + "account_id": { + "name": "account_id", + "type": "text CHECK (\"account_id\" = upper(\"account_id\") AND length(\"account_id\") = 26 AND substr(\"account_id\", 1, 1) GLOB '[0-7]' AND \"account_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "id": { + "name": "id", + "type": "text CHECK (\"id\" = upper(\"id\") AND length(\"id\") = 26 AND substr(\"id\", 1, 1) GLOB '[0-7]' AND \"id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "last_used_at": { + "name": "last_used_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "revoked_at": { + "name": "revoked_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "token_hash": { + "name": "token_hash", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "personal_access_token_account_created_idx": { + "name": "personal_access_token_account_created_idx", + "columns": ["account_id", "created_at"], + "isUnique": false + }, + "personal_access_token_hash_idx": { + "name": "personal_access_token_hash_idx", + "columns": ["token_hash"], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "email_log": { + "name": "email_log", + "columns": { + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "recipient_domain": { + "name": "recipient_domain", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "recipient_masked": { + "name": "recipient_masked", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "subject": { + "name": "subject", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "email_log_created_at_idx": { + "name": "email_log_created_at_idx", + "columns": ["created_at"], + "isUnique": false + }, + "email_log_type_status_idx": { + "name": "email_log_type_status_idx", + "columns": ["type", "status"], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "environment_revision": { + "name": "environment_revision", + "columns": { + "allow_mcp_servers": { + "name": "allow_mcp_servers", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "allow_package_managers": { + "name": "allow_package_managers", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "allowed_hosts_json": { + "name": "allowed_hosts_json", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_by_account_id": { + "name": "created_by_account_id", + "type": "text CHECK (\"created_by_account_id\" = upper(\"created_by_account_id\") AND length(\"created_by_account_id\") = 26 AND substr(\"created_by_account_id\", 1, 1) GLOB '[0-7]' AND \"created_by_account_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "env_vars_json": { + "name": "env_vars_json", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "environment_id": { + "name": "environment_id", + "type": "text CHECK (\"environment_id\" = upper(\"environment_id\") AND length(\"environment_id\") = 26 AND substr(\"environment_id\", 1, 1) GLOB '[0-7]' AND \"environment_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "id": { + "name": "id", + "type": "text CHECK (\"id\" = upper(\"id\") AND length(\"id\") = 26 AND substr(\"id\", 1, 1) GLOB '[0-7]' AND \"id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "network_policy": { + "name": "network_policy", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "packages_json": { + "name": "packages_json", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "app_id": { + "name": "app_id", + "type": "text CHECK (\"app_id\" = upper(\"app_id\") AND length(\"app_id\") = 26 AND substr(\"app_id\", 1, 1) GLOB '[0-7]' AND \"app_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "setup_script": { + "name": "setup_script", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "environment_revision_environment_created_at_idx": { + "name": "environment_revision_environment_created_at_idx", + "columns": ["environment_id", "created_at"], + "isUnique": false + }, + "environment_revision_app_created_at_idx": { + "name": "environment_revision_app_created_at_idx", + "columns": ["app_id", "created_at"], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": { + "environment_revision_network_policy_check": { + "name": "environment_revision_network_policy_check", + "value": "\"environment_revision\".\"network_policy\" IN ('full', 'limited')" + } + } + }, + "environment": { + "name": "environment", + "columns": { + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "current_revision_id": { + "name": "current_revision_id", + "type": "text CHECK (\"current_revision_id\" = upper(\"current_revision_id\") AND length(\"current_revision_id\") = 26 AND substr(\"current_revision_id\", 1, 1) GLOB '[0-7]' AND \"current_revision_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "forked_from_environment_id": { + "name": "forked_from_environment_id", + "type": "text CHECK (\"forked_from_environment_id\" = upper(\"forked_from_environment_id\") AND length(\"forked_from_environment_id\") = 26 AND substr(\"forked_from_environment_id\", 1, 1) GLOB '[0-7]' AND \"forked_from_environment_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "forked_from_environment_name": { + "name": "forked_from_environment_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "forked_from_owner_name": { + "name": "forked_from_owner_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "id": { + "name": "id", + "type": "text CHECK (\"id\" = upper(\"id\") AND length(\"id\") = 26 AND substr(\"id\", 1, 1) GLOB '[0-7]' AND \"id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "owner_account_id": { + "name": "owner_account_id", + "type": "text CHECK (\"owner_account_id\" = upper(\"owner_account_id\") AND length(\"owner_account_id\") = 26 AND substr(\"owner_account_id\", 1, 1) GLOB '[0-7]' AND \"owner_account_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "app_id": { + "name": "app_id", + "type": "text CHECK (\"app_id\" = upper(\"app_id\") AND length(\"app_id\") = 26 AND substr(\"app_id\", 1, 1) GLOB '[0-7]' AND \"app_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "environment_app_updated_at_idx": { + "name": "environment_app_updated_at_idx", + "columns": ["app_id", "updated_at"], + "isUnique": false + }, + "environment_owner_updated_at_idx": { + "name": "environment_owner_updated_at_idx", + "columns": ["owner_account_id", "updated_at"], + "isUnique": false + }, + "environment_owner_name_idx": { + "name": "environment_owner_name_idx", + "columns": ["app_id", "owner_account_id", "name"], + "isUnique": true, + "where": "\"environment\".\"owner_account_id\" IS NOT NULL" + }, + "environment_system_default_idx": { + "name": "environment_system_default_idx", + "columns": ["app_id"], + "isUnique": true, + "where": "\"environment\".\"owner_account_id\" IS NULL" + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "file_record": { + "name": "file_record", + "columns": { + "committed": { + "name": "committed", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_by_account_id": { + "name": "created_by_account_id", + "type": "text CHECK (\"created_by_account_id\" = upper(\"created_by_account_id\") AND length(\"created_by_account_id\") = 26 AND substr(\"created_by_account_id\", 1, 1) GLOB '[0-7]' AND \"created_by_account_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "etag": { + "name": "etag", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "id": { + "name": "id", + "type": "text CHECK (\"id\" = upper(\"id\") AND length(\"id\") = 26 AND substr(\"id\", 1, 1) GLOB '[0-7]' AND \"id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "mime_type": { + "name": "mime_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "object_key": { + "name": "object_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "owner_id": { + "name": "owner_id", + "type": "text CHECK (\"owner_id\" = upper(\"owner_id\") AND length(\"owner_id\") = 26 AND substr(\"owner_id\", 1, 1) GLOB '[0-7]' AND \"owner_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "owner_kind": { + "name": "owner_kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "parent_path": { + "name": "parent_path", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "path": { + "name": "path", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "purpose": { + "name": "purpose", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "scope_id": { + "name": "scope_id", + "type": "text CHECK (\"scope_id\" = upper(\"scope_id\") AND length(\"scope_id\") = 26 AND substr(\"scope_id\", 1, 1) GLOB '[0-7]' AND \"scope_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "scope_kind": { + "name": "scope_kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "session_kind": { + "name": "session_kind", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "size": { + "name": "size", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "version": { + "name": "version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "file_record_object_key_idx": { + "name": "file_record_object_key_idx", + "columns": ["object_key"], + "isUnique": true + }, + "file_record_unscoped_parent_path_name_status_idx": { + "name": "file_record_unscoped_parent_path_name_status_idx", + "columns": ["scope_kind", "parent_path", "name", "status"], + "isUnique": true, + "where": "\"file_record\".\"scope_id\" IS NULL" + }, + "file_record_scoped_parent_path_name_status_idx": { + "name": "file_record_scoped_parent_path_name_status_idx", + "columns": ["scope_kind", "scope_id", "parent_path", "name", "status"], + "isUnique": true + }, + "file_record_unscoped_pending_path_idx": { + "name": "file_record_unscoped_pending_path_idx", + "columns": ["scope_kind", "path"], + "isUnique": true, + "where": "\"file_record\".\"status\" = 'pending' AND \"file_record\".\"scope_id\" IS NULL" + }, + "file_record_scoped_pending_path_idx": { + "name": "file_record_scoped_pending_path_idx", + "columns": ["scope_kind", "scope_id", "path"], + "isUnique": true, + "where": "\"file_record\".\"status\" = 'pending' AND \"file_record\".\"scope_id\" IS NOT NULL" + }, + "file_record_unscoped_ready_path_idx": { + "name": "file_record_unscoped_ready_path_idx", + "columns": ["scope_kind", "path"], + "isUnique": true, + "where": "\"file_record\".\"status\" = 'ready' AND \"file_record\".\"scope_id\" IS NULL" + }, + "file_record_scoped_ready_path_idx": { + "name": "file_record_scoped_ready_path_idx", + "columns": ["scope_kind", "scope_id", "path"], + "isUnique": true, + "where": "\"file_record\".\"status\" = 'ready' AND \"file_record\".\"scope_id\" IS NOT NULL" + }, + "file_record_governance_idx": { + "name": "file_record_governance_idx", + "columns": ["purpose", "owner_kind", "owner_id", "status", "expires_at"], + "isUnique": false + }, + "file_record_listing_idx": { + "name": "file_record_listing_idx", + "columns": ["scope_kind", "scope_id", "parent_path", "status", "lower(\"name\")"], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "file_upload": { + "name": "file_upload", + "columns": { + "content_type": { + "name": "content_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_by_account_id": { + "name": "created_by_account_id", + "type": "text CHECK (\"created_by_account_id\" = upper(\"created_by_account_id\") AND length(\"created_by_account_id\") = 26 AND substr(\"created_by_account_id\", 1, 1) GLOB '[0-7]' AND \"created_by_account_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expected_size": { + "name": "expected_size", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "file_id": { + "name": "file_id", + "type": "text CHECK (\"file_id\" = upper(\"file_id\") AND length(\"file_id\") = 26 AND substr(\"file_id\", 1, 1) GLOB '[0-7]' AND \"file_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "id": { + "name": "id", + "type": "text CHECK (\"id\" = upper(\"id\") AND length(\"id\") = 26 AND substr(\"id\", 1, 1) GLOB '[0-7]' AND \"id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "if_match_etag": { + "name": "if_match_etag", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "multipart_upload_id": { + "name": "multipart_upload_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "overwrite": { + "name": "overwrite", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "part_size": { + "name": "part_size", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "scope_id": { + "name": "scope_id", + "type": "text CHECK (\"scope_id\" = upper(\"scope_id\") AND length(\"scope_id\") = 26 AND substr(\"scope_id\", 1, 1) GLOB '[0-7]' AND \"scope_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "scope_kind": { + "name": "scope_kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "strategy": { + "name": "strategy", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "file_upload_file_id_idx": { + "name": "file_upload_file_id_idx", + "columns": ["file_id"], + "isUnique": true + }, + "file_upload_status_expires_idx": { + "name": "file_upload_status_expires_idx", + "columns": ["status", "expires_at"], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "file_version": { + "name": "file_version", + "columns": { + "committed": { + "name": "committed", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "committed_at": { + "name": "committed_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_by_account_id": { + "name": "created_by_account_id", + "type": "text CHECK (\"created_by_account_id\" = upper(\"created_by_account_id\") AND length(\"created_by_account_id\") = 26 AND substr(\"created_by_account_id\", 1, 1) GLOB '[0-7]' AND \"created_by_account_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "file_id": { + "name": "file_id", + "type": "text CHECK (\"file_id\" = upper(\"file_id\") AND length(\"file_id\") = 26 AND substr(\"file_id\", 1, 1) GLOB '[0-7]' AND \"file_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "id": { + "name": "id", + "type": "text CHECK (\"id\" = upper(\"id\") AND length(\"id\") = 26 AND substr(\"id\", 1, 1) GLOB '[0-7]' AND \"id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "mime_type": { + "name": "mime_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "object_key": { + "name": "object_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "path": { + "name": "path", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "reason": { + "name": "reason", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "scope_id": { + "name": "scope_id", + "type": "text CHECK (\"scope_id\" = upper(\"scope_id\") AND length(\"scope_id\") = 26 AND substr(\"scope_id\", 1, 1) GLOB '[0-7]' AND \"scope_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "scope_kind": { + "name": "scope_kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "size": { + "name": "size", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "source_etag": { + "name": "source_etag", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "source_object_key": { + "name": "source_object_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "version": { + "name": "version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "file_version_object_key_idx": { + "name": "file_version_object_key_idx", + "columns": ["object_key"], + "isUnique": true + }, + "file_version_scope_path_created_idx": { + "name": "file_version_scope_path_created_idx", + "columns": ["scope_kind", "scope_id", "path", "created_at"], + "isUnique": false + }, + "file_version_file_created_idx": { + "name": "file_version_file_created_idx", + "columns": ["file_id", "created_at"], + "isUnique": false + }, + "file_version_pending_idx": { + "name": "file_version_pending_idx", + "columns": ["committed", "created_at"], + "isUnique": false, + "where": "\"file_version\".\"committed\" = 0" + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "mcp_credential": { + "name": "mcp_credential", + "columns": { + "account_id": { + "name": "account_id", + "type": "text CHECK (\"account_id\" = upper(\"account_id\") AND length(\"account_id\") = 26 AND substr(\"account_id\", 1, 1) GLOB '[0-7]' AND \"account_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "agent_id": { + "name": "agent_id", + "type": "text CHECK (\"agent_id\" = upper(\"agent_id\") AND length(\"agent_id\") = 26 AND substr(\"agent_id\", 1, 1) GLOB '[0-7]' AND \"agent_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "auth_type": { + "name": "auth_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "id": { + "name": "id", + "type": "text CHECK (\"id\" = upper(\"id\") AND length(\"id\") = 26 AND substr(\"id\", 1, 1) GLOB '[0-7]' AND \"id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "last_refreshed_at": { + "name": "last_refreshed_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "oauth_client_id": { + "name": "oauth_client_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "oauth_client_secret_secret_id": { + "name": "oauth_client_secret_secret_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "app_id": { + "name": "app_id", + "type": "text CHECK (\"app_id\" = upper(\"app_id\") AND length(\"app_id\") = 26 AND substr(\"app_id\", 1, 1) GLOB '[0-7]' AND \"app_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "refresh_secret_id": { + "name": "refresh_secret_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "scope_values_json": { + "name": "scope_values_json", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "secret_id": { + "name": "secret_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "server_id": { + "name": "server_id", + "type": "text CHECK (\"server_id\" = upper(\"server_id\") AND length(\"server_id\") = 26 AND substr(\"server_id\", 1, 1) GLOB '[0-7]' AND \"server_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "subject_label": { + "name": "subject_label", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "mcp_credential_server_scope_status_idx": { + "name": "mcp_credential_server_scope_status_idx", + "columns": ["server_id", "scope", "status"], + "isUnique": false + }, + "mcp_credential_app_scope_status_idx": { + "name": "mcp_credential_app_scope_status_idx", + "columns": ["app_id", "scope", "status"], + "isUnique": false + }, + "mcp_credential_app_scope_idx": { + "name": "mcp_credential_app_scope_idx", + "columns": ["server_id", "scope"], + "isUnique": true, + "where": "\"mcp_credential\".\"scope\" = 'app'" + }, + "mcp_credential_agent_scope_idx": { + "name": "mcp_credential_agent_scope_idx", + "columns": ["server_id", "agent_id", "scope"], + "isUnique": true, + "where": "\"mcp_credential\".\"scope\" = 'agent' AND \"mcp_credential\".\"agent_id\" IS NOT NULL" + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": { + "mcp_credential_scope_shape_check": { + "name": "mcp_credential_scope_shape_check", + "value": "\n (\"mcp_credential\".\"scope\" = 'app' AND \"mcp_credential\".\"account_id\" IS NULL AND \"mcp_credential\".\"agent_id\" IS NULL)\n OR (\"mcp_credential\".\"scope\" = 'agent' AND \"mcp_credential\".\"account_id\" IS NULL AND \"mcp_credential\".\"agent_id\" IS NOT NULL)\n " + }, + "mcp_credential_scope_values_json_check": { + "name": "mcp_credential_scope_values_json_check", + "value": "\n \"mcp_credential\".\"scope_values_json\" IS NULL\n OR (json_valid(\"mcp_credential\".\"scope_values_json\") AND json_type(\"mcp_credential\".\"scope_values_json\") = 'array')\n " + }, + "mcp_credential_bearer_shape_check": { + "name": "mcp_credential_bearer_shape_check", + "value": "\n \"mcp_credential\".\"auth_type\" != 'bearer'\n OR (\n \"mcp_credential\".\"oauth_client_id\" IS NULL\n AND \"mcp_credential\".\"oauth_client_secret_secret_id\" IS NULL\n AND \"mcp_credential\".\"refresh_secret_id\" IS NULL\n )\n " + } + } + }, + "mcp_oauth_flow": { + "name": "mcp_oauth_flow", + "columns": { + "authorization_endpoint": { + "name": "authorization_endpoint", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "cleanup_after": { + "name": "cleanup_after", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "code_verifier": { + "name": "code_verifier", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "completed_at": { + "name": "completed_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "id": { + "name": "id", + "type": "text CHECK (\"id\" = upper(\"id\") AND length(\"id\") = 26 AND substr(\"id\", 1, 1) GLOB '[0-7]' AND \"id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "initiator_account_id": { + "name": "initiator_account_id", + "type": "text CHECK (\"initiator_account_id\" = upper(\"initiator_account_id\") AND length(\"initiator_account_id\") = 26 AND substr(\"initiator_account_id\", 1, 1) GLOB '[0-7]' AND \"initiator_account_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "oauth_client_id": { + "name": "oauth_client_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "oauth_client_secret_secret_id": { + "name": "oauth_client_secret_secret_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "app_id": { + "name": "app_id", + "type": "text CHECK (\"app_id\" = upper(\"app_id\") AND length(\"app_id\") = 26 AND substr(\"app_id\", 1, 1) GLOB '[0-7]' AND \"app_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "registration_endpoint": { + "name": "registration_endpoint", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "return_url": { + "name": "return_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "scope_values_json": { + "name": "scope_values_json", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "server_id": { + "name": "server_id", + "type": "text CHECK (\"server_id\" = upper(\"server_id\") AND length(\"server_id\") = 26 AND substr(\"server_id\", 1, 1) GLOB '[0-7]' AND \"server_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "subject_label": { + "name": "subject_label", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "token_endpoint": { + "name": "token_endpoint", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "mcp_oauth_flow_status_cleanup_after_idx": { + "name": "mcp_oauth_flow_status_cleanup_after_idx", + "columns": ["status", "cleanup_after"], + "isUnique": false + }, + "mcp_oauth_flow_expires_at_idx": { + "name": "mcp_oauth_flow_expires_at_idx", + "columns": ["expires_at"], + "isUnique": false + }, + "mcp_oauth_flow_server_account_idx": { + "name": "mcp_oauth_flow_server_account_idx", + "columns": ["server_id", "initiator_account_id"], + "isUnique": false + }, + "mcp_oauth_flow_app_server_account_idx": { + "name": "mcp_oauth_flow_app_server_account_idx", + "columns": ["app_id", "server_id", "initiator_account_id"], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": { + "mcp_oauth_flow_scope_values_json_check": { + "name": "mcp_oauth_flow_scope_values_json_check", + "value": "\n \"mcp_oauth_flow\".\"scope_values_json\" IS NULL\n OR (json_valid(\"mcp_oauth_flow\".\"scope_values_json\") AND json_type(\"mcp_oauth_flow\".\"scope_values_json\") = 'array')\n " + } + } + }, + "mcp_server": { + "name": "mcp_server", + "columns": { + "auth_type": { + "name": "auth_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "byo_client_id": { + "name": "byo_client_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "byo_client_secret_secret_id": { + "name": "byo_client_secret_secret_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "credential_scope": { + "name": "credential_scope", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "enabled": { + "name": "enabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "icon_url": { + "name": "icon_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "id": { + "name": "id", + "type": "text CHECK (\"id\" = upper(\"id\") AND length(\"id\") = 26 AND substr(\"id\", 1, 1) GLOB '[0-7]' AND \"id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "oauth_metadata_json": { + "name": "oauth_metadata_json", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "owner_account_id": { + "name": "owner_account_id", + "type": "text CHECK (\"owner_account_id\" = upper(\"owner_account_id\") AND length(\"owner_account_id\") = 26 AND substr(\"owner_account_id\", 1, 1) GLOB '[0-7]' AND \"owner_account_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "app_id": { + "name": "app_id", + "type": "text CHECK (\"app_id\" = upper(\"app_id\") AND length(\"app_id\") = 26 AND substr(\"app_id\", 1, 1) GLOB '[0-7]' AND \"app_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "mcp_server_app_enabled_idx": { + "name": "mcp_server_app_enabled_idx", + "columns": ["app_id", "enabled"], + "isUnique": false + }, + "mcp_server_owner_app_idx": { + "name": "mcp_server_owner_app_idx", + "columns": ["owner_account_id", "app_id"], + "isUnique": false + }, + "mcp_server_app_url_idx": { + "name": "mcp_server_app_url_idx", + "columns": ["app_id", "url"], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": { + "mcp_server_source_scope_check": { + "name": "mcp_server_source_scope_check", + "value": "\"mcp_server\".\"source\" = 'app' AND \"mcp_server\".\"credential_scope\" = 'app'" + } + } + }, + "vault_secret": { + "name": "vault_secret", + "columns": { + "algorithm": { + "name": "algorithm", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'AES-GCM'" + }, + "ciphertext": { + "name": "ciphertext", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "ciphertext_iv": { + "name": "ciphertext_iv", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "id": { + "name": "id", + "type": "text CHECK (\"id\" = upper(\"id\") AND length(\"id\") = 26 AND substr(\"id\", 1, 1) GLOB '[0-7]' AND \"id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "wrapped_dek": { + "name": "wrapped_dek", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "wrapped_dek_iv": { + "name": "wrapped_dek_iv", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "vault_secret_kind_created_at_idx": { + "name": "vault_secret_kind_created_at_idx", + "columns": ["kind", "created_at"], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "organization": { + "name": "organization", + "columns": { + "avatar_url": { + "name": "avatar_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "creator_account_id": { + "name": "creator_account_id", + "type": "text CHECK (\"creator_account_id\" = upper(\"creator_account_id\") AND length(\"creator_account_id\") = 26 AND substr(\"creator_account_id\", 1, 1) GLOB '[0-7]' AND \"creator_account_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "id": { + "name": "id", + "type": "text CHECK (\"id\" = upper(\"id\") AND length(\"id\") = 26 AND substr(\"id\", 1, 1) GLOB '[0-7]' AND \"id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "organization_creator_account_idx": { + "name": "organization_creator_account_idx", + "columns": ["creator_account_id"], + "isUnique": true, + "where": "\"organization\".\"creator_account_id\" IS NOT NULL" + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "app_deployment_run": { + "name": "app_deployment_run", + "columns": { + "app_id": { + "name": "app_id", + "type": "text CHECK (\"app_id\" = upper(\"app_id\") AND length(\"app_id\") = 26 AND substr(\"app_id\", 1, 1) GLOB '[0-7]' AND \"app_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "deployment_id": { + "name": "deployment_id", + "type": "text CHECK (\"deployment_id\" = upper(\"deployment_id\") AND length(\"deployment_id\") = 26 AND substr(\"deployment_id\", 1, 1) GLOB '[0-7]' AND \"deployment_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "error_code": { + "name": "error_code", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "external_deployment_id": { + "name": "external_deployment_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "external_project_id": { + "name": "external_project_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "external_version_id": { + "name": "external_version_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "generated_wrangler_config_json": { + "name": "generated_wrangler_config_json", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "id": { + "name": "id", + "type": "text CHECK (\"id\" = upper(\"id\") AND length(\"id\") = 26 AND substr(\"id\", 1, 1) GLOB '[0-7]' AND \"id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "mosoo_config_json": { + "name": "mosoo_config_json", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "plan_json": { + "name": "plan_json", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "source_branch": { + "name": "source_branch", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "source_commit_sha": { + "name": "source_commit_sha", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "target_kind": { + "name": "target_kind", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "target_project_name": { + "name": "target_project_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "target_script_name": { + "name": "target_script_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "app_deployment_run_app_id_idx": { + "name": "app_deployment_run_app_id_idx", + "columns": ["app_id", "id"], + "isUnique": false + }, + "app_deployment_run_deployment_id_idx": { + "name": "app_deployment_run_deployment_id_idx", + "columns": ["deployment_id", "id"], + "isUnique": false + }, + "app_deployment_run_active_app_idx": { + "name": "app_deployment_run_active_app_idx", + "columns": ["app_id"], + "isUnique": true, + "where": "\"app_deployment_run\".\"status\" IN ('queued', 'preparing', 'building', 'submitting', 'submitted', 'activating')" + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": { + "app_deployment_run_status_check": { + "name": "app_deployment_run_status_check", + "value": "\"app_deployment_run\".\"status\" IN ('queued', 'preparing', 'building', 'submitting', 'submitted', 'activating', 'success', 'failed')" + }, + "app_deployment_run_target_kind_check": { + "name": "app_deployment_run_target_kind_check", + "value": "\"app_deployment_run\".\"target_kind\" IS NULL OR \"app_deployment_run\".\"target_kind\" IN ('cloudflare_pages', 'cloudflare_worker')" + } + } + }, + "app_deployment_secret": { + "name": "app_deployment_secret", + "columns": { + "app_id": { + "name": "app_id", + "type": "text CHECK (\"app_id\" = upper(\"app_id\") AND length(\"app_id\") = 26 AND substr(\"app_id\", 1, 1) GLOB '[0-7]' AND \"app_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "vault_secret_id": { + "name": "vault_secret_id", + "type": "text CHECK (\"vault_secret_id\" = upper(\"vault_secret_id\") AND length(\"vault_secret_id\") = 26 AND substr(\"vault_secret_id\", 1, 1) GLOB '[0-7]' AND \"vault_secret_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "app_deployment_secret_app_name_idx": { + "name": "app_deployment_secret_app_name_idx", + "columns": ["app_id", "name"], + "isUnique": true + }, + "app_deployment_secret_vault_secret_idx": { + "name": "app_deployment_secret_vault_secret_idx", + "columns": ["vault_secret_id"], + "isUnique": true + } + }, + "foreignKeys": { + "app_deployment_secret_vault_secret_id_vault_secret_id_fk": { + "name": "app_deployment_secret_vault_secret_id_vault_secret_id_fk", + "tableFrom": "app_deployment_secret", + "tableTo": "vault_secret", + "columnsFrom": ["vault_secret_id"], + "columnsTo": ["id"], + "onDelete": "restrict", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "app_deployment": { + "name": "app_deployment", + "columns": { + "app_id": { + "name": "app_id", + "type": "text CHECK (\"app_id\" = upper(\"app_id\") AND length(\"app_id\") = 26 AND substr(\"app_id\", 1, 1) GLOB '[0-7]' AND \"app_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "default_branch": { + "name": "default_branch", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "deleted_at": { + "name": "deleted_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "id": { + "name": "id", + "type": "text CHECK (\"id\" = upper(\"id\") AND length(\"id\") = 26 AND substr(\"id\", 1, 1) GLOB '[0-7]' AND \"id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "last_successful_url": { + "name": "last_successful_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "latest_run_id": { + "name": "latest_run_id", + "type": "text CHECK (\"latest_run_id\" = upper(\"latest_run_id\") AND length(\"latest_run_id\") = 26 AND substr(\"latest_run_id\", 1, 1) GLOB '[0-7]' AND \"latest_run_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "mosoo_subdomain": { + "name": "mosoo_subdomain", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "owner_account_id": { + "name": "owner_account_id", + "type": "text CHECK (\"owner_account_id\" = upper(\"owner_account_id\") AND length(\"owner_account_id\") = 26 AND substr(\"owner_account_id\", 1, 1) GLOB '[0-7]' AND \"owner_account_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "repo_name": { + "name": "repo_name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "repo_owner": { + "name": "repo_owner", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "repo_url": { + "name": "repo_url", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "source_kind": { + "name": "source_kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "app_deployment_active_app_idx": { + "name": "app_deployment_active_app_idx", + "columns": ["app_id"], + "isUnique": true, + "where": "\"app_deployment\".\"deleted_at\" IS NULL" + }, + "app_deployment_active_subdomain_idx": { + "name": "app_deployment_active_subdomain_idx", + "columns": ["mosoo_subdomain"], + "isUnique": true, + "where": "\"app_deployment\".\"deleted_at\" IS NULL" + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": { + "app_deployment_source_kind_check": { + "name": "app_deployment_source_kind_check", + "value": "\"app_deployment\".\"source_kind\" IN ('github_public')" + } + } + }, + "app": { + "name": "app", + "columns": { + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "default_environment_id": { + "name": "default_environment_id", + "type": "text CHECK (\"default_environment_id\" = upper(\"default_environment_id\") AND length(\"default_environment_id\") = 26 AND substr(\"default_environment_id\", 1, 1) GLOB '[0-7]' AND \"default_environment_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "id": { + "name": "id", + "type": "text CHECK (\"id\" = upper(\"id\") AND length(\"id\") = 26 AND substr(\"id\", 1, 1) GLOB '[0-7]' AND \"id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "organization_id": { + "name": "organization_id", + "type": "text CHECK (\"organization_id\" = upper(\"organization_id\") AND length(\"organization_id\") = 26 AND substr(\"organization_id\", 1, 1) GLOB '[0-7]' AND \"organization_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "owner_account_id": { + "name": "owner_account_id", + "type": "text CHECK (\"owner_account_id\" = upper(\"owner_account_id\") AND length(\"owner_account_id\") = 26 AND substr(\"owner_account_id\", 1, 1) GLOB '[0-7]' AND \"owner_account_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "bound_agent_call_idempotency_key": { + "name": "bound_agent_call_idempotency_key", + "columns": { + "body_hash": { + "name": "body_hash", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "id": { + "name": "id", + "type": "text CHECK (\"id\" = upper(\"id\") AND length(\"id\") = 26 AND substr(\"id\", 1, 1) GLOB '[0-7]' AND \"id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "idempotency_key": { + "name": "idempotency_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "run_id": { + "name": "run_id", + "type": "text CHECK (\"run_id\" = upper(\"run_id\") AND length(\"run_id\") = 26 AND substr(\"run_id\", 1, 1) GLOB '[0-7]' AND \"run_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "session_id": { + "name": "session_id", + "type": "text CHECK (\"session_id\" = upper(\"session_id\") AND length(\"session_id\") = 26 AND substr(\"session_id\", 1, 1) GLOB '[0-7]' AND \"session_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "subject_hash": { + "name": "subject_hash", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "bound_agent_call_idempotency_subject_key_idx": { + "name": "bound_agent_call_idempotency_subject_key_idx", + "columns": ["subject_hash", "idempotency_key"], + "isUnique": true + }, + "bound_agent_call_idempotency_updated_idx": { + "name": "bound_agent_call_idempotency_updated_idx", + "columns": ["updated_at"], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "public_api_idempotency_key": { + "name": "public_api_idempotency_key", + "columns": { + "body_hash": { + "name": "body_hash", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "id": { + "name": "id", + "type": "text CHECK (\"id\" = upper(\"id\") AND length(\"id\") = 26 AND substr(\"id\", 1, 1) GLOB '[0-7]' AND \"id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "idempotency_key": { + "name": "idempotency_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "method": { + "name": "method", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "response_json": { + "name": "response_json", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "response_status": { + "name": "response_status", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "route": { + "name": "route", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "token_id": { + "name": "token_id", + "type": "text CHECK (\"token_id\" = upper(\"token_id\") AND length(\"token_id\") = 26 AND substr(\"token_id\", 1, 1) GLOB '[0-7]' AND \"token_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "public_api_idempotency_token_key_idx": { + "name": "public_api_idempotency_token_key_idx", + "columns": ["token_id", "idempotency_key"], + "isUnique": true + }, + "public_api_idempotency_updated_idx": { + "name": "public_api_idempotency_updated_idx", + "columns": ["updated_at"], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "public_api_rate_limit_window": { + "name": "public_api_rate_limit_window", + "columns": { + "bucket_key": { + "name": "bucket_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "request_count": { + "name": "request_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "shard": { + "name": "shard", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "window_start": { + "name": "window_start", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "public_api_rate_limit_window_updated_idx": { + "name": "public_api_rate_limit_window_updated_idx", + "columns": ["updated_at"], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "public_api_rate_limit_window_bucket_key_window_start_shard_pk": { + "columns": ["bucket_key", "window_start", "shard"], + "name": "public_api_rate_limit_window_bucket_key_window_start_shard_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "driver_command": { + "name": "driver_command", + "columns": { + "acked_at": { + "name": "acked_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "completed_at": { + "name": "completed_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "delivery_connection_id": { + "name": "delivery_connection_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "driver_instance_id": { + "name": "driver_instance_id", + "type": "text CHECK (\"driver_instance_id\" = upper(\"driver_instance_id\") AND length(\"driver_instance_id\") = 26 AND substr(\"driver_instance_id\", 1, 1) GLOB '[0-7]' AND \"driver_instance_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "error_json": { + "name": "error_json", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "id": { + "name": "id", + "type": "text CHECK (\"id\" = upper(\"id\") AND length(\"id\") = 26 AND substr(\"id\", 1, 1) GLOB '[0-7]' AND \"id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "issued_at": { + "name": "issued_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "payload_json": { + "name": "payload_json", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "result_json": { + "name": "result_json", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "seq": { + "name": "seq", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "driver_command_instance_seq_idx": { + "name": "driver_command_instance_seq_idx", + "columns": ["driver_instance_id", "seq"], + "isUnique": true + }, + "driver_command_instance_status_idx": { + "name": "driver_command_instance_status_idx", + "columns": ["driver_instance_id", "status", "expires_at"], + "isUnique": false + } + }, + "foreignKeys": { + "driver_command_driver_instance_id_driver_instance_id_fk": { + "name": "driver_command_driver_instance_id_driver_instance_id_fk", + "tableFrom": "driver_command", + "tableTo": "driver_instance", + "columnsFrom": ["driver_instance_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "driver_instance_mcp_grant": { + "name": "driver_instance_mcp_grant", + "columns": { + "auth_type": { + "name": "auth_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "authorization_state": { + "name": "authorization_state", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "can_invalidate": { + "name": "can_invalidate", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "can_refresh": { + "name": "can_refresh", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "credential_id": { + "name": "credential_id", + "type": "text CHECK (\"credential_id\" = upper(\"credential_id\") AND length(\"credential_id\") = 26 AND substr(\"credential_id\", 1, 1) GLOB '[0-7]' AND \"credential_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "driver_instance_id": { + "name": "driver_instance_id", + "type": "text CHECK (\"driver_instance_id\" = upper(\"driver_instance_id\") AND length(\"driver_instance_id\") = 26 AND substr(\"driver_instance_id\", 1, 1) GLOB '[0-7]' AND \"driver_instance_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "app_id": { + "name": "app_id", + "type": "text CHECK (\"app_id\" = upper(\"app_id\") AND length(\"app_id\") = 26 AND substr(\"app_id\", 1, 1) GLOB '[0-7]' AND \"app_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "server_id": { + "name": "server_id", + "type": "text CHECK (\"server_id\" = upper(\"server_id\") AND length(\"server_id\") = 26 AND substr(\"server_id\", 1, 1) GLOB '[0-7]' AND \"server_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "driver_instance_mcp_grant_instance_server_idx": { + "name": "driver_instance_mcp_grant_instance_server_idx", + "columns": ["driver_instance_id", "server_id"], + "isUnique": true + }, + "driver_instance_mcp_grant_instance_credential_idx": { + "name": "driver_instance_mcp_grant_instance_credential_idx", + "columns": ["driver_instance_id", "credential_id"], + "isUnique": false + } + }, + "foreignKeys": { + "driver_instance_mcp_grant_driver_instance_id_driver_instance_id_fk": { + "name": "driver_instance_mcp_grant_driver_instance_id_driver_instance_id_fk", + "tableFrom": "driver_instance_mcp_grant", + "tableTo": "driver_instance", + "columnsFrom": ["driver_instance_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "driver_instance": { + "name": "driver_instance", + "columns": { + "boot_token_expires_at": { + "name": "boot_token_expires_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "boot_token_hash": { + "name": "boot_token_hash", + "type": "blob", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "boot_token_used_at": { + "name": "boot_token_used_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "close_code": { + "name": "close_code", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "close_reason": { + "name": "close_reason", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "connection_id": { + "name": "connection_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "command_seq_cursor": { + "name": "command_seq_cursor", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "driver_pid": { + "name": "driver_pid", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "driver_started_at": { + "name": "driver_started_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "driver_version": { + "name": "driver_version", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "heartbeat_count": { + "name": "heartbeat_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "generation": { + "name": "generation", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "id": { + "name": "id", + "type": "text CHECK (\"id\" = upper(\"id\") AND length(\"id\") = 26 AND substr(\"id\", 1, 1) GLOB '[0-7]' AND \"id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "last_heartbeat_at": { + "name": "last_heartbeat_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "process_id": { + "name": "process_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "protocol": { + "name": "protocol", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "protocol_version": { + "name": "protocol_version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "restart_count": { + "name": "restart_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "runtime": { + "name": "runtime", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "sandbox_id": { + "name": "sandbox_id", + "type": "text CHECK (\"sandbox_id\" = upper(\"sandbox_id\") AND length(\"sandbox_id\") = 26 AND substr(\"sandbox_id\", 1, 1) GLOB '[0-7]' AND \"sandbox_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "sandbox_session_id": { + "name": "sandbox_session_id", + "type": "text CHECK (\"sandbox_session_id\" = upper(\"sandbox_session_id\") AND length(\"sandbox_session_id\") = 26 AND substr(\"sandbox_session_id\", 1, 1) GLOB '[0-7]' AND \"sandbox_session_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status_changed_at": { + "name": "status_changed_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "status_event": { + "name": "status_event", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'driver.provision'" + }, + "status_operation_id": { + "name": "status_operation_id", + "type": "text CHECK (\"status_operation_id\" = upper(\"status_operation_id\") AND length(\"status_operation_id\") = 26 AND substr(\"status_operation_id\", 1, 1) GLOB '[0-7]' AND \"status_operation_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status_seq": { + "name": "status_seq", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "status_source": { + "name": "status_source", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'system'" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "driver_instance_completed_idx": { + "name": "driver_instance_completed_idx", + "columns": ["expires_at", "status"], + "isUnique": false + }, + "driver_instance_connection_idx": { + "name": "driver_instance_connection_idx", + "columns": ["connection_id"], + "isUnique": true, + "where": "\"driver_instance\".\"connection_id\" IS NOT NULL" + }, + "driver_instance_boot_token_expiry_idx": { + "name": "driver_instance_boot_token_expiry_idx", + "columns": ["status", "boot_token_expires_at"], + "isUnique": false, + "where": "\"driver_instance\".\"boot_token_used_at\" IS NULL" + }, + "driver_instance_boot_token_hash_idx": { + "name": "driver_instance_boot_token_hash_idx", + "columns": ["boot_token_hash"], + "isUnique": true + }, + "driver_instance_sandbox_session_idx": { + "name": "driver_instance_sandbox_session_idx", + "columns": ["sandbox_id", "sandbox_session_id", "status", "updated_at"], + "isUnique": false + }, + "driver_instance_live_sandbox_session_idx": { + "name": "driver_instance_live_sandbox_session_idx", + "columns": ["sandbox_id", "sandbox_session_id"], + "isUnique": true, + "where": "\"driver_instance\".\"status\" IN ('provisioning', 'connecting', 'ready', 'stopping')" + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": { + "driver_instance_status_check": { + "name": "driver_instance_status_check", + "value": "\"driver_instance\".\"status\" IN ('provisioning', 'connecting', 'ready', 'stopping', 'stopped', 'failed')" + }, + "driver_instance_status_seq_check": { + "name": "driver_instance_status_seq_check", + "value": "\"driver_instance\".\"status_seq\" >= 0" + } + } + }, + "external_tool_effect_attempt": { + "name": "external_tool_effect_attempt", + "columns": { + "attempt": { + "name": "attempt", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "completed_at": { + "name": "completed_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "effect_id": { + "name": "effect_id", + "type": "text CHECK (\"effect_id\" = upper(\"effect_id\") AND length(\"effect_id\") = 26 AND substr(\"effect_id\", 1, 1) GLOB '[0-7]' AND \"effect_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "provider_receipt_json": { + "name": "provider_receipt_json", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "result_json": { + "name": "result_json", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "external_tool_effect_attempt_status_idx": { + "name": "external_tool_effect_attempt_status_idx", + "columns": ["status", "created_at"], + "isUnique": false + } + }, + "foreignKeys": { + "external_tool_effect_attempt_effect_id_external_tool_effect_id_fk": { + "name": "external_tool_effect_attempt_effect_id_external_tool_effect_id_fk", + "tableFrom": "external_tool_effect_attempt", + "tableTo": "external_tool_effect", + "columnsFrom": ["effect_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "external_tool_effect_attempt_effect_id_attempt_pk": { + "columns": ["effect_id", "attempt"], + "name": "external_tool_effect_attempt_effect_id_attempt_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": { + "external_tool_effect_attempt_status_check": { + "name": "external_tool_effect_attempt_status_check", + "value": "\"external_tool_effect_attempt\".\"status\" IN ('executing', 'succeeded', 'unknown')" + } + } + }, + "external_tool_effect": { + "name": "external_tool_effect", + "columns": { + "attempt_count": { + "name": "attempt_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "command_id": { + "name": "command_id", + "type": "text CHECK (\"command_id\" = upper(\"command_id\") AND length(\"command_id\") = 26 AND substr(\"command_id\", 1, 1) GLOB '[0-7]' AND \"command_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "driver_instance_id": { + "name": "driver_instance_id", + "type": "text CHECK (\"driver_instance_id\" = upper(\"driver_instance_id\") AND length(\"driver_instance_id\") = 26 AND substr(\"driver_instance_id\", 1, 1) GLOB '[0-7]' AND \"driver_instance_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "id": { + "name": "id", + "type": "text CHECK (\"id\" = upper(\"id\") AND length(\"id\") = 26 AND substr(\"id\", 1, 1) GLOB '[0-7]' AND \"id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "idempotency_key": { + "name": "idempotency_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "provider_receipt_json": { + "name": "provider_receipt_json", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "result_json": { + "name": "result_json", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "server_id": { + "name": "server_id", + "type": "text CHECK (\"server_id\" = upper(\"server_id\") AND length(\"server_id\") = 26 AND substr(\"server_id\", 1, 1) GLOB '[0-7]' AND \"server_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "session_run_id": { + "name": "session_run_id", + "type": "text CHECK (\"session_run_id\" = upper(\"session_run_id\") AND length(\"session_run_id\") = 26 AND substr(\"session_run_id\", 1, 1) GLOB '[0-7]' AND \"session_run_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "tool_name": { + "name": "tool_name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "external_tool_effect_command_idx": { + "name": "external_tool_effect_command_idx", + "columns": ["command_id"], + "isUnique": true + }, + "external_tool_effect_idempotency_key_idx": { + "name": "external_tool_effect_idempotency_key_idx", + "columns": ["idempotency_key"], + "isUnique": true + }, + "external_tool_effect_run_status_idx": { + "name": "external_tool_effect_run_status_idx", + "columns": ["session_run_id", "status", "id"], + "isUnique": false + }, + "external_tool_effect_driver_status_idx": { + "name": "external_tool_effect_driver_status_idx", + "columns": ["driver_instance_id", "status"], + "isUnique": false + } + }, + "foreignKeys": { + "external_tool_effect_command_id_driver_command_id_fk": { + "name": "external_tool_effect_command_id_driver_command_id_fk", + "tableFrom": "external_tool_effect", + "tableTo": "driver_command", + "columnsFrom": ["command_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "external_tool_effect_driver_instance_id_driver_instance_id_fk": { + "name": "external_tool_effect_driver_instance_id_driver_instance_id_fk", + "tableFrom": "external_tool_effect", + "tableTo": "driver_instance", + "columnsFrom": ["driver_instance_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "external_tool_effect_session_run_id_session_run_id_fk": { + "name": "external_tool_effect_session_run_id_session_run_id_fk", + "tableFrom": "external_tool_effect", + "tableTo": "session_run", + "columnsFrom": ["session_run_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": { + "external_tool_effect_status_check": { + "name": "external_tool_effect_status_check", + "value": "\"external_tool_effect\".\"status\" IN ('intent', 'executing', 'succeeded', 'unknown')" + } + } + }, + "native_resume_ref": { + "name": "native_resume_ref", + "columns": { + "committed_session_run_id": { + "name": "committed_session_run_id", + "type": "text CHECK (\"committed_session_run_id\" = upper(\"committed_session_run_id\") AND length(\"committed_session_run_id\") = 26 AND substr(\"committed_session_run_id\", 1, 1) GLOB '[0-7]' AND \"committed_session_run_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "committed_value": { + "name": "committed_value", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "observed_driver_instance_id": { + "name": "observed_driver_instance_id", + "type": "text CHECK (\"observed_driver_instance_id\" = upper(\"observed_driver_instance_id\") AND length(\"observed_driver_instance_id\") = 26 AND substr(\"observed_driver_instance_id\", 1, 1) GLOB '[0-7]' AND \"observed_driver_instance_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "observed_session_run_id": { + "name": "observed_session_run_id", + "type": "text CHECK (\"observed_session_run_id\" = upper(\"observed_session_run_id\") AND length(\"observed_session_run_id\") = 26 AND substr(\"observed_session_run_id\", 1, 1) GLOB '[0-7]' AND \"observed_session_run_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "runtime_id": { + "name": "runtime_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "session_id": { + "name": "session_id", + "type": "text CHECK (\"session_id\" = upper(\"session_id\") AND length(\"session_id\") = 26 AND substr(\"session_id\", 1, 1) GLOB '[0-7]' AND \"session_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "native_resume_ref_runtime_updated_idx": { + "name": "native_resume_ref_runtime_updated_idx", + "columns": ["runtime_id", "updated_at"], + "isUnique": false + } + }, + "foreignKeys": { + "native_resume_ref_session_id_session_id_fk": { + "name": "native_resume_ref_session_id_session_id_fk", + "tableFrom": "native_resume_ref", + "tableTo": "session", + "columnsFrom": ["session_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "sandbox_backup": { + "name": "sandbox_backup", + "columns": { + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "dir": { + "name": "dir", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "id": { + "name": "id", + "type": "text CHECK (\"id\" = upper(\"id\") AND length(\"id\") = 26 AND substr(\"id\", 1, 1) GLOB '[0-7]' AND \"id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "keep": { + "name": "keep", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "sandbox_id": { + "name": "sandbox_id", + "type": "text CHECK (\"sandbox_id\" = upper(\"sandbox_id\") AND length(\"sandbox_id\") = 26 AND substr(\"sandbox_id\", 1, 1) GLOB '[0-7]' AND \"sandbox_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "session_run_id": { + "name": "session_run_id", + "type": "text CHECK (\"session_run_id\" = upper(\"session_run_id\") AND length(\"session_run_id\") = 26 AND substr(\"session_run_id\", 1, 1) GLOB '[0-7]' AND \"session_run_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "ttl_seconds": { + "name": "ttl_seconds", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "sandbox_backup_sandbox_status_created_idx": { + "name": "sandbox_backup_sandbox_status_created_idx", + "columns": ["sandbox_id", "status", "created_at"], + "isUnique": false + }, + "sandbox_backup_terminal_checkpoint_idx": { + "name": "sandbox_backup_terminal_checkpoint_idx", + "columns": ["sandbox_id", "dir", "session_run_id"], + "isUnique": true, + "where": "\"sandbox_backup\".\"session_run_id\" IS NOT NULL AND \"sandbox_backup\".\"status\" = 'ready'" + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "sandbox_session": { + "name": "sandbox_session", + "columns": { + "cloudflare_session_id": { + "name": "cloudflare_session_id", + "type": "text CHECK (\"cloudflare_session_id\" = upper(\"cloudflare_session_id\") AND length(\"cloudflare_session_id\") = 26 AND substr(\"cloudflare_session_id\", 1, 1) GLOB '[0-7]' AND \"cloudflare_session_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "cwd": { + "name": "cwd", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "origin_json": { + "name": "origin_json", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "sandbox_id": { + "name": "sandbox_id", + "type": "text CHECK (\"sandbox_id\" = upper(\"sandbox_id\") AND length(\"sandbox_id\") = 26 AND substr(\"sandbox_id\", 1, 1) GLOB '[0-7]' AND \"sandbox_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "session_id": { + "name": "session_id", + "type": "text CHECK (\"session_id\" = upper(\"session_id\") AND length(\"session_id\") = 26 AND substr(\"session_id\", 1, 1) GLOB '[0-7]' AND \"session_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "sandbox_session_sandbox_status_idx": { + "name": "sandbox_session_sandbox_status_idx", + "columns": ["sandbox_id", "status", "updated_at"], + "isUnique": false + }, + "sandbox_session_cloudflare_session_idx": { + "name": "sandbox_session_cloudflare_session_idx", + "columns": ["cloudflare_session_id"], + "isUnique": true + } + }, + "foreignKeys": { + "sandbox_session_session_id_session_id_fk": { + "name": "sandbox_session_session_id_session_id_fk", + "tableFrom": "sandbox_session", + "tableTo": "session", + "columnsFrom": ["session_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "sandbox": { + "name": "sandbox", + "columns": { + "agent_id": { + "name": "agent_id", + "type": "text CHECK (\"agent_id\" = upper(\"agent_id\") AND length(\"agent_id\") = 26 AND substr(\"agent_id\", 1, 1) GLOB '[0-7]' AND \"agent_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "app_id": { + "name": "app_id", + "type": "text CHECK (\"app_id\" = upper(\"app_id\") AND length(\"app_id\") = 26 AND substr(\"app_id\", 1, 1) GLOB '[0-7]' AND \"app_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "bind_mount_ready": { + "name": "bind_mount_ready", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "claim_expires_at": { + "name": "claim_expires_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "claim_owner": { + "name": "claim_owner", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "global_mounts_json": { + "name": "global_mounts_json", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'[]'" + }, + "id": { + "name": "id", + "type": "text CHECK (\"id\" = upper(\"id\") AND length(\"id\") = 26 AND substr(\"id\", 1, 1) GLOB '[0-7]' AND \"id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "inactive_deadline_at": { + "name": "inactive_deadline_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "last_backup_id": { + "name": "last_backup_id", + "type": "text CHECK (\"last_backup_id\" = upper(\"last_backup_id\") AND length(\"last_backup_id\") = 26 AND substr(\"last_backup_id\", 1, 1) GLOB '[0-7]' AND \"last_backup_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_error_code": { + "name": "last_error_code", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_restore_backup_id": { + "name": "last_restore_backup_id", + "type": "text CHECK (\"last_restore_backup_id\" = upper(\"last_restore_backup_id\") AND length(\"last_restore_backup_id\") = 26 AND substr(\"last_restore_backup_id\", 1, 1) GLOB '[0-7]' AND \"last_restore_backup_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "owner_account_id": { + "name": "owner_account_id", + "type": "text CHECK (\"owner_account_id\" = upper(\"owner_account_id\") AND length(\"owner_account_id\") = 26 AND substr(\"owner_account_id\", 1, 1) GLOB '[0-7]' AND \"owner_account_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status_changed_at": { + "name": "status_changed_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "status_event": { + "name": "status_event", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'runtime_subject.cold'" + }, + "status_operation_id": { + "name": "status_operation_id", + "type": "text CHECK (\"status_operation_id\" = upper(\"status_operation_id\") AND length(\"status_operation_id\") = 26 AND substr(\"status_operation_id\", 1, 1) GLOB '[0-7]' AND \"status_operation_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status_seq": { + "name": "status_seq", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "status_source": { + "name": "status_source", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'system'" + }, + "subject_id": { + "name": "subject_id", + "type": "text CHECK (\"subject_id\" = upper(\"subject_id\") AND length(\"subject_id\") = 26 AND substr(\"subject_id\", 1, 1) GLOB '[0-7]' AND \"subject_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "subject_kind": { + "name": "subject_kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "sandbox_subject_idx": { + "name": "sandbox_subject_idx", + "columns": ["kind", "subject_kind", "subject_id"], + "isUnique": true + }, + "sandbox_status_deadline_idx": { + "name": "sandbox_status_deadline_idx", + "columns": ["status", "inactive_deadline_at", "updated_at"], + "isUnique": false + }, + "sandbox_claim_idx": { + "name": "sandbox_claim_idx", + "columns": ["claim_expires_at", "claim_owner"], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": { + "sandbox_status_check": { + "name": "sandbox_status_check", + "value": "\"sandbox\".\"status\" IN ('cold', 'restoring', 'active', 'backing_up', 'destroying', 'error')" + }, + "sandbox_status_seq_check": { + "name": "sandbox_status_seq_check", + "value": "\"sandbox\".\"status_seq\" >= 0" + } + } + }, + "session_message": { + "name": "session_message", + "columns": { + "content_text": { + "name": "content_text", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_by_account_id": { + "name": "created_by_account_id", + "type": "text CHECK (\"created_by_account_id\" = upper(\"created_by_account_id\") AND length(\"created_by_account_id\") = 26 AND substr(\"created_by_account_id\", 1, 1) GLOB '[0-7]' AND \"created_by_account_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "id": { + "name": "id", + "type": "text CHECK (\"id\" = upper(\"id\") AND length(\"id\") = 26 AND substr(\"id\", 1, 1) GLOB '[0-7]' AND \"id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "plan_json": { + "name": "plan_json", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "segments_json": { + "name": "segments_json", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "seq": { + "name": "seq", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "session_id": { + "name": "session_id", + "type": "text CHECK (\"session_id\" = upper(\"session_id\") AND length(\"session_id\") = 26 AND substr(\"session_id\", 1, 1) GLOB '[0-7]' AND \"session_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "session_run_id": { + "name": "session_run_id", + "type": "text CHECK (\"session_run_id\" = upper(\"session_run_id\") AND length(\"session_run_id\") = 26 AND substr(\"session_run_id\", 1, 1) GLOB '[0-7]' AND \"session_run_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "session_message_session_seq_idx": { + "name": "session_message_session_seq_idx", + "columns": ["session_id", "seq"], + "isUnique": true + }, + "session_message_run_idx": { + "name": "session_message_run_idx", + "columns": ["session_run_id"], + "isUnique": false + } + }, + "foreignKeys": { + "session_message_session_id_session_id_fk": { + "name": "session_message_session_id_session_id_fk", + "tableFrom": "session_message", + "tableTo": "session", + "columnsFrom": ["session_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "session": { + "name": "session", + "columns": { + "agent_id": { + "name": "agent_id", + "type": "text CHECK (\"agent_id\" = upper(\"agent_id\") AND length(\"agent_id\") = 26 AND substr(\"agent_id\", 1, 1) GLOB '[0-7]' AND \"agent_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "archived_at": { + "name": "archived_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "end_user_id": { + "name": "end_user_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "attributed_user_id": { + "name": "attributed_user_id", + "type": "text CHECK (\"attributed_user_id\" = upper(\"attributed_user_id\") AND length(\"attributed_user_id\") = 26 AND substr(\"attributed_user_id\", 1, 1) GLOB '[0-7]' AND \"attributed_user_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "creator_account_id": { + "name": "creator_account_id", + "type": "text CHECK (\"creator_account_id\" = upper(\"creator_account_id\") AND length(\"creator_account_id\") = 26 AND substr(\"creator_account_id\", 1, 1) GLOB '[0-7]' AND \"creator_account_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "deployment_version_id": { + "name": "deployment_version_id", + "type": "text CHECK (\"deployment_version_id\" = upper(\"deployment_version_id\") AND length(\"deployment_version_id\") = 26 AND substr(\"deployment_version_id\", 1, 1) GLOB '[0-7]' AND \"deployment_version_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "deployment_version_number": { + "name": "deployment_version_number", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "id": { + "name": "id", + "type": "text CHECK (\"id\" = upper(\"id\") AND length(\"id\") = 26 AND substr(\"id\", 1, 1) GLOB '[0-7]' AND \"id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "last_message_at": { + "name": "last_message_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_run_id": { + "name": "last_run_id", + "type": "text CHECK (\"last_run_id\" = upper(\"last_run_id\") AND length(\"last_run_id\") = 26 AND substr(\"last_run_id\", 1, 1) GLOB '[0-7]' AND \"last_run_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "message_seq_cursor": { + "name": "message_seq_cursor", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "metadata_json": { + "name": "metadata_json", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'{}'" + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "app_id": { + "name": "app_id", + "type": "text CHECK (\"app_id\" = upper(\"app_id\") AND length(\"app_id\") = 26 AND substr(\"app_id\", 1, 1) GLOB '[0-7]' AND \"app_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "renamed": { + "name": "renamed", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "runtime_id": { + "name": "runtime_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status_operation_id": { + "name": "status_operation_id", + "type": "text CHECK (\"status_operation_id\" = upper(\"status_operation_id\") AND length(\"status_operation_id\") = 26 AND substr(\"status_operation_id\", 1, 1) GLOB '[0-7]' AND \"status_operation_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status_seq": { + "name": "status_seq", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "runtime_event_seq_cursor": { + "name": "runtime_event_seq_cursor", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'preview'" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "workspace_checkpoint_required": { + "name": "workspace_checkpoint_required", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + } + }, + "indexes": { + "session_agent_updated_idx": { + "name": "session_agent_updated_idx", + "columns": ["agent_id", "updated_at", "id"], + "isUnique": false + }, + "session_app_creator_archived_updated_idx": { + "name": "session_app_creator_archived_updated_idx", + "columns": ["app_id", "creator_account_id", "archived_at", "updated_at", "id"], + "isUnique": false + }, + "session_app_attributed_archived_updated_idx": { + "name": "session_app_attributed_archived_updated_idx", + "columns": ["app_id", "attributed_user_id", "archived_at", "updated_at", "id"], + "isUnique": false + }, + "session_app_creator_type_archived_updated_idx": { + "name": "session_app_creator_type_archived_updated_idx", + "columns": ["app_id", "creator_account_id", "type", "archived_at", "updated_at", "id"], + "isUnique": false + }, + "session_app_attributed_type_archived_updated_idx": { + "name": "session_app_attributed_type_archived_updated_idx", + "columns": ["app_id", "attributed_user_id", "type", "archived_at", "updated_at", "id"], + "isUnique": false + }, + "session_status_operation_updated_idx": { + "name": "session_status_operation_updated_idx", + "columns": ["status", "status_operation_id", "updated_at"], + "isUnique": false + }, + "session_status_updated_idx": { + "name": "session_status_updated_idx", + "columns": ["status", "updated_at", "id"], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": { + "session_status_check": { + "name": "session_status_check", + "value": "\"session\".\"status\" IN ('IDLE', 'RUNNING', 'RESCHEDULING', 'TERMINATED')" + }, + "session_status_seq_check": { + "name": "session_status_seq_check", + "value": "\"session\".\"status_seq\" >= 0" + } + } + }, + "session_execution_snapshot": { + "name": "session_execution_snapshot", + "columns": { + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "plan_json": { + "name": "plan_json", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "session_id": { + "name": "session_id", + "type": "text CHECK (\"session_id\" = upper(\"session_id\") AND length(\"session_id\") = 26 AND substr(\"session_id\", 1, 1) GLOB '[0-7]' AND \"session_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": true, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "session_execution_snapshot_session_id_session_id_fk": { + "name": "session_execution_snapshot_session_id_session_id_fk", + "tableFrom": "session_execution_snapshot", + "tableTo": "session", + "columnsFrom": ["session_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "session_run_skill": { + "name": "session_run_skill", + "columns": { + "blob_sha256": { + "name": "blob_sha256", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "materialization_status": { + "name": "materialization_status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "mount_path": { + "name": "mount_path", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "resolution_mode": { + "name": "resolution_mode", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "session_run_id": { + "name": "session_run_id", + "type": "text CHECK (\"session_run_id\" = upper(\"session_run_id\") AND length(\"session_run_id\") = 26 AND substr(\"session_run_id\", 1, 1) GLOB '[0-7]' AND \"session_run_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "skill_id": { + "name": "skill_id", + "type": "text CHECK (\"skill_id\" = upper(\"skill_id\") AND length(\"skill_id\") = 26 AND substr(\"skill_id\", 1, 1) GLOB '[0-7]' AND \"skill_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "skill_name": { + "name": "skill_name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "snapshot_id": { + "name": "snapshot_id", + "type": "text CHECK (\"snapshot_id\" = upper(\"snapshot_id\") AND length(\"snapshot_id\") = 26 AND substr(\"snapshot_id\", 1, 1) GLOB '[0-7]' AND \"snapshot_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "warning_code": { + "name": "warning_code", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "session_run_skill_run_resolution_idx": { + "name": "session_run_skill_run_resolution_idx", + "columns": ["session_run_id", "resolution_mode"], + "isUnique": false + } + }, + "foreignKeys": { + "session_run_skill_session_run_id_session_run_id_fk": { + "name": "session_run_skill_session_run_id_session_run_id_fk", + "tableFrom": "session_run_skill", + "tableTo": "session_run", + "columnsFrom": ["session_run_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "session_run_skill_session_run_id_skill_id_pk": { + "columns": ["session_run_id", "skill_id"], + "name": "session_run_skill_session_run_id_skill_id_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "session_run": { + "name": "session_run", + "columns": { + "agent_id": { + "name": "agent_id", + "type": "text CHECK (\"agent_id\" = upper(\"agent_id\") AND length(\"agent_id\") = 26 AND substr(\"agent_id\", 1, 1) GLOB '[0-7]' AND \"agent_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "bound_capability_agent_id": { + "name": "bound_capability_agent_id", + "type": "text CHECK (\"bound_capability_agent_id\" = upper(\"bound_capability_agent_id\") AND length(\"bound_capability_agent_id\") = 26 AND substr(\"bound_capability_agent_id\", 1, 1) GLOB '[0-7]' AND \"bound_capability_agent_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "bound_capability_app_id": { + "name": "bound_capability_app_id", + "type": "text CHECK (\"bound_capability_app_id\" = upper(\"bound_capability_app_id\") AND length(\"bound_capability_app_id\") = 26 AND substr(\"bound_capability_app_id\", 1, 1) GLOB '[0-7]' AND \"bound_capability_app_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "bound_capability_binding_env": { + "name": "bound_capability_binding_env", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "bound_capability_binding_name": { + "name": "bound_capability_binding_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "bound_capability_deployment_id": { + "name": "bound_capability_deployment_id", + "type": "text CHECK (\"bound_capability_deployment_id\" = upper(\"bound_capability_deployment_id\") AND length(\"bound_capability_deployment_id\") = 26 AND substr(\"bound_capability_deployment_id\", 1, 1) GLOB '[0-7]' AND \"bound_capability_deployment_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "bound_capability_deployment_run_id": { + "name": "bound_capability_deployment_run_id", + "type": "text CHECK (\"bound_capability_deployment_run_id\" = upper(\"bound_capability_deployment_run_id\") AND length(\"bound_capability_deployment_run_id\") = 26 AND substr(\"bound_capability_deployment_run_id\", 1, 1) GLOB '[0-7]' AND \"bound_capability_deployment_run_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "completed_at": { + "name": "completed_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_by_account_id": { + "name": "created_by_account_id", + "type": "text CHECK (\"created_by_account_id\" = upper(\"created_by_account_id\") AND length(\"created_by_account_id\") = 26 AND substr(\"created_by_account_id\", 1, 1) GLOB '[0-7]' AND \"created_by_account_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "deployment_version_id": { + "name": "deployment_version_id", + "type": "text CHECK (\"deployment_version_id\" = upper(\"deployment_version_id\") AND length(\"deployment_version_id\") = 26 AND substr(\"deployment_version_id\", 1, 1) GLOB '[0-7]' AND \"deployment_version_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "deployment_version_number": { + "name": "deployment_version_number", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "driver_instance_id": { + "name": "driver_instance_id", + "type": "text CHECK (\"driver_instance_id\" = upper(\"driver_instance_id\") AND length(\"driver_instance_id\") = 26 AND substr(\"driver_instance_id\", 1, 1) GLOB '[0-7]' AND \"driver_instance_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "error_code": { + "name": "error_code", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "error_details_json": { + "name": "error_details_json", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "id": { + "name": "id", + "type": "text CHECK (\"id\" = upper(\"id\") AND length(\"id\") = 26 AND substr(\"id\", 1, 1) GLOB '[0-7]' AND \"id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "runtime_id": { + "name": "runtime_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "session_id": { + "name": "session_id", + "type": "text CHECK (\"session_id\" = upper(\"session_id\") AND length(\"session_id\") = 26 AND substr(\"session_id\", 1, 1) GLOB '[0-7]' AND \"session_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "started_at": { + "name": "started_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status_changed_at": { + "name": "status_changed_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "status_event": { + "name": "status_event", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'run.queue'" + }, + "status_operation_id": { + "name": "status_operation_id", + "type": "text CHECK (\"status_operation_id\" = upper(\"status_operation_id\") AND length(\"status_operation_id\") = 26 AND substr(\"status_operation_id\", 1, 1) GLOB '[0-7]' AND \"status_operation_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status_seq": { + "name": "status_seq", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "status_source": { + "name": "status_source", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'system'" + }, + "trace_id": { + "name": "trace_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "trigger": { + "name": "trigger", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "session_run_driver_instance_idx": { + "name": "session_run_driver_instance_idx", + "columns": ["driver_instance_id", "created_at"], + "isUnique": false + }, + "session_run_active_driver_lease_idx": { + "name": "session_run_active_driver_lease_idx", + "columns": ["driver_instance_id"], + "isUnique": true, + "where": "\"session_run\".\"driver_instance_id\" IS NOT NULL AND \"session_run\".\"status\" IN ('queued', 'booting', 'running', 'waiting_input')" + }, + "session_run_session_created_at_idx": { + "name": "session_run_session_created_at_idx", + "columns": ["session_id", "created_at"], + "isUnique": false + }, + "session_run_session_status_idx": { + "name": "session_run_session_status_idx", + "columns": ["session_id", "status"], + "isUnique": false + } + }, + "foreignKeys": { + "session_run_session_id_session_id_fk": { + "name": "session_run_session_id_session_id_fk", + "tableFrom": "session_run", + "tableTo": "session", + "columnsFrom": ["session_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": { + "session_run_status_check": { + "name": "session_run_status_check", + "value": "\"session_run\".\"status\" IN ('queued', 'booting', 'running', 'waiting_input', 'completed', 'failed', 'cancelled', 'expired')" + }, + "session_run_status_seq_check": { + "name": "session_run_status_seq_check", + "value": "\"session_run\".\"status_seq\" >= 0" + } + } + }, + "session_agent_task_snapshot": { + "name": "session_agent_task_snapshot", + "columns": { + "driver_instance_id": { + "name": "driver_instance_id", + "type": "text CHECK (\"driver_instance_id\" = upper(\"driver_instance_id\") AND length(\"driver_instance_id\") = 26 AND substr(\"driver_instance_id\", 1, 1) GLOB '[0-7]' AND \"driver_instance_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "run_id": { + "name": "run_id", + "type": "text CHECK (\"run_id\" = upper(\"run_id\") AND length(\"run_id\") = 26 AND substr(\"run_id\", 1, 1) GLOB '[0-7]' AND \"run_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "seq": { + "name": "seq", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "session_id": { + "name": "session_id", + "type": "text CHECK (\"session_id\" = upper(\"session_id\") AND length(\"session_id\") = 26 AND substr(\"session_id\", 1, 1) GLOB '[0-7]' AND \"session_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tasks_json": { + "name": "tasks_json", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "session_agent_task_snapshot_run_id_session_run_id_fk": { + "name": "session_agent_task_snapshot_run_id_session_run_id_fk", + "tableFrom": "session_agent_task_snapshot", + "tableTo": "session_run", + "columnsFrom": ["run_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "session_agent_task_snapshot_session_id_session_id_fk": { + "name": "session_agent_task_snapshot_session_id_session_id_fk", + "tableFrom": "session_agent_task_snapshot", + "tableTo": "session", + "columnsFrom": ["session_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "session_event": { + "name": "session_event", + "columns": { + "agent_id": { + "name": "agent_id", + "type": "text CHECK (\"agent_id\" = upper(\"agent_id\") AND length(\"agent_id\") = 26 AND substr(\"agent_id\", 1, 1) GLOB '[0-7]' AND \"agent_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "content_text": { + "name": "content_text", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "ended_at": { + "name": "ended_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "event_type": { + "name": "event_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "family": { + "name": "family", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "id": { + "name": "id", + "type": "text CHECK (\"id\" = upper(\"id\") AND length(\"id\") = 26 AND substr(\"id\", 1, 1) GLOB '[0-7]' AND \"id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "occurred_at": { + "name": "occurred_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "process_status": { + "name": "process_status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "process_type": { + "name": "process_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "run_id": { + "name": "run_id", + "type": "text CHECK (\"run_id\" = upper(\"run_id\") AND length(\"run_id\") = 26 AND substr(\"run_id\", 1, 1) GLOB '[0-7]' AND \"run_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "seq": { + "name": "seq", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "session_id": { + "name": "session_id", + "type": "text CHECK (\"session_id\" = upper(\"session_id\") AND length(\"session_id\") = 26 AND substr(\"session_id\", 1, 1) GLOB '[0-7]' AND \"session_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "source_event_id": { + "name": "source_event_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "tool_call_id": { + "name": "tool_call_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "tool_input_json": { + "name": "tool_input_json", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "tool_name": { + "name": "tool_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "tokens": { + "name": "tokens", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "trace_id": { + "name": "trace_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "visibility": { + "name": "visibility", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "session_event_agent_family_created_idx": { + "name": "session_event_agent_family_created_idx", + "columns": ["agent_id", "family", "created_at", "id"], + "isUnique": false + }, + "session_event_agent_visibility_created_idx": { + "name": "session_event_agent_visibility_created_idx", + "columns": ["agent_id", "visibility", "created_at", "id"], + "isUnique": false + }, + "session_event_agent_created_idx": { + "name": "session_event_agent_created_idx", + "columns": ["agent_id", "created_at", "id"], + "isUnique": false + }, + "session_event_session_visibility_seq_idx": { + "name": "session_event_session_visibility_seq_idx", + "columns": ["session_id", "visibility", "seq"], + "isUnique": false + }, + "session_event_run_event_type_idx": { + "name": "session_event_run_event_type_idx", + "columns": ["run_id", "event_type"], + "isUnique": false + }, + "session_event_session_seq_idx": { + "name": "session_event_session_seq_idx", + "columns": ["session_id", "seq"], + "isUnique": true + }, + "session_event_session_source_idx": { + "name": "session_event_session_source_idx", + "columns": ["session_id", "source_event_id"], + "isUnique": true + } + }, + "foreignKeys": { + "session_event_session_id_session_id_fk": { + "name": "session_event_session_id_session_id_fk", + "tableFrom": "session_event", + "tableTo": "session", + "columnsFrom": ["session_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "session_model_call": { + "name": "session_model_call", + "columns": { + "cache_creation_tokens": { + "name": "cache_creation_tokens", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "cache_read_tokens": { + "name": "cache_read_tokens", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "call_key": { + "name": "call_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "completed_at": { + "name": "completed_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "cost_currency": { + "name": "cost_currency", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "driver_instance_id": { + "name": "driver_instance_id", + "type": "text CHECK (\"driver_instance_id\" = upper(\"driver_instance_id\") AND length(\"driver_instance_id\") = 26 AND substr(\"driver_instance_id\", 1, 1) GLOB '[0-7]' AND \"driver_instance_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "error_code": { + "name": "error_code", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "id": { + "name": "id", + "type": "text CHECK (\"id\" = upper(\"id\") AND length(\"id\") = 26 AND substr(\"id\", 1, 1) GLOB '[0-7]' AND \"id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "input_tokens": { + "name": "input_tokens", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "metadata_json": { + "name": "metadata_json", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "native_call_id": { + "name": "native_call_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "output_tokens": { + "name": "output_tokens", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "session_id": { + "name": "session_id", + "type": "text CHECK (\"session_id\" = upper(\"session_id\") AND length(\"session_id\") = 26 AND substr(\"session_id\", 1, 1) GLOB '[0-7]' AND \"session_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "session_run_id": { + "name": "session_run_id", + "type": "text CHECK (\"session_run_id\" = upper(\"session_run_id\") AND length(\"session_run_id\") = 26 AND substr(\"session_run_id\", 1, 1) GLOB '[0-7]' AND \"session_run_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "started_at": { + "name": "started_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "total_cost_usd_micros": { + "name": "total_cost_usd_micros", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "trace_id": { + "name": "trace_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "session_model_call_run_created_idx": { + "name": "session_model_call_run_created_idx", + "columns": ["session_run_id", "created_at"], + "isUnique": false + }, + "session_model_call_session_created_idx": { + "name": "session_model_call_session_created_idx", + "columns": ["session_id", "created_at"], + "isUnique": false + }, + "session_model_call_run_key_idx": { + "name": "session_model_call_run_key_idx", + "columns": ["session_run_id", "call_key"], + "isUnique": true + }, + "session_model_call_native_idx": { + "name": "session_model_call_native_idx", + "columns": ["driver_instance_id", "native_call_id"], + "isUnique": true + } + }, + "foreignKeys": { + "session_model_call_session_id_session_id_fk": { + "name": "session_model_call_session_id_session_id_fk", + "tableFrom": "session_model_call", + "tableTo": "session", + "columnsFrom": ["session_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "session_model_call_session_run_id_session_run_id_fk": { + "name": "session_model_call_session_run_id_session_run_id_fk", + "tableFrom": "session_model_call", + "tableTo": "session_run", + "columnsFrom": ["session_run_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "session_permission_request": { + "name": "session_permission_request", + "columns": { + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "driver_instance_id": { + "name": "driver_instance_id", + "type": "text CHECK (\"driver_instance_id\" = upper(\"driver_instance_id\") AND length(\"driver_instance_id\") = 26 AND substr(\"driver_instance_id\", 1, 1) GLOB '[0-7]' AND \"driver_instance_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "raw_input": { + "name": "raw_input", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "request_id": { + "name": "request_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "run_id": { + "name": "run_id", + "type": "text CHECK (\"run_id\" = upper(\"run_id\") AND length(\"run_id\") = 26 AND substr(\"run_id\", 1, 1) GLOB '[0-7]' AND \"run_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "session_id": { + "name": "session_id", + "type": "text CHECK (\"session_id\" = upper(\"session_id\") AND length(\"session_id\") = 26 AND substr(\"session_id\", 1, 1) GLOB '[0-7]' AND \"session_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "tool_call_id": { + "name": "tool_call_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "tool_kind": { + "name": "tool_kind", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "session_permission_request_run_idx": { + "name": "session_permission_request_run_idx", + "columns": ["session_id", "run_id"], + "isUnique": false + } + }, + "foreignKeys": { + "session_permission_request_session_id_session_id_fk": { + "name": "session_permission_request_session_id_session_id_fk", + "tableFrom": "session_permission_request", + "tableTo": "session", + "columnsFrom": ["session_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "session_permission_request_session_id_request_id_pk": { + "columns": ["session_id", "request_id"], + "name": "session_permission_request_session_id_request_id_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "session_readiness_snapshot": { + "name": "session_readiness_snapshot", + "columns": { + "readiness_json": { + "name": "readiness_json", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "session_id": { + "name": "session_id", + "type": "text CHECK (\"session_id\" = upper(\"session_id\") AND length(\"session_id\") = 26 AND substr(\"session_id\", 1, 1) GLOB '[0-7]' AND \"session_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "session_readiness_snapshot_session_id_session_id_fk": { + "name": "session_readiness_snapshot_session_id_session_id_fk", + "tableFrom": "session_readiness_snapshot", + "tableTo": "session", + "columnsFrom": ["session_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "skill_snapshot_entry": { + "name": "skill_snapshot_entry", + "columns": { + "entry_kind": { + "name": "entry_kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "is_executable": { + "name": "is_executable", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "mime_type": { + "name": "mime_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "path": { + "name": "path", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "sha256": { + "name": "sha256", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "size": { + "name": "size", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "snapshot_id": { + "name": "snapshot_id", + "type": "text CHECK (\"snapshot_id\" = upper(\"snapshot_id\") AND length(\"snapshot_id\") = 26 AND substr(\"snapshot_id\", 1, 1) GLOB '[0-7]' AND \"snapshot_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "skill_snapshot_entry_snapshot_id_path_pk": { + "columns": ["snapshot_id", "path"], + "name": "skill_snapshot_entry_snapshot_id_path_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "skill_snapshot": { + "name": "skill_snapshot", + "columns": { + "author": { + "name": "author", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "blob_key": { + "name": "blob_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "blob_sha256": { + "name": "blob_sha256", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "blob_size": { + "name": "blob_size", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "id": { + "name": "id", + "type": "text CHECK (\"id\" = upper(\"id\") AND length(\"id\") = 26 AND substr(\"id\", 1, 1) GLOB '[0-7]' AND \"id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "app_id": { + "name": "app_id", + "type": "text CHECK (\"app_id\" = upper(\"app_id\") AND length(\"app_id\") = 26 AND substr(\"app_id\", 1, 1) GLOB '[0-7]' AND \"app_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "skill_markdown_path": { + "name": "skill_markdown_path", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "uncompressed_size": { + "name": "uncompressed_size", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "version": { + "name": "version", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "skill_snapshot_app_created_at_idx": { + "name": "skill_snapshot_app_created_at_idx", + "columns": ["app_id", "created_at"], + "isUnique": false + }, + "skill_snapshot_blob_sha256_idx": { + "name": "skill_snapshot_blob_sha256_idx", + "columns": ["app_id", "blob_sha256"], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "skill": { + "name": "skill", + "columns": { + "author": { + "name": "author", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "current_snapshot_id": { + "name": "current_snapshot_id", + "type": "text CHECK (\"current_snapshot_id\" = upper(\"current_snapshot_id\") AND length(\"current_snapshot_id\") = 26 AND substr(\"current_snapshot_id\", 1, 1) GLOB '[0-7]' AND \"current_snapshot_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "forked_from_owner_name": { + "name": "forked_from_owner_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "forked_from_skill_id": { + "name": "forked_from_skill_id", + "type": "text CHECK (\"forked_from_skill_id\" = upper(\"forked_from_skill_id\") AND length(\"forked_from_skill_id\") = 26 AND substr(\"forked_from_skill_id\", 1, 1) GLOB '[0-7]' AND \"forked_from_skill_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "forked_from_skill_name": { + "name": "forked_from_skill_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "id": { + "name": "id", + "type": "text CHECK (\"id\" = upper(\"id\") AND length(\"id\") = 26 AND substr(\"id\", 1, 1) GLOB '[0-7]' AND \"id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "owner_account_id": { + "name": "owner_account_id", + "type": "text CHECK (\"owner_account_id\" = upper(\"owner_account_id\") AND length(\"owner_account_id\") = 26 AND substr(\"owner_account_id\", 1, 1) GLOB '[0-7]' AND \"owner_account_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "app_id": { + "name": "app_id", + "type": "text CHECK (\"app_id\" = upper(\"app_id\") AND length(\"app_id\") = 26 AND substr(\"app_id\", 1, 1) GLOB '[0-7]' AND \"app_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "source_kind": { + "name": "source_kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "version": { + "name": "version", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "skill_app_updated_at_idx": { + "name": "skill_app_updated_at_idx", + "columns": ["app_id", "updated_at"], + "isUnique": false + }, + "skill_owner_account_updated_at_idx": { + "name": "skill_owner_account_updated_at_idx", + "columns": ["owner_account_id", "updated_at"], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "account": { + "name": "account", + "columns": { + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "email_verified": { + "name": "email_verified", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "id": { + "name": "id", + "type": "text CHECK (\"id\" = upper(\"id\") AND length(\"id\") = 26 AND substr(\"id\", 1, 1) GLOB '[0-7]' AND \"id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "image_url": { + "name": "image_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_active_organization_id": { + "name": "last_active_organization_id", + "type": "text CHECK (\"last_active_organization_id\" = upper(\"last_active_organization_id\") AND length(\"last_active_organization_id\") = 26 AND substr(\"last_active_organization_id\", 1, 1) GLOB '[0-7]' AND \"last_active_organization_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "system_agent_model": { + "name": "system_agent_model", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "account_email_idx": { + "name": "account_email_idx", + "columns": ["email"], + "isUnique": true + }, + "account_last_active_organization_idx": { + "name": "account_last_active_organization_idx", + "columns": ["last_active_organization_id"], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "usage_daily_rollup": { + "name": "usage_daily_rollup", + "columns": { + "actor_user_id": { + "name": "actor_user_id", + "type": "text CHECK (\"actor_user_id\" = upper(\"actor_user_id\") AND length(\"actor_user_id\") = 26 AND substr(\"actor_user_id\", 1, 1) GLOB '[0-7]' AND \"actor_user_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "agent_id": { + "name": "agent_id", + "type": "text CHECK (\"agent_id\" = upper(\"agent_id\") AND length(\"agent_id\") = 26 AND substr(\"agent_id\", 1, 1) GLOB '[0-7]' AND \"agent_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "agent_owner_user_id": { + "name": "agent_owner_user_id", + "type": "text CHECK (\"agent_owner_user_id\" = upper(\"agent_owner_user_id\") AND length(\"agent_owner_user_id\") = 26 AND substr(\"agent_owner_user_id\", 1, 1) GLOB '[0-7]' AND \"agent_owner_user_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "agent_publication_state_at_run": { + "name": "agent_publication_state_at_run", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "cache_creation_tokens": { + "name": "cache_creation_tokens", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "cache_read_tokens": { + "name": "cache_read_tokens", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "date": { + "name": "date", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "input_tokens": { + "name": "input_tokens", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "organization_id": { + "name": "organization_id", + "type": "text CHECK (\"organization_id\" = upper(\"organization_id\") AND length(\"organization_id\") = 26 AND substr(\"organization_id\", 1, 1) GLOB '[0-7]' AND \"organization_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "app_id": { + "name": "app_id", + "type": "text CHECK (\"app_id\" = upper(\"app_id\") AND length(\"app_id\") = 26 AND substr(\"app_id\", 1, 1) GLOB '[0-7]' AND \"app_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "output_tokens": { + "name": "output_tokens", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "request_count": { + "name": "request_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "run_purpose": { + "name": "run_purpose", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "total_cost_usd_micros": { + "name": "total_cost_usd_micros", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "unpriced_request_count": { + "name": "unpriced_request_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "usage_daily_rollup_app_date_idx": { + "name": "usage_daily_rollup_app_date_idx", + "columns": ["app_id", "date"], + "isUnique": false + }, + "usage_daily_rollup_organization_date_idx": { + "name": "usage_daily_rollup_organization_date_idx", + "columns": ["organization_id", "date"], + "isUnique": false + }, + "usage_daily_rollup_agent_date_idx": { + "name": "usage_daily_rollup_agent_date_idx", + "columns": ["agent_id", "date"], + "isUnique": false + }, + "usage_daily_rollup_actor_date_idx": { + "name": "usage_daily_rollup_actor_date_idx", + "columns": ["actor_user_id", "date"], + "isUnique": false + }, + "usage_daily_rollup_owner_date_idx": { + "name": "usage_daily_rollup_owner_date_idx", + "columns": ["agent_owner_user_id", "date"], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "usage_daily_rollup_organization_id_app_id_agent_id_actor_user_id_agent_owner_user_id_date_agent_publication_state_at_run_run_purpose_provider_model_pk": { + "columns": [ + "organization_id", + "app_id", + "agent_id", + "actor_user_id", + "agent_owner_user_id", + "date", + "agent_publication_state_at_run", + "run_purpose", + "provider", + "model" + ], + "name": "usage_daily_rollup_organization_id_app_id_agent_id_actor_user_id_agent_owner_user_id_date_agent_publication_state_at_run_run_purpose_provider_model_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "usage_event_rollup_receipt": { + "name": "usage_event_rollup_receipt", + "columns": { + "rolled_up_at": { + "name": "rolled_up_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "source_event_id": { + "name": "source_event_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "usage_event_rollup_receipt_rolled_up_at_idx": { + "name": "usage_event_rollup_receipt_rolled_up_at_idx", + "columns": ["rolled_up_at"], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "usage_event_rollup_receipt_source_source_event_id_pk": { + "columns": ["source", "source_event_id"], + "name": "usage_event_rollup_receipt_source_source_event_id_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "usage_event": { + "name": "usage_event", + "columns": { + "actor_user_id": { + "name": "actor_user_id", + "type": "text CHECK (\"actor_user_id\" = upper(\"actor_user_id\") AND length(\"actor_user_id\") = 26 AND substr(\"actor_user_id\", 1, 1) GLOB '[0-7]' AND \"actor_user_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "agent_id": { + "name": "agent_id", + "type": "text CHECK (\"agent_id\" = upper(\"agent_id\") AND length(\"agent_id\") = 26 AND substr(\"agent_id\", 1, 1) GLOB '[0-7]' AND \"agent_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "agent_owner_user_id": { + "name": "agent_owner_user_id", + "type": "text CHECK (\"agent_owner_user_id\" = upper(\"agent_owner_user_id\") AND length(\"agent_owner_user_id\") = 26 AND substr(\"agent_owner_user_id\", 1, 1) GLOB '[0-7]' AND \"agent_owner_user_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "agent_publication_state_at_run": { + "name": "agent_publication_state_at_run", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "agent_revision_id": { + "name": "agent_revision_id", + "type": "text CHECK (\"agent_revision_id\" = upper(\"agent_revision_id\") AND length(\"agent_revision_id\") = 26 AND substr(\"agent_revision_id\", 1, 1) GLOB '[0-7]' AND \"agent_revision_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "cache_creation_tokens": { + "name": "cache_creation_tokens", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "cache_read_tokens": { + "name": "cache_read_tokens", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "id": { + "name": "id", + "type": "text CHECK (\"id\" = upper(\"id\") AND length(\"id\") = 26 AND substr(\"id\", 1, 1) GLOB '[0-7]' AND \"id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "input_tokens": { + "name": "input_tokens", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "organization_id": { + "name": "organization_id", + "type": "text CHECK (\"organization_id\" = upper(\"organization_id\") AND length(\"organization_id\") = 26 AND substr(\"organization_id\", 1, 1) GLOB '[0-7]' AND \"organization_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "app_id": { + "name": "app_id", + "type": "text CHECK (\"app_id\" = upper(\"app_id\") AND length(\"app_id\") = 26 AND substr(\"app_id\", 1, 1) GLOB '[0-7]' AND \"app_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "output_tokens": { + "name": "output_tokens", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "price_snapshot_json": { + "name": "price_snapshot_json", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "pricing_status": { + "name": "pricing_status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "run_purpose": { + "name": "run_purpose", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "runtime_id": { + "name": "runtime_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "session_id": { + "name": "session_id", + "type": "text CHECK (\"session_id\" = upper(\"session_id\") AND length(\"session_id\") = 26 AND substr(\"session_id\", 1, 1) GLOB '[0-7]' AND \"session_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "session_run_id": { + "name": "session_run_id", + "type": "text CHECK (\"session_run_id\" = upper(\"session_run_id\") AND length(\"session_run_id\") = 26 AND substr(\"session_run_id\", 1, 1) GLOB '[0-7]' AND \"session_run_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "source_event_id": { + "name": "source_event_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "total_cost_usd_micros": { + "name": "total_cost_usd_micros", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "usage_contract": { + "name": "usage_contract", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "usage_event_app_created_idx": { + "name": "usage_event_app_created_idx", + "columns": ["app_id", "created_at"], + "isUnique": false + }, + "usage_event_organization_created_idx": { + "name": "usage_event_organization_created_idx", + "columns": ["organization_id", "created_at"], + "isUnique": false + }, + "usage_event_agent_created_idx": { + "name": "usage_event_agent_created_idx", + "columns": ["agent_id", "created_at"], + "isUnique": false + }, + "usage_event_actor_created_idx": { + "name": "usage_event_actor_created_idx", + "columns": ["actor_user_id", "created_at"], + "isUnique": false + }, + "usage_event_owner_created_idx": { + "name": "usage_event_owner_created_idx", + "columns": ["agent_owner_user_id", "created_at"], + "isUnique": false + }, + "usage_event_session_run_idx": { + "name": "usage_event_session_run_idx", + "columns": ["session_run_id"], + "isUnique": false + }, + "usage_event_source_event_idx": { + "name": "usage_event_source_event_idx", + "columns": ["source", "source_event_id"], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "vendor_credential": { + "name": "vendor_credential", + "columns": { + "api_base": { + "name": "api_base", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "api_key_secret_id": { + "name": "api_key_secret_id", + "type": "text CHECK (\"api_key_secret_id\" = upper(\"api_key_secret_id\") AND length(\"api_key_secret_id\") = 26 AND substr(\"api_key_secret_id\", 1, 1) GLOB '[0-7]' AND \"api_key_secret_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "id": { + "name": "id", + "type": "text CHECK (\"id\" = upper(\"id\") AND length(\"id\") = 26 AND substr(\"id\", 1, 1) GLOB '[0-7]' AND \"id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "is_default": { + "name": "is_default", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "models": { + "name": "models", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "app_id": { + "name": "app_id", + "type": "text CHECK (\"app_id\" = upper(\"app_id\") AND length(\"app_id\") = 26 AND substr(\"app_id\", 1, 1) GLOB '[0-7]' AND \"app_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "vendor_id": { + "name": "vendor_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "vendor_credential_app_vendor_idx": { + "name": "vendor_credential_app_vendor_idx", + "columns": ["app_id", "vendor_id"], + "isUnique": false + }, + "vendor_credential_app_vendor_name_idx": { + "name": "vendor_credential_app_vendor_name_idx", + "columns": ["app_id", "vendor_id", "name"], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + } + }, + "views": {}, + "enums": {}, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + }, + "internal": { + "indexes": { + "file_record_listing_idx": { + "columns": { + "lower(\"name\")": { + "isExpression": true + } + } + } + } + } +} diff --git a/pkgs/db/drizzle/meta/_journal.json b/pkgs/db/drizzle/meta/_journal.json index 08e8cec7..c626e569 100644 --- a/pkgs/db/drizzle/meta/_journal.json +++ b/pkgs/db/drizzle/meta/_journal.json @@ -85,6 +85,13 @@ "when": 1787207755791, "tag": "0011_cattle-terminal-checkpoints", "breakpoints": true + }, + { + "idx": 12, + "version": "6", + "when": 1787948328549, + "tag": "0012_agent-task-snapshot-state", + "breakpoints": true } ] } diff --git a/pkgs/db/src/schema/session/events.schema.ts b/pkgs/db/src/schema/session/events.schema.ts index b1551bd7..8f7b37e5 100644 --- a/pkgs/db/src/schema/session/events.schema.ts +++ b/pkgs/db/src/schema/session/events.schema.ts @@ -110,5 +110,18 @@ export const sessionEventsTable = sqliteTable( ], ); +export const sessionAgentTaskSnapshotsTable = sqliteTable("session_agent_task_snapshot", { + driverInstanceId: platformIdColumn("driver_instance_id").notNull(), + runId: platformIdColumn("run_id") + .notNull() + .references(() => sessionRunsTable.id, { onDelete: "cascade" }), + seq: integer("seq").notNull(), + sessionId: platformIdColumn("session_id") + .primaryKey() + .references(() => sessionsTable.id, { onDelete: "cascade" }), + tasksJson: text("tasks_json").notNull(), +}); + export type SessionEventRow = typeof sessionEventsTable.$inferSelect; +export type SessionAgentTaskSnapshotRow = typeof sessionAgentTaskSnapshotsTable.$inferSelect; export type SessionModelCallRow = typeof sessionModelCallsTable.$inferSelect; diff --git a/pkgs/runtime-events/src/process-draft.ts b/pkgs/runtime-events/src/process-draft.ts index fbfc6da0..a1ec78d9 100644 --- a/pkgs/runtime-events/src/process-draft.ts +++ b/pkgs/runtime-events/src/process-draft.ts @@ -10,6 +10,7 @@ import type { import type { RuntimeEventEnvelope } from "./runtime-event"; import { + readRuntimeAgentTaskSnapshot, readRuntimeEventFileChangePath, readRuntimeEventMessageDelta, readRuntimeEventMessageRole, @@ -88,6 +89,13 @@ export function createProcessDraftFromRuntimeEvent(event: RuntimeEventEnvelope): const payload = readRuntimeEventPayload(event); switch (event.kind) { + case "agent.tasks.replaced": { + const count = readRuntimeAgentTaskSnapshot(event).tasks.length; + return { + content: `${count} background ${count === 1 ? "task" : "tasks"} active.`, + type: "session.status", + }; + } case "message.added": case "message.delta": case "message.completed": diff --git a/pkgs/runtime-events/src/runtime-event-payload.ts b/pkgs/runtime-events/src/runtime-event-payload.ts index 2a47015a..d0f9db2b 100644 --- a/pkgs/runtime-events/src/runtime-event-payload.ts +++ b/pkgs/runtime-events/src/runtime-event-payload.ts @@ -1,3 +1,9 @@ +import { AgentTasksReplacedPayload } from "@mosoo/contracts/session"; +import type { + AgentTaskSnapshot, + AgentTasksReplacedPayload as AgentTasksReplacedPayloadValue, +} from "@mosoo/contracts/session"; +import { parseSchemaValue } from "@mosoo/contracts/validation"; import type { DriverInstanceId, SessionId, SessionRunId } from "@mosoo/id"; import type { RuntimeEventEnvelope, RuntimeEventKind } from "./runtime-event"; @@ -41,6 +47,8 @@ export interface RuntimeEventPermissionRequest { readonly toolKind: string | null; } +export type RuntimeAgentTasksReplacedPayload = AgentTasksReplacedPayloadValue; + export interface RuntimeEventToolCallUpdate { readonly content: string | null; readonly kind: string | null; @@ -157,6 +165,9 @@ export function admitRuntimeEventPayload( const kind = context.kind; switch (kind) { + case "agent.tasks.replaced": { + return readStrictRuntimeAgentTasksReplacedPayload(context, payload); + } case "diagnostic.reported": { const record = requireRuntimeEventPayloadRecord(kind, payload); requireOptionalString(record, "code", kind); @@ -271,6 +282,28 @@ export function admitRuntimeEventPayload( } } +export function readRuntimeAgentTaskSnapshot(event: RuntimeEventEnvelope): AgentTaskSnapshot { + if (event.kind !== "agent.tasks.replaced") { + throw new Error("Runtime agent task snapshot can only be read from agent.tasks.replaced."); + } + + const payload = readStrictRuntimeAgentTasksReplacedPayload( + { + ...(event.driverInstanceId === undefined ? {} : { driverInstanceId: event.driverInstanceId }), + kind: event.kind, + ...(event.runId === undefined ? {} : { runId: event.runId }), + sessionId: event.sessionId, + }, + event.payload, + ); + + return { + driverInstanceId: event.driverInstanceId!, + runId: event.runId!, + tasks: [...payload.tasks], + }; +} + export function readRuntimeEventPayload(event: RuntimeEventEnvelope): RuntimeEventRecord { return isRuntimeEventRecord(event.payload) ? event.payload : {}; } @@ -517,6 +550,22 @@ function readStrictRuntimeToolCallUpdatePayload(payload: unknown): RuntimeEventT }; } +function readStrictRuntimeAgentTasksReplacedPayload( + context: RuntimeEventPayloadAdmissionContext, + payload: unknown, +): RuntimeAgentTasksReplacedPayload { + const kind = "agent.tasks.replaced"; + + if (context.driverInstanceId === undefined) { + throw new Error(`Runtime event ${kind} requires a driver instance ID.`); + } + if (context.runId === undefined) { + throw new Error(`Runtime event ${kind} requires a run ID.`); + } + + return parseSchemaValue(AgentTasksReplacedPayload, payload); +} + export function readRuntimeEventFileChangePath(payload: RuntimeEventRecord): string | null { const directPath = readRuntimeEventString(payload, "path"); diff --git a/pkgs/runtime-events/src/runtime-event.ts b/pkgs/runtime-events/src/runtime-event.ts index 5a6e936a..961185f2 100644 --- a/pkgs/runtime-events/src/runtime-event.ts +++ b/pkgs/runtime-events/src/runtime-event.ts @@ -19,6 +19,7 @@ export const RUNTIME_EVENT_KINDS = [ "account.limits.updated", "account.updated", "agent.task.updated", + "agent.tasks.replaced", "auth.methods.updated", "auth.session.updated", "catalog.updated", @@ -446,6 +447,17 @@ export function parseRuntimeEventEnvelope(value: unknown): RuntimeEventEnvelope const sourceEventId = readOptionalString(value, "sourceEventId"); const traceId = readOptionalString(value, "traceId"); + if (kind === "agent.tasks.replaced") { + if (driverInstanceId === undefined || runId === undefined) { + throw new Error("Runtime event agent.tasks.replaced requires driver instance and run IDs."); + } + if (delivery !== "lossless" || visibility !== "participant") { + throw new Error( + "Runtime event agent.tasks.replaced must be lossless and participant-visible.", + ); + } + } + if (receivedAt !== undefined) { assertRuntimeEventTimestamp(receivedAt, "received time"); } diff --git a/pkgs/runtime-events/src/session-event-projection.ts b/pkgs/runtime-events/src/session-event-projection.ts index 248a781a..b99aad15 100644 --- a/pkgs/runtime-events/src/session-event-projection.ts +++ b/pkgs/runtime-events/src/session-event-projection.ts @@ -9,6 +9,7 @@ import type { AgUiSessionEvent } from "@mosoo/ag-ui-session"; import type { RuntimeEventEnvelope } from "./runtime-event"; import { + readRuntimeAgentTaskSnapshot, readRuntimeEventPermissionRequest, readRuntimeEventMessageDelta, readRuntimeEventMessageKey, @@ -79,6 +80,7 @@ function appSessionRunUpdated(event: RuntimeEventEnvelope): AgUiSessionEvent[] { return [ createServerCustomEvent(MOSOO_CUSTOM_EVENT.sessionRunUpdated.name, { + driverInstanceId: event.driverInstanceId ?? null, lifecycle: payload.lifecycle ?? toRuntimeRunLifecycleStatus(run.status), run, }), @@ -382,6 +384,14 @@ export function appRuntimeEventToAgUiSessionEvents( case "agent.task.updated": { return appAgentTaskUpdated(event); } + case "agent.tasks.replaced": { + return [ + createServerCustomEvent( + MOSOO_CUSTOM_EVENT.sessionTasksReplaced.name, + readRuntimeAgentTaskSnapshot(event), + ), + ]; + } case "runtime.config.updated": case "runtime.driver.updated": case "runtime.provisioning.updated": diff --git a/pkgs/runtime-events/tests/ag-ui-adapter.test.ts b/pkgs/runtime-events/tests/ag-ui-adapter.test.ts index 1546aaaa..18cfdb45 100644 --- a/pkgs/runtime-events/tests/ag-ui-adapter.test.ts +++ b/pkgs/runtime-events/tests/ag-ui-adapter.test.ts @@ -53,6 +53,48 @@ describe("runtime event AG-UI adapter", () => { expect(event.schemaVersion).toBe("2026-05-26"); }); + test("projects task snapshots as one participant state replacement", () => { + const event = createRuntimeEvent({ + driverInstanceId: PLATFORM_ID_FIXTURES.driverInstance, + id: createPlatformId(), + kind: "agent.tasks.replaced", + occurredAt: OCCURRED_AT, + payload: { + tasks: [ + { taskId: "task-1", title: "Inspect" }, + { taskId: "task-2", taskType: "review" }, + ], + }, + runId: PLATFORM_ID_FIXTURES.sessionRun, + sessionId: PLATFORM_ID_FIXTURES.session, + }); + + expect(appRuntimeEventToAgUiSessionEvents(event)).toEqual([ + { + name: MOSOO_CUSTOM_EVENT.sessionTasksReplaced.name, + type: EventType.CUSTOM, + value: { + driverInstanceId: PLATFORM_ID_FIXTURES.driverInstance, + runId: PLATFORM_ID_FIXTURES.sessionRun, + tasks: [ + { taskId: "task-1", title: "Inspect" }, + { taskId: "task-2", taskType: "review" }, + ], + }, + }, + ]); + expect(createProcessDraftFromRuntimeEvent(event)).toEqual({ + content: "2 background tasks active.", + type: "session.status", + }); + expect(parseRuntimeEventEnvelope(event)).toMatchObject({ + delivery: "lossless", + visibility: "participant", + }); + expect(() => parseRuntimeEventEnvelope({ ...event, delivery: "best_effort" })).toThrow(); + expect(() => parseRuntimeEventEnvelope({ ...event, visibility: "owner_debug" })).toThrow(); + }); + test("uses the build context run id as the canonical runtime run id", () => { const event = first( toRuntimeEventInput( @@ -77,6 +119,7 @@ describe("runtime event AG-UI adapter", () => { const started = first( appRuntimeEventToAgUiSessionEvents( createRuntimeEvent({ + driverInstanceId: PLATFORM_ID_FIXTURES.driverInstance, id: createPlatformId(), kind: "run.started", occurredAt: OCCURRED_AT, @@ -93,6 +136,7 @@ describe("runtime event AG-UI adapter", () => { const failed = first( appRuntimeEventToAgUiSessionEvents( createRuntimeEvent({ + driverInstanceId: PLATFORM_ID_FIXTURES.driverInstance, id: createPlatformId(), kind: "run.failed", occurredAt: failedAt, @@ -113,6 +157,7 @@ describe("runtime event AG-UI adapter", () => { name: MOSOO_CUSTOM_EVENT.sessionRunUpdated.name, type: EventType.CUSTOM, value: { + driverInstanceId: PLATFORM_ID_FIXTURES.driverInstance, lifecycle: "RUNNING", run: { id: PLATFORM_ID_FIXTURES.sessionRun, @@ -126,6 +171,7 @@ describe("runtime event AG-UI adapter", () => { name: MOSOO_CUSTOM_EVENT.sessionRunUpdated.name, type: EventType.CUSTOM, value: { + driverInstanceId: PLATFORM_ID_FIXTURES.driverInstance, lifecycle: "IDLE", run: { completedAt: failedAt, @@ -171,6 +217,7 @@ describe("runtime event AG-UI adapter", () => { name: MOSOO_CUSTOM_EVENT.sessionRunUpdated.name, type: EventType.CUSTOM, value: { + driverInstanceId: null, lifecycle: "TERMINATED", run: { completedAt, diff --git a/pkgs/runtime-events/tests/runtime-event-ingress.test.ts b/pkgs/runtime-events/tests/runtime-event-ingress.test.ts index c073d2cf..fdb6d483 100644 --- a/pkgs/runtime-events/tests/runtime-event-ingress.test.ts +++ b/pkgs/runtime-events/tests/runtime-event-ingress.test.ts @@ -175,6 +175,84 @@ describe("runtime event ingress", () => { }); }); + test("admits strict bounded agent task snapshots", () => { + const outcome = ingestRuntimeEventInput( + { + ...createContext(), + driverInstanceId: PLATFORM_ID_FIXTURES.driverInstance, + }, + { + kind: "agent.tasks.replaced", + payload: { + tasks: [ + { + taskId: "🦄".repeat(64), + taskType: "review", + title: "Review the repository", + }, + ], + }, + }, + ); + + expect(outcome).toMatchObject({ + event: { + delivery: "lossless", + driverInstanceId: PLATFORM_ID_FIXTURES.driverInstance, + kind: "agent.tasks.replaced", + runId: PLATFORM_ID_FIXTURES.sessionRun, + visibility: "participant", + }, + status: "accepted", + }); + }); + + test.each([ + ["missing driver", { tasks: [] }, { driverInstanceId: undefined }], + [ + "too many tasks", + { tasks: Array.from({ length: 257 }, (_, index) => ({ taskId: `${index}` })) }, + {}, + ], + ["duplicate IDs", { tasks: [{ taskId: "same" }, { taskId: "same" }] }, {}], + ["oversized ID", { tasks: [{ taskId: "🦄".repeat(65) }] }, {}], + ["empty metadata", { tasks: [{ taskId: "task-1", title: "" }] }, {}], + ["oversized metadata", { tasks: [{ taskId: "task-1", title: "x".repeat(4097) }] }, {}], + ["unknown payload field", { extra: true, tasks: [] }, {}], + ["unknown task field", { tasks: [{ extra: true, taskId: "task-1" }] }, {}], + [ + "oversized aggregate", + { + tasks: Array.from({ length: 256 }, (_, index) => ({ + taskId: `${index}`, + taskType: "x".repeat(4096), + title: "x".repeat(4096), + })), + }, + {}, + ], + ])("rejects %s in agent task snapshots", (_label, payload, contextOverrides) => { + const outcome = ingestRuntimeEventInput( + { + ...createContext(), + driverInstanceId: PLATFORM_ID_FIXTURES.driverInstance, + ...contextOverrides, + }, + { + kind: "agent.tasks.replaced", + payload, + }, + ); + + expect(outcome).toMatchObject({ + rejection: { + code: "malformed_event", + kind: "agent.tasks.replaced", + }, + status: "rejected", + }); + }); + test("owns canonical permission request payload projection", () => { const event = first( toRuntimeEventInput( diff --git a/scripts/check-driver-submodule-cutover.ts b/scripts/check-driver-submodule-cutover.ts index 11595dac..810c24ff 100644 --- a/scripts/check-driver-submodule-cutover.ts +++ b/scripts/check-driver-submodule-cutover.ts @@ -1,6 +1,5 @@ import { spawnSync } from "node:child_process"; import { - cpSync, existsSync, mkdirSync, mkdtempSync, @@ -13,19 +12,6 @@ import { tmpdir } from "node:os"; import { join } from "node:path"; import { fileURLToPath } from "node:url"; -const driverEntries = [ - ".containerignore", - ".github", - ".gitignore", - "Containerfile", - "README.md", - "package.json", - "src", - "tests", - "tsconfig.json", - "tsconfig.types.json", -] as const; - const expectedDriverRepoUrl = "https://github.com/langgenius/mosoo-agent-driver.git"; function fail(message: string): never { @@ -48,28 +34,6 @@ function run(command: string, args: readonly string[], cwd: string): string { return result.stdout; } -function copyDriverTree(sourceRoot: string, destinationRoot: string): void { - mkdirSync(destinationRoot, { recursive: true }); - - for (const entry of driverEntries) { - const source = join(sourceRoot, entry); - - if (!existsSync(source)) { - fail(`driver tree is missing ${entry}.`); - } - - cpSync(source, join(destinationRoot, entry), { recursive: true }); - } -} - -function initializeRepository(path: string): void { - run("git", ["init"], path); - run("git", ["config", "user.email", "driver-submodule-smoke@example.invalid"], path); - run("git", ["config", "user.name", "Driver Submodule Smoke"], path); - run("git", ["add", "."], path); - run("git", ["commit", "-m", "chore(driver): seed standalone smoke repo"], path); -} - function readPackageName(packageRoot: string): string { const packageJson = JSON.parse(readFileSync(join(packageRoot, "package.json"), "utf8")) as { readonly name?: unknown; @@ -121,8 +85,12 @@ try { verifyCurrentMainRepoPin(repoRoot); readPackageName(driverSourceRoot); - copyDriverTree(driverSourceRoot, driverRepo); - initializeRepository(driverRepo); + run("bun", ["install", "--frozen-lockfile"], repoRoot); + run("bun", ["run", "--cwd", "apps/driver", "tc"], repoRoot); + run("git", ["clone", "--no-hardlinks", driverSourceRoot, driverRepo], tempRoot); + run("git", ["switch", "-c", "chore/submodule-smoke"], driverRepo); + run("git", ["config", "user.email", "driver-submodule-smoke@example.invalid"], driverRepo); + run("git", ["config", "user.name", "Driver Submodule Smoke"], driverRepo); mkdirSync(mainRepo, { recursive: true }); run("git", ["init"], mainRepo); @@ -133,11 +101,8 @@ try { JSON.stringify( { name: "mosoo-submodule-smoke", - packageManager: "bun@1.3.14", + packageManager: "bun@1.4.0", private: true, - scripts: { - "driver:checkout-smoke": "bun --cwd apps/driver run tc", - }, type: "module", workspaces: ["apps/*"], }, @@ -196,7 +161,7 @@ try { const clonedSubmodulePath = join(clonedMainRepo, "apps/driver"); symlinkSync(nodeModules, join(clonedSubmodulePath, "node_modules"), "dir"); - run("bun", ["run", "driver:checkout-smoke"], clonedMainRepo); + run("bun", ["run", "--cwd", clonedSubmodulePath, "tc"], clonedMainRepo); writeFileSync(join(driverRepo, ".submodule-smoke-version"), "2\n", "utf8"); run("git", ["add", ".submodule-smoke-version"], driverRepo); From a49d589b92cae2ee6edaa3e43cbebeca26a2b9c7 Mon Sep 17 00:00:00 2001 From: WH-2099 Date: Mon, 31 Aug 2026 19:47:35 +0000 Subject: [PATCH 2/2] refactor(runtime)!: enforce durable protocol v3 boundaries --- .github/workflows/deploy-try.yml | 7 +- CONTRIBUTING.md | 50 +- apps/api/bin/d1-json.ts | 74 + apps/api/bin/deploy-prod.ts | 2034 ++++- apps/api/bin/prod-deploy-lease.ts | 79 + apps/api/bin/prod-schema-guard.ts | 98 +- apps/api/bin/prod-wfp-preflight.ts | 622 ++ apps/api/bin/protocol-v3-cutover.ts | 3138 +++++++ apps/api/package.json | 14 +- .../adapters/durable-objects/sandbox.do.ts | 637 +- .../adapters/durable-objects/session.do.ts | 37 +- .../adapters/graphql/schema.generated.graphql | 2 +- .../src/adapters/graphql/schema/app-schema.ts | 2 +- apps/api/src/index.ts | 1 + .../application/api-command-enqueue.ts | 36 +- .../application/api-command-ledger.ts | 468 +- .../application/api-command-message.ts | 13 + .../application/api-command-payload.ts | 55 + .../application/api-command-processor.ts | 1143 ++- .../application/api-command-workflow.ts | 43 + .../app-deployment-cloudflare-client.ts | 637 +- .../application/app-deployment-detector.ts | 109 +- .../app-deployment-executor.service.ts | 1605 +++- .../application/app-deployment-gateway.ts | 150 + ...eployment-script-reconciliation.service.ts | 629 ++ .../application/app-deployment.service.ts | 302 +- .../personal-access-token.service.ts | 2 +- .../cost/application/cost-rollup.service.ts | 33 +- .../application/cost-usage-event.service.ts | 431 +- ...vironment-package-artifact-backup-store.ts | 826 ++ .../environment-package-artifact-backup.ts | 304 + ...ironment-package-artifact-build.service.ts | 341 +- .../environment-package-artifact.service.ts | 37 +- .../domain/environment-package-artifact.ts | 132 +- .../modules/files/application/file-store.ts | 300 +- .../files/infrastructure/file-record-model.ts | 12 +- .../files/infrastructure/file-record-store.ts | 1 - .../infrastructure/r2-s3-client-types.ts | 1 + .../infrastructure/r2-s3-object-client.ts | 1 + .../modules/mcp/application/mcp-mappers.ts | 6 +- .../public-api/app-agent-bound-ask.service.ts | 1 - .../public-api/app-agent-capability.ts | 28 +- .../public-api-idempotency.service.ts | 2 +- .../public-api/public-thread-create.ts | 1 - .../public-api/public-thread-events.ts | 909 +- .../modules/public-api/public-thread-store.ts | 39 +- .../execution-plane-adapter.ts | 13 +- .../owner-debug-terminal.service.ts | 13 +- .../application/runtime-diagnostic-events.ts | 12 +- ...runtime-state-operation-event-authority.ts | 195 + .../runtime-state-operation-events.ts | 33 +- .../runtime-state-operation-execution.ts | 18 +- .../runtime-state-operation-phases.ts | 132 +- .../runtime-state-operation-subjects.ts | 4 + .../runtime-state-operation-target-events.ts | 602 +- ...runtime-state-operation-target-recovery.ts | 130 +- .../runtime-state-operation-target-store.ts | 966 +- .../session-lifecycle-transition.service.ts | 23 - .../session-runs/cancel-run.service.ts | 155 +- .../dispatch-queued-run.service.ts | 33 +- .../dispatch-run-cleanup.service.ts | 4 + .../session-runs/dispatch-run.service.ts | 19 +- .../prewarm-agent-session-runtime.service.ts | 36 +- .../resolve-permission-request.service.ts | 9 +- .../send-agent-session-events.service.ts | 2 +- .../session-permission-decision.service.ts | 72 +- .../session-run-state.repository.ts | 36 +- .../session-run-terminal-failure.service.ts | 340 +- .../session-run-view-events.service.ts | 50 +- .../stale-run-reconciliation.service.ts | 48 +- .../terminal-run-reconciliation.service.ts | 483 +- .../runtime/domain/runtime-kind-policy.ts | 11 +- .../runtime-subject-lifecycle.machine.ts | 84 +- .../domain/sandbox-network-constraints.ts | 12 + .../domain/session-run-lifecycle.machine.ts | 88 +- .../domain/session-run-terminal-event-id.ts | 14 +- .../assistant-message-projection.ts | 185 +- .../infrastructure/driver-instance/client.ts | 34 +- .../driver-instance/commands.ts | 14 +- .../completed-run-commit.repository.ts | 2864 ++++++ .../driver-instance/connections.ts | 1 + .../driver-instance/debug-resume-snapshot.ts | 5 - .../infrastructure/driver-instance/do.ts | 429 +- .../driver-event-canonicalization.ts | 127 + .../driver-instance/driver-event-receipts.ts | 90 - .../driver-instance-record.repository.ts | 421 +- .../driver-instance-token.repository.ts | 49 +- .../driver-instance/event-link-assertion.ts | 21 +- .../driver-instance/event-persistence.ts | 506 +- .../driver-instance/event-projection.ts | 18 - .../driver-instance/event-types.ts | 22 +- .../infrastructure/driver-instance/events.ts | 574 +- .../infrastructure/driver-instance/http.ts | 94 +- .../driver-instance/lifecycle.ts | 100 +- .../live-driver-instance.repository.ts | 10 +- .../driver-instance/maintenance.ts | 42 +- .../driver-instance/rpc-command-controller.ts | 114 +- .../rpc-controller-dependencies.ts | 3 +- .../driver-instance/rpc-controller.ts | 33 +- .../rpc-event-ingestion-controller.ts | 557 +- .../rpc-handshake-controller.ts | 89 +- .../rpc-run-terminal-controller.ts | 90 +- .../driver-instance/rpc-wire.ts | 343 +- .../infrastructure/driver-instance/rpc.ts | 31 +- .../driver-instance/run-transitions.ts | 12 - .../runtime-artifact-attempt.repository.ts | 896 ++ .../runtime-artifact-staging.ts | 665 ++ .../runtime-event-compaction.ts | 197 - .../runtime-event-persistence-compactor.ts | 440 - .../runtime-event-replay-filter.ts | 208 - .../runtime-session-outputs.ts | 91 +- .../driver-instance/runtime-state-store.ts | 106 +- .../driver-instance/runtime-state.ts | 549 +- .../driver-instance/sandbox-binding.ts | 6 - .../session-link.repository.ts | 52 +- .../session-viewer-event-delivery-buffer.ts | 265 +- .../infrastructure/driver-instance/sockets.ts | 108 +- .../infrastructure/driver-instance/state.ts | 29 +- .../driver-instance/terminal-driver-events.ts | 347 +- .../driver-instance/terminal-run-release.ts | 886 +- .../driver-instance/terminal-runtime-lease.ts | 28 +- .../terminal-state-coordinator.ts | 267 +- .../infrastructure/driver-session-startup.ts | 6 +- .../infrastructure/driver-session-state.ts | 19 +- .../driver-session-stop.service.ts | 470 +- .../infrastructure/driver-session.service.ts | 77 +- .../sandbox-execution-plane-adapter.ts | 167 +- .../native-resume-ref.repository.ts | 181 +- .../runtime-driver-files.service.ts | 7 +- .../runtime-driver-process-cleanup.ts | 23 + .../runtime-driver-provisioning.service.ts | 70 +- .../runtime-environment-artifact.ts | 5 +- .../runtime-sandbox-provisioning.types.ts | 3 + .../lease-ownership-renewal.ts | 34 + .../runtime-conversation-session-store.ts | 447 +- .../runtime-provisioning-cleanup.service.ts | 160 + .../runtime-provisioning-lease-store.ts | 841 ++ .../runtime-run-lease-store.ts | 425 +- .../runtime-subject-driver-stop.ts | 34 +- .../runtime-subject-errors.ts | 9 + .../runtime-subject-lifecycle.service.ts | 378 +- .../runtime-subject-maintenance-store.ts | 121 +- .../runtime-subject-maintenance.service.ts | 305 +- .../runtime-subject-operations.service.ts | 516 +- .../runtime-subject-platform.ts | 133 +- .../runtime-subject-record-store.ts | 550 +- .../runtime-subject-recycle.service.ts | 124 +- .../runtime-subject-store-queries.ts | 20 +- .../runtime-subject-store.ts | 12 +- .../runtime-subject-store.types.ts | 48 +- .../infrastructure/sandbox-backup-platform.ts | 172 +- .../infrastructure/sandbox-backup-pruning.ts | 6 +- .../sandbox-backup-reconciliation.service.ts | 520 ++ .../infrastructure/sandbox-backup-store.ts | 1386 ++- .../infrastructure/sandbox-backup.service.ts | 440 +- .../infrastructure/sandbox-file-bytes.ts | 52 +- .../runtime/infrastructure/sandbox-handles.ts | 49 + .../infrastructure/sandbox-session.service.ts | 1 + .../sandbox-conversation-session-delete.ts | 46 +- .../sandbox-conversation-session-platform.ts | 41 +- .../sandbox-conversation-session.service.ts | 238 +- .../sandbox-session/sandbox-session.types.ts | 5 + .../session-resource-mount.service.ts | 117 +- .../external-tool-effect-store.repository.ts | 561 +- .../runtime-command-record.mapper.ts | 104 +- .../runtime-command-store.repository.ts | 1410 ++- .../runtime-command-transition.ts | 3 +- .../session-run-admission.repository.ts | 268 +- .../session-run-read.repository.ts | 1 + .../session-runs/session-run-row.mapper.ts | 11 +- .../session-run-store.repository.ts | 1 - .../session-run-write.repository.ts | 364 +- .../application/session-cleanup.service.ts | 401 +- .../session-event-write.service.ts | 42 +- .../session-lifecycle-mutation.service.ts | 510 +- .../application/session-live-state.service.ts | 14 +- .../session-message-query.service.ts | 7 +- .../session-message-write.service.ts | 6 - .../application/session-model-call.service.ts | 4 - .../session-process-events.service.ts | 207 +- .../session-runtime-recovery-query.service.ts | 35 +- .../application/session-title.service.ts | 154 +- .../session-viewer-events.service.ts | 5 +- .../domain/provider-private-markup.ts | 6 +- .../sessions/domain/session-cleanup-plan.ts | 102 +- .../domain/session-event-stream-fold.ts | 471 +- .../domain/session-event-tool-content.ts | 35 + .../session-message-projection-parser.ts | 13 + .../domain/session-runtime-event-authority.ts | 33 + .../session-runtime-event-projection.ts | 181 +- .../session-terminal-event-authority.ts | 55 + .../session-agent-task-snapshot.repository.ts | 74 +- ...session-message-event-stream.repository.ts | 301 + .../session-message-reference.repository.ts | 1310 +++ .../session-message-snapshot.repository.ts | 16 +- .../session-model-call.repository.ts | 536 +- .../session-runtime-event-store.repository.ts | 677 +- .../session-runtime-event-store.types.ts | 44 +- ...sion-viewer-event-projection.repository.ts | 459 +- ...session-viewer-live-snapshot.repository.ts | 74 +- .../sessions/infrastructure/session/client.ts | 19 +- .../sessions/infrastructure/session/do.ts | 29 +- .../session/viewer-live-state.ts | 16 +- .../session/viewer-permissions.ts | 2 +- .../session/viewer-socket-hub.ts | 225 +- .../session/viewer-socket-state-sync.ts | 33 +- .../infrastructure/session/viewer-socket.ts | 5 + .../application/skill-package.shared.ts | 2 +- .../application/viewer-context.service.ts | 13 +- .../cloudflare/app-deployment-workflow.ts | 15 + .../platform/cloudflare/create-api-worker.ts | 29 +- .../src/platform/cloudflare/worker-types.ts | 5 + apps/api/src/platform/db/drizzle.ts | 4 +- apps/api/src/shared/bytes.ts | 33 +- apps/api/src/shared/shell.ts | 3 + .../tests/agent-package-file-import.test.ts | 2 +- .../agent-runtime-events-projection.test.ts | 154 +- apps/api/tests/api-command-queue.test.ts | 2043 ++++- .../api/tests/api-driver-boundary-fixtures.ts | 3 + apps/api/tests/api-driver-boundary.test.ts | 691 +- .../app-agent-bound-run-revocation.test.ts | 61 +- ...p-agent-capability-revocation-http.test.ts | 65 +- .../app-deployment-cloudflare-client.test.ts | 85 +- .../api/tests/app-deployment-detector.test.ts | 34 +- apps/api/tests/app-deployment-gateway.test.ts | 402 + ...p-deployment-script-reconciliation.test.ts | 809 ++ apps/api/tests/app-deployment-service.test.ts | 1993 +++- apps/api/tests/app-overview.test.ts | 123 +- .../tests/bound-agent-idempotency.e2e.test.ts | 209 +- apps/api/tests/bound-capability-fixtures.ts | 123 +- ...d-capability-public-thread-api.e2e.test.ts | 119 +- .../tests/cattle-continuation-restore.test.ts | 11 + .../tests/cattle-terminal-checkpoint.test.ts | 398 +- ...loudflare-sandbox-network-contract.test.ts | 4 +- .../tests/cost-ledger-reconciliation.test.ts | 23 +- apps/api/tests/cost-usage-event.test.ts | 170 +- apps/api/tests/cost-usage-idempotency.test.ts | 258 + .../tests/driver-command-terminal-ack.test.ts | 100 +- .../driver-event-batch-admission.test.ts | 63 + .../tests/driver-finalization-repair.test.ts | 1177 ++- apps/api/tests/driver-instance-http.test.ts | 87 +- apps/api/tests/driver-instance-record.test.ts | 340 +- .../driver-instance-runtime-state.test.ts | 199 + .../api/tests/driver-instance-sockets.test.ts | 89 +- apps/api/tests/driver-llm-proxy-route.test.ts | 18 +- .../driver-log-pre-hello-buffering.test.ts | 2 +- apps/api/tests/driver-session-link.test.ts | 8 +- apps/api/tests/driver-session-ready.test.ts | 61 +- apps/api/tests/driver-session-stop.test.ts | 220 +- .../tests/driver-skill-package-route.test.ts | 8 +- .../driver-terminal-state-coordinator.test.ts | 479 + .../environment-package-artifact.test.ts | 77 +- .../external-tool-effect-migration.test.ts | 2327 +++++ .../tests/external-tool-effect-store.test.ts | 711 ++ apps/api/tests/file-upload-access.test.ts | 1 + apps/api/tests/file-upload-recovery.test.ts | 1 + apps/api/tests/helpers/api-test-fixture.ts | 464 +- apps/api/tests/helpers/drizzle-migrations.ts | 1 + .../helpers/public-api-http-core-schema.sql | 241 - .../public-api-http-runtime-schema.sql | 352 - .../helpers/public-api-http-test-fixture.ts | 162 +- .../tests/helpers/runtime-output-sandbox.ts | 104 + apps/api/tests/helpers/sqlite-d1.ts | 14 +- .../api/tests/lease-ownership-renewal.test.ts | 44 + apps/api/tests/native-resume-ref.test.ts | 72 + apps/api/tests/owner-debug-terminal.test.ts | 30 +- apps/api/tests/pet-stranded-recycle.test.ts | 100 +- apps/api/tests/prod-deploy-lease.test.ts | 209 + apps/api/tests/prod-schema-guard.test.ts | 437 +- apps/api/tests/prod-wfp-preflight.test.ts | 511 ++ .../tests/production-deploy-safety.test.ts | 2741 +++++- apps/api/tests/public-thread-api-fixtures.ts | 59 +- apps/api/tests/public-thread-api.e2e.test.ts | 2412 ++++- apps/api/tests/rpc-wire-v3.test.ts | 585 ++ .../tests/runtime-artifact-migration.test.ts | 286 + apps/api/tests/runtime-command-store.test.ts | 655 +- .../runtime-conversation-idle-sweep.test.ts | 81 +- ...untime-conversation-session-record.test.ts | 30 +- ...untime-event-persistence-compactor.test.ts | 551 -- .../runtime-final-output-ingestion.test.ts | 5302 ++++++++++- ...peration-ready-authority-migration.test.ts | 188 + .../tests/runtime-provisioning-lease.test.ts | 799 ++ apps/api/tests/runtime-session-link.test.ts | 25 +- .../api/tests/runtime-session-outputs.test.ts | 650 +- .../runtime-state-operation-execution.test.ts | 40 + .../runtime-state-operation-phases.test.ts | 83 +- .../runtime-state-operation-scope.test.ts | 6 +- ...time-state-operation-target-events.test.ts | 975 +- .../runtime-subject-activation-record.test.ts | 11 +- .../tests/runtime-subject-lifecycle.test.ts | 1051 ++- .../tests/runtime-subject-maintenance.test.ts | 505 +- .../api/tests/runtime-subject-network.test.ts | 2 +- ...ject-operation-authority-migration.test.ts | 959 ++ .../api/tests/runtime-subject-recycle.test.ts | 258 +- .../tests/runtime-subject-run-lease.test.ts | 197 +- .../tests/sandbox-backup-lifecycle.test.ts | 3762 ++++++++ apps/api/tests/sandbox-backup-pruning.test.ts | 319 +- apps/api/tests/sandbox-backup-staging.test.ts | 365 + .../sandbox-conversation-session.test.ts | 315 +- .../tests/sandbox-runtime-incarnation.test.ts | 742 ++ .../tests/send-agent-session-events.test.ts | 8 + .../tests/session-event-stream-fold.test.ts | 711 +- .../tests/session-lifecycle-mutation.test.ts | 699 +- apps/api/tests/session-message-store.test.ts | 14 +- .../tests/session-model-call-identity.test.ts | 268 +- apps/api/tests/session-process-events.test.ts | 471 +- apps/api/tests/session-resource-files.test.ts | 57 +- apps/api/tests/session-resource-mount.test.ts | 11 +- .../session-run-admission-atomicity.test.ts | 260 +- apps/api/tests/session-run-cancel.test.ts | 354 +- apps/api/tests/session-run-lifecycle.test.ts | 490 +- apps/api/tests/session-run-read.test.ts | 9 +- .../tests/session-run-reconciliation.test.ts | 127 +- .../session-run-skill-persistence.test.ts | 3 +- apps/api/tests/session-run-state.test.ts | 40 +- .../session-run-terminal-failure.test.ts | 813 +- .../tests/session-runtime-event-store.test.ts | 584 +- apps/api/tests/session-title-mutation.test.ts | 58 + ...ssion-viewer-event-delivery-buffer.test.ts | 155 +- .../session-viewer-socket-state-order.test.ts | 453 +- apps/api/tests/session-viewer-state.test.ts | 208 +- apps/api/tests/skill-package-snapshot.test.ts | 2 +- apps/api/tests/sqlite-d1.test.ts | 24 + .../terminal-run-release-recovery.test.ts | 325 + .../wrangler-d1-migration-atomicity.test.ts | 510 ++ apps/api/wrangler.toml | 33 + apps/driver | 2 +- apps/web/package.json | 12 +- .../session-stream/session-stream-actions.ts | 7 +- .../session-stream/session-stream-socket.ts | 61 +- apps/web/src/features/help/help-menu.tsx | 8 +- apps/web/src/gql/graphql.ts | 2 +- .../src/routes/agent/agent-detail.route.tsx | 40 +- .../agent/components/editor/form-view.tsx | 23 +- .../agent/components/editor/use-auto-save.ts | 7 +- .../routes/agent/components/terminal-mode.tsx | 78 +- .../use-agent-session-panel-model.ts | 59 +- .../deploy/deploy-console-data.ts | 2 +- .../app-overview/deploy/local-preview-url.ts | 23 +- .../web/src/routes/cost/cost-models-panel.tsx | 9 +- .../skills/skill-detail-dialog.tsx | 7 +- .../integrations/skills/skills-sh-catalog.tsx | 15 +- .../web/src/routes/org/org-settings.route.tsx | 13 +- .../web/src/routes/threads/model/read-sync.ts | 8 +- .../shared/ui/session-events/drawer-core.tsx | 18 +- apps/web/tests/deployment-status.test.ts | 8 + apps/web/tests/session-stream-socket.test.tsx | 128 + bun.lock | 566 +- config/bun-script-types.d.ts | 1 + docs/architecture.md | 34 +- docs/production-deploy-verification.md | 536 +- package.json | 16 +- pkgs/ag-ui-session/package.json | 2 +- .../ag-ui-session/src/ag-ui-session-events.ts | 2 + .../src/custom-event-registry.ts | 5 +- pkgs/ag-ui-session/src/custom-event-schema.ts | 18 + pkgs/ag-ui-session/src/custom-event-values.ts | 15 + pkgs/ag-ui-session/src/index.ts | 1 + .../src/live-state-custom.reducer.ts | 39 +- .../src/live-state-message-core.reducer.ts | 38 +- .../src/live-state-message-text.reducer.ts | 3 + .../src/live-state-message-tool.reducer.ts | 189 +- .../src/live-state-message.reducer.ts | 10 +- pkgs/ag-ui-session/src/live-state.reducer.ts | 60 +- pkgs/ag-ui-session/src/live-state.ts | 2 + .../src/session-live-state-schema.ts | 2 + .../tests/live-state.reducer.test.ts | 363 +- pkgs/agent-package/package.json | 2 +- pkgs/contracts/package.json | 2 +- pkgs/contracts/src/app/app.contract.ts | 2 +- .../src/runtime/driver-instance.contract.ts | 2 +- .../runtime/external-tool-effect.contract.ts | 95 +- .../src/runtime/runtime-command.contract.ts | 117 +- .../contracts/src/runtime/sandbox.contract.ts | 5 +- .../src/session/session-run.contract.ts | 25 +- .../contracts/src/session/session.contract.ts | 35 +- .../src/validation/primitives.contract.ts | 4 +- pkgs/contracts/tests/owner-boundaries.test.ts | 307 +- .../db/drizzle/0013_durable-mcp-effect-v3.sql | 987 ++ .../0014_session-event-stream-identity.sql | 755 ++ .../0015_session-cleanup-operation.sql | 8 + .../0016_durable-event-side-effects.sql | 161 + ...017_terminal-reconciliation-scheduling.sql | 12 + ...0018_runtime-operation-ready-authority.sql | 14 + ...19_runtime-subject-operation-authority.sql | 1391 +++ .../0020_sandbox-backup-object-authority.sql | 738 ++ pkgs/db/drizzle/meta/0013_snapshot.json | 6634 ++++++++++++++ pkgs/db/drizzle/meta/0014_snapshot.json | 6768 ++++++++++++++ pkgs/db/drizzle/meta/0015_snapshot.json | 6821 ++++++++++++++ pkgs/db/drizzle/meta/0016_snapshot.json | 7194 +++++++++++++++ pkgs/db/drizzle/meta/0017_snapshot.json | 7217 +++++++++++++++ pkgs/db/drizzle/meta/0018_snapshot.json | 7228 +++++++++++++++ pkgs/db/drizzle/meta/0019_snapshot.json | 7922 ++++++++++++++++ pkgs/db/drizzle/meta/0020_snapshot.json | 8116 +++++++++++++++++ pkgs/db/drizzle/meta/_journal.json | 56 + pkgs/db/package.json | 6 +- pkgs/db/scripts/check-schema.ts | 51 + pkgs/db/scripts/drizzle-migrations.ts | 128 + pkgs/db/src/deploy-schema-guard.ts | 841 ++ pkgs/db/src/schema/api-command.schema.ts | 10 +- pkgs/db/src/schema/app.schema.ts | 71 +- pkgs/db/src/schema/environment.schema.ts | 116 +- pkgs/db/src/schema/file.schema.ts | 115 +- pkgs/db/src/schema/runtime.schema.ts | 223 +- pkgs/db/src/schema/session/core.schema.ts | 55 + pkgs/db/src/schema/session/events.schema.ts | 70 +- pkgs/db/src/schema/session/runs.schema.ts | 14 + pkgs/db/src/schema/usage.schema.ts | 4 + pkgs/development-auth/package.json | 2 +- pkgs/effects/package.json | 2 +- pkgs/id/package.json | 2 +- pkgs/observability/package.json | 2 +- .../src/metadata/log-metadata.ts | 2 +- pkgs/public-api-client/package.json | 2 +- pkgs/runtime-catalog/package.json | 2 +- pkgs/runtime-catalog/src/runtime-catalog.ts | 3 +- pkgs/runtime-events/package.json | 2 +- pkgs/runtime-events/src/process-draft.ts | 20 +- .../src/runtime-event-payload.ts | 141 +- pkgs/runtime-events/src/runtime-event.ts | 155 +- .../src/session-event-projection.ts | 167 +- .../tests/ag-ui-adapter.test.ts | 265 +- .../tests/runtime-event-ingress.test.ts | 333 +- pkgs/session-policy/package.json | 2 +- pkgs/skill-package/package.json | 2 +- 425 files changed, 146891 insertions(+), 16330 deletions(-) create mode 100644 apps/api/bin/d1-json.ts create mode 100644 apps/api/bin/prod-deploy-lease.ts create mode 100644 apps/api/bin/prod-wfp-preflight.ts create mode 100644 apps/api/bin/protocol-v3-cutover.ts create mode 100644 apps/api/src/modules/api-command/application/api-command-workflow.ts create mode 100644 apps/api/src/modules/apps/application/app-deployment-gateway.ts create mode 100644 apps/api/src/modules/apps/application/app-deployment-script-reconciliation.service.ts create mode 100644 apps/api/src/modules/environments/application/environment-package-artifact-backup-store.ts create mode 100644 apps/api/src/modules/environments/application/environment-package-artifact-backup.ts create mode 100644 apps/api/src/modules/runtime/application/runtime-state-operation-event-authority.ts delete mode 100644 apps/api/src/modules/runtime/application/session-lifecycle-transition.service.ts create mode 100644 apps/api/src/modules/runtime/infrastructure/driver-instance/completed-run-commit.repository.ts create mode 100644 apps/api/src/modules/runtime/infrastructure/driver-instance/driver-event-canonicalization.ts delete mode 100644 apps/api/src/modules/runtime/infrastructure/driver-instance/driver-event-receipts.ts delete mode 100644 apps/api/src/modules/runtime/infrastructure/driver-instance/run-transitions.ts create mode 100644 apps/api/src/modules/runtime/infrastructure/driver-instance/runtime-artifact-attempt.repository.ts create mode 100644 apps/api/src/modules/runtime/infrastructure/driver-instance/runtime-artifact-staging.ts delete mode 100644 apps/api/src/modules/runtime/infrastructure/driver-instance/runtime-event-compaction.ts delete mode 100644 apps/api/src/modules/runtime/infrastructure/driver-instance/runtime-event-persistence-compactor.ts delete mode 100644 apps/api/src/modules/runtime/infrastructure/driver-instance/runtime-event-replay-filter.ts delete mode 100644 apps/api/src/modules/runtime/infrastructure/driver-instance/sandbox-binding.ts create mode 100644 apps/api/src/modules/runtime/infrastructure/runtime-subject-lifecycle/lease-ownership-renewal.ts create mode 100644 apps/api/src/modules/runtime/infrastructure/runtime-subject-lifecycle/runtime-provisioning-cleanup.service.ts create mode 100644 apps/api/src/modules/runtime/infrastructure/runtime-subject-lifecycle/runtime-provisioning-lease-store.ts create mode 100644 apps/api/src/modules/runtime/infrastructure/sandbox-backup-reconciliation.service.ts delete mode 100644 apps/api/src/modules/sessions/application/session-message-write.service.ts delete mode 100644 apps/api/src/modules/sessions/application/session-model-call.service.ts create mode 100644 apps/api/src/modules/sessions/domain/session-event-tool-content.ts create mode 100644 apps/api/src/modules/sessions/domain/session-runtime-event-authority.ts create mode 100644 apps/api/src/modules/sessions/domain/session-terminal-event-authority.ts create mode 100644 apps/api/src/modules/sessions/infrastructure/session-message-event-stream.repository.ts create mode 100644 apps/api/src/modules/sessions/infrastructure/session-message-reference.repository.ts create mode 100644 apps/api/src/platform/cloudflare/app-deployment-workflow.ts create mode 100644 apps/api/src/shared/shell.ts create mode 100644 apps/api/tests/app-deployment-gateway.test.ts create mode 100644 apps/api/tests/app-deployment-script-reconciliation.test.ts create mode 100644 apps/api/tests/driver-event-batch-admission.test.ts create mode 100644 apps/api/tests/driver-instance-runtime-state.test.ts create mode 100644 apps/api/tests/driver-terminal-state-coordinator.test.ts create mode 100644 apps/api/tests/external-tool-effect-migration.test.ts create mode 100644 apps/api/tests/external-tool-effect-store.test.ts create mode 100644 apps/api/tests/helpers/drizzle-migrations.ts delete mode 100644 apps/api/tests/helpers/public-api-http-core-schema.sql delete mode 100644 apps/api/tests/helpers/public-api-http-runtime-schema.sql create mode 100644 apps/api/tests/helpers/runtime-output-sandbox.ts create mode 100644 apps/api/tests/lease-ownership-renewal.test.ts create mode 100644 apps/api/tests/prod-deploy-lease.test.ts create mode 100644 apps/api/tests/prod-wfp-preflight.test.ts create mode 100644 apps/api/tests/rpc-wire-v3.test.ts create mode 100644 apps/api/tests/runtime-artifact-migration.test.ts delete mode 100644 apps/api/tests/runtime-event-persistence-compactor.test.ts create mode 100644 apps/api/tests/runtime-operation-ready-authority-migration.test.ts create mode 100644 apps/api/tests/runtime-provisioning-lease.test.ts create mode 100644 apps/api/tests/runtime-subject-operation-authority-migration.test.ts create mode 100644 apps/api/tests/sandbox-backup-lifecycle.test.ts create mode 100644 apps/api/tests/sandbox-backup-staging.test.ts create mode 100644 apps/api/tests/sandbox-runtime-incarnation.test.ts create mode 100644 apps/api/tests/sqlite-d1.test.ts create mode 100644 apps/api/tests/terminal-run-release-recovery.test.ts create mode 100644 apps/api/tests/wrangler-d1-migration-atomicity.test.ts create mode 100644 apps/web/tests/session-stream-socket.test.tsx create mode 100644 pkgs/db/drizzle/0013_durable-mcp-effect-v3.sql create mode 100644 pkgs/db/drizzle/0014_session-event-stream-identity.sql create mode 100644 pkgs/db/drizzle/0015_session-cleanup-operation.sql create mode 100644 pkgs/db/drizzle/0016_durable-event-side-effects.sql create mode 100644 pkgs/db/drizzle/0017_terminal-reconciliation-scheduling.sql create mode 100644 pkgs/db/drizzle/0018_runtime-operation-ready-authority.sql create mode 100644 pkgs/db/drizzle/0019_runtime-subject-operation-authority.sql create mode 100644 pkgs/db/drizzle/0020_sandbox-backup-object-authority.sql create mode 100644 pkgs/db/drizzle/meta/0013_snapshot.json create mode 100644 pkgs/db/drizzle/meta/0014_snapshot.json create mode 100644 pkgs/db/drizzle/meta/0015_snapshot.json create mode 100644 pkgs/db/drizzle/meta/0016_snapshot.json create mode 100644 pkgs/db/drizzle/meta/0017_snapshot.json create mode 100644 pkgs/db/drizzle/meta/0018_snapshot.json create mode 100644 pkgs/db/drizzle/meta/0019_snapshot.json create mode 100644 pkgs/db/drizzle/meta/0020_snapshot.json create mode 100644 pkgs/db/scripts/check-schema.ts create mode 100644 pkgs/db/scripts/drizzle-migrations.ts create mode 100644 pkgs/db/src/deploy-schema-guard.ts diff --git a/.github/workflows/deploy-try.yml b/.github/workflows/deploy-try.yml index 4eddaa8d..22db4e1d 100644 --- a/.github/workflows/deploy-try.yml +++ b/.github/workflows/deploy-try.yml @@ -78,19 +78,22 @@ jobs: env: CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }} - run: ../../node_modules/.bin/vp exec wrangler deploy --env prod --minify --dry-run + run: ../../node_modules/.bin/vp exec wrangler deploy --env prod --minify --dry-run --experimental-provision=false - name: Dry-run Web Worker working-directory: apps/web env: CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }} - run: ../../node_modules/.bin/vp exec wrangler deploy --env prod --dry-run + run: ../../node_modules/.bin/vp exec wrangler deploy --env prod --dry-run --experimental-provision=false - name: Deploy env: CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }} + CLOUDFLARE_ZONE_ID: ${{ secrets.CLOUDFLARE_ZONE_ID }} + MOSOO_PROTOCOL_V3_SMOKE_AGENT_ID: ${{ vars.MOSOO_PROTOCOL_V3_SMOKE_AGENT_ID }} + MOSOO_PROTOCOL_V3_SMOKE_TOKEN: ${{ secrets.MOSOO_PROTOCOL_V3_SMOKE_TOKEN }} VITE_MOSOO_DEPLOYMENT_MODE: cloud VITE_MOSOO_ENVIRONMENT: production VITE_POSTHOG_API_HOST: https://us.i.posthog.com diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index df8d0df0..4e6b2a83 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -52,7 +52,7 @@ Minimum generated-file rules: Required tools: -- `bun >= 1.4.0-canary.1` with text `bun.lock` support. +- `bun >= 1.4.0` with text `bun.lock` support. - `just >= 1.51` The repository already pins Vite Plus and Git hook tooling dependencies. Human-facing repository operations use `just`; the `justfile` delegates to `bun run`, which resolves the pinned local Vite Plus binary after `bun install`. A global Vite Plus install is optional for direct shell use: `curl -fsSL https://vite.plus | bash`. @@ -164,6 +164,12 @@ just test-file apps/api/tests/session-run-cancel.test.ts mosoo uses generated Drizzle migrations for local and production D1: - The schema source of truth is `pkgs/db/src/schema/**`. +- `just check` generates that schema in memory with `drizzle-kit/api` and + requires it to match the latest checked-in snapshot without writing temp + migration output. +- The production schema test also applies the complete migration chain to an + in-memory SQLite database and requires its managed catalog to match that same + snapshot. - The chain is append-only. Once a migration SQL file, Drizzle snapshot, or journal entry is merged or applied, do not modify, delete, rename, or regenerate it. @@ -191,14 +197,40 @@ D1 state and reapplies the chain. Never delete or regenerate established files under `pkgs/db/drizzle`. Production D1 is not reset during deploy. `just deploy-api` runs -`apps/api/bin/deploy-prod.ts`, whose first remote action is applying pending D1 -migrations. It then verifies that every table in the latest Drizzle snapshot -exists in production — the DEPLOY-D1-001 missing-table guard — ensures the -environment-artifact queue, builds the Driver, and deploys the API Worker. -The guard does not compare columns, indexes, constraints, or extra live tables. +`apps/api/bin/deploy-prod.ts`, which builds the Driver and dry-runs the API +Worker before its first remote mutation. It then acquires the durable D1 deploy +lease before it ensures production queues or applies ordinary pending +migrations. Every D1, Queue, API Worker, or smoke mutation renews and verifies +that exact owner before and after the mutation through an independent final D1 +read; the canonical lease table may have no triggers. The protocol v3 breaking migrations +use the one-shot paused-queue, D1 admission-gate, drain, legacy terminal +integrity preflight, Time Travel bookmark, deploy, live Driver handshake smoke, +durable queue-resume phase, and Queue API readback sequence documented in the +production runbook. +Migration `0013` is allowed to proceed only when its read-only inventory reports zero historical loss or conflict candidates, and the migration repeats that guard before its first rewrite. +Run `bun apps/api/bin/deploy-prod.ts --protocol-v3-lossy-migration-inventory` before scheduling a release with `0013` pending. +The legacy preflight requires a provably collision-free terminal identity and +fully committed Run, Session, assistant, cursor, and permission state; migration +`0014` normalizes only that proven identity and never guesses ambiguous history. +The migration itself repeats those fail-closed checks and requires a short-lived +authorization bound to the bookmark, exact candidate manifest, and current +deploy lease owner, so applying `0014` directly cannot bypass the cutover. +Run `bun apps/api/bin/deploy-prod.ts --protocol-v3-legacy-inventory` as the +read-only human gate before scheduling this one-shot production cutover. +After the breaking migration is recorded, deployment recovery is roll-forward +only with the exact same v3 release. +The bookmark is an emergency destructive backup, not a routine rollback: a D1 +Time Travel restore loses every later D1 write and does not restore Durable +Object storage. +After migration, the script compares every managed table's columns, primary key, +defaults, nullability, named indexes and partial predicates, foreign keys, CHECK +expressions, autoincrement state, and exact migration-owned trigger definitions +with the latest migration contract before it deploys the API Worker. +Unknown extra application tables fail closed. The eight retained Channels and +WeChat tables are explicitly unmanaged legacy data because #577 removed their +runtime subsystem without an approved destructive data migration. No other automated migration gate exists today: there is no trusted-Git-range -check, no clean-worktree check, and no dry-run inside the deploy script. Run -`just check` and the +or clean-worktree check. Run `just check` and the [Production Deploy Verification](./docs/production-deploy-verification.md) runbook before deploying. @@ -407,7 +439,7 @@ just deploy-web # Web only — runs no gate; does not touch D1 Only `just deploy` runs the repository gate before publishing; `just deploy-api` and `just deploy-web` publish directly. The API deploy applies pending remote D1 -migrations as its first remote action (see +migrations only after its local Driver build and API dry-run (see [Database And Migrations](#database-and-migrations)). API production config lives in `apps/api/wrangler.toml`; web production config lives in `apps/web/wrangler.toml`. Cloudflare routes send `cloud.mosoo.ai/api/*` to the API Worker and `cloud.mosoo.ai/*` to the console Web Worker. The legacy console host redirects Web traffic to `cloud.mosoo.ai` but keeps `try.mosoo.ai/api/*` as a direct compatibility route. The public landing page and blog on `mosoo.ai/*` are owned by `langgenius/mosoo-website`. diff --git a/apps/api/bin/d1-json.ts b/apps/api/bin/d1-json.ts new file mode 100644 index 00000000..fd76049b --- /dev/null +++ b/apps/api/bin/d1-json.ts @@ -0,0 +1,74 @@ +export type D1JsonRow = Readonly>; + +function parseJsonArrayAt(raw: string, start: number): unknown { + let depth = 0; + let escaped = false; + let inString = false; + for (let index = start; index < raw.length; index += 1) { + const character = raw[index]; + if (inString) { + if (escaped) escaped = false; + else if (character === "\\") escaped = true; + else if (character === '"') inString = false; + continue; + } + if (character === '"') inString = true; + else if (character === "[") depth += 1; + else if (character === "]") { + depth -= 1; + if (depth === 0) { + try { + return JSON.parse(raw.slice(start, index + 1)); + } catch { + return null; + } + } + } + } + return null; +} + +export function parseD1JsonResults(raw: string): D1JsonRow[][] { + for (let start = raw.indexOf("["); start !== -1; start = raw.indexOf("[", start + 1)) { + const value = parseJsonArrayAt(raw, start); + if (!Array.isArray(value) || value.length === 0) continue; + + const statements: D1JsonRow[][] = []; + let valid = true; + for (const result of value) { + if ( + typeof result !== "object" || + result === null || + !("results" in result) || + !("success" in result) || + result.success !== true || + !Array.isArray(result.results) + ) { + valid = false; + break; + } + const rows: D1JsonRow[] = []; + for (const row of result.results) { + if (typeof row !== "object" || row === null || Array.isArray(row)) { + valid = false; + break; + } + rows.push(row as D1JsonRow); + } + if (!valid) break; + statements.push(rows); + } + if (valid) return statements; + } + throw new Error("D1 output has no successful JSON statement results."); +} + +export function requireSingleD1Row(raw: string): D1JsonRow { + const statements = parseD1JsonResults(raw); + if (statements.length !== 1) { + throw new Error("D1 JSON output must contain exactly one statement result."); + } + const rows = statements[0]; + if (rows?.length !== 1) throw new Error("D1 JSON output must contain exactly one row."); + return rows[0]; +} diff --git a/apps/api/bin/deploy-prod.ts b/apps/api/bin/deploy-prod.ts index a9014198..6503696f 100755 --- a/apps/api/bin/deploy-prod.ts +++ b/apps/api/bin/deploy-prod.ts @@ -1,11 +1,143 @@ #!/usr/bin/env bun -import type { BunRuntime } from "../../../config/bun-script-types"; +import { DRIVER_PROTOCOL_VERSION } from "@mosoo/agent-driver/boot"; +import { RUNTIME_EVENT_SCHEMA_VERSION } from "@mosoo/agent-driver/events"; +import Cloudflare from "cloudflare"; + +import type { BunRuntime, BunSpawnSyncResult } from "../../../config/bun-script-types"; +import { APP_DEPLOYMENT_PROBE_SCRIPT_NAME } from "../src/modules/apps/application/app-deployment-gateway"; +import { parseD1JsonResults } from "./d1-json"; +import { + acquireProdDeployLeaseStatements, + assertProdDeployLeaseOwned, + assertProdDeployLeaseReleased, + releaseProdDeployLeaseStatements, + verifyProdDeployLeaseStatements, +} from "./prod-deploy-lease"; +import type { ProdSchemaCatalog } from "./prod-schema-guard"; import { - extractTableNames, - findMissingProdTables, - getLatestSnapshotFilename, - parseExpectedTableNames, + assertProdSchemaMatches, + createProdSchemaCatalogFromIntrospectionRows, + createProdSchemaIntrospectionStatements, + parseGeneratedProdSchemaCatalog, } from "./prod-schema-guard"; +import { + apiWorkerDeployArgs, + apiWorkerDryRunArgs, + assertProdWfpNamespaceExact, + assertProdWfpReadOnlyInfrastructure, + assertProdWfpWorkflowBoundary, + assertProdWfpWorkflowDeployGate, + probeProdWfpWebSocket, + PROD_API_WORKER_NAME, + PROD_APP_DEPLOYMENT_DOMAIN, + PROD_APP_DEPLOYMENT_WORKFLOW, + PROD_APP_DEPLOYMENT_WORKFLOW_BINDING, + PROD_APP_DEPLOYMENT_WORKFLOW_CLASS, + PROD_APP_DISPATCH_NAMESPACE, + PROD_APP_WILDCARD_DNS, + PROD_APP_WILDCARD_ROUTE, + PROD_WFP_WRITE_CANARY_SCRIPT, + shouldProbeProdWfpBeforeMutation, + verifyProdWfpGatewayProbe, + verifyProdWfpWriteCanary, +} from "./prod-wfp-preflight"; +import type { + ProdWfpInfrastructureConfig, + ProdWfpReadOnlyInventory, + ProdWfpWorkflowMode, + WfpNamespaceInventory, + WfpScriptInventory, + WfpWorkerBindingInventory, + WfpWorkflowInventory, + WfpWriteCanaryClient, +} from "./prod-wfp-preflight"; +import type { + ProtocolV3ContainerApplication, + ProtocolV3ContainerInstance, + ProtocolV3CutoverState, + ProtocolV3LegacyTerminalSourceInventory, + ProtocolV3LossyMigrationInventory, +} from "./protocol-v3-cutover"; +import { + ACCEPT_PROTOCOL_V3_QUEUE_RESUME_SQL, + assertCutoverMigrationJournalAudited, + assertProtocolV3SmokeAgent, + assertProtocolV3Release, + assertProtocolV3WorkerVersion, + assertProtocolV3LegacyTerminalIntegrity, + assertProtocolV3LegacyTerminalSourceInventory, + assertProtocolV3LossyMigrationInventory, + assertProtocolV3RuntimeAuthorityPreflight, + authorizeProtocolV3LegacyRewriteSql, + beginProtocolV3MigrationSql, + CLOSE_PROTOCOL_V3_SMOKE_WINDOW_SQL, + collectProtocolV3ContainerApplications, + collectProtocolV3ContainerInstances, + completeProtocolV3QueueResume, + ENABLE_PROTOCOL_V3_COMMAND_FREEZE_SQL, + ENTER_PROTOCOL_V3_DRAIN_SQL, + ENTER_PROTOCOL_V3_QUEUES_RESUMING_SQL, + findPendingProdMigrations, + installProtocolV3PostMigrationCutoverSql, + installProtocolV3CutoverSql, + isProtocolV3ContainerRolloutConverged, + isProtocolV3CutoverDrained, + isProtocolV3RuntimeDrained, + isProtocolV3SmokeReady, + openProtocolV3SmokeWindowSql, + parseProtocolV3CommandFreeze, + parseProtocolV3ContainerManifestDigest, + parseProtocolV3CutoverDrain, + parseProtocolV3CutoverObjects, + parseProtocolV3CutoverProbe, + parseProtocolV3CutoverState, + parseProtocolV3LegacyTerminalIntegrity, + parseProtocolV3LegacyRewriteAuthorization, + parseProtocolV3LegacyTerminalSourceInventory, + parseProtocolV3LossyMigrationInventory, + parseProtocolV3RuntimeAuthorityPreflight, + parseProtocolV3SmokeStatus, + parseCleanGitTreeOid, + parseProtocolV3WorkerDeployment, + parseStoredProtocolV3SmokeRequestKey, + parseStoredProtocolV3SmokeSession, + parseStoredProtocolV3CutoverBookmark, + parseTimeTravelBookmark, + PROD_APPLIED_MIGRATIONS_SQL, + PROTOCOL_V3_CUTOVER_BOOKMARK_SQL, + PROTOCOL_V3_CUTOVER_DRAIN_SQL, + PROTOCOL_V3_CUTOVER_OBJECT_COUNT, + PROTOCOL_V3_CUTOVER_OBJECTS_SQL, + PROTOCOL_V3_CUTOVER_PROBE_SQL, + PROTOCOL_V3_COMMAND_FREEZE_SQL, + PROTOCOL_V3_LEGACY_TERMINAL_INTEGRITY_SQL, + PROTOCOL_V3_LEGACY_TERMINAL_SOURCE_INVENTORY_SQL, + PROTOCOL_V3_LEGACY_REWRITE_AUTHORIZATION_SQL, + PROTOCOL_V3_LOSSY_MIGRATION_INVENTORY_SQL, + PROTOCOL_V3_MIGRATION, + PROTOCOL_V3_POST_MIGRATION_CUTOVER_OBJECT_COUNT, + PROTOCOL_V3_POST_MIGRATION_CUTOVER_DRAIN_SQL, + PROTOCOL_V3_POST_MIGRATION_UNSAFE_SANDBOXES_SQL, + PROTOCOL_V3_CUTOVER_QUEUE_NAMES, + PROTOCOL_V3_RUNTIME_AUTHORITY_MIGRATION, + PROTOCOL_V3_SESSION_CLEANUP_MIGRATION, + PROTOCOL_V3_SESSION_EVENT_MIGRATION, + recoverProtocolV3CutoverFailure, + PROTOCOL_V3_SMOKE_REQUEST_KEY_SQL, + PROTOCOL_V3_SMOKE_SESSION_SQL, + PROTOCOL_V3_UNSAFE_SANDBOXES_SQL, + protocolV3SmokeAgentSql, + protocolV3SmokeStatusSql, + protocolV3RuntimeAuthorityPreflightSql, + protocolV3ReleaseTag, + protocolV3ContainerImageTag, + REMOVE_PROTOCOL_V3_CUTOVER_SQL, + storeProtocolV3CutoverBookmarkSql, + storeProtocolV3RolloutSql, + storeProtocolV3SmokeRequestKeySql, + storeProtocolV3SmokeSessionSql, + updateAndVerifyProtocolV3QueueDelivery, +} from "./protocol-v3-cutover"; declare const Bun: BunRuntime; @@ -14,6 +146,28 @@ const apiDir = `${scriptDir}/..`; const repoRoot = `${apiDir}/../..`; const D1_BINDING = "DB"; const ENV = "prod"; +const EXPECTED_DRIVER_PROTOCOL_VERSION = 3; +const EXPECTED_RUNTIME_EVENT_SCHEMA_VERSION = "2026-08-29"; +const PROD_CONTAINER_APPLICATION = "mosoo-api-prod-sandbox-prod"; +const PROD_PUBLIC_API_URL = "https://cloud.mosoo.ai/api/v1"; +const PROD_GRAPHQL_URL = "https://cloud.mosoo.ai/api/graphql"; +const PROD_HEALTH_URL = "https://cloud.mosoo.ai/api/health?deep=1"; +const CUTOVER_DRAIN_TIMEOUT_MS = 15 * 60 * 1_000; +const CUTOVER_ROLLOUT_TIMEOUT_MS = 10 * 60 * 1_000; +const CUTOVER_SMOKE_TIMEOUT_MS = 5 * 60 * 1_000; +const REMOTE_MUTATION_TIMEOUT_MS = 10 * 60 * 1_000; +const POLL_INTERVAL_MS = 5_000; +const PLATFORM_ID_PATTERN = /^[0-7][0-9A-HJKMNP-TV-Z]{25}$/iu; +const CLOUDFLARE_ACCOUNT_ID_PATTERN = /^[0-9a-f]{32}$/iu; +const CLOUDFLARE_ZONE_ID_PATTERN = /^[0-9a-f]{32}$/iu; +const PROD_ZONE_NAME = "mosoo.ai"; +const PROD_APP_DISPATCH_BINDING = "APP_DEPLOYMENT_DISPATCHER"; +const APP_DEPLOYMENT_SCRIPT_LEDGER_PROBE_SQL = `SELECT EXISTS ( + SELECT 1 + FROM sqlite_schema + WHERE type = 'table' AND name = 'app_deployment_script' +) AS ledger_present`; +const LEGACY_APP_RESOURCE_NAME_PATTERN = /^app-[0-7][0-9a-hjkmnp-tv-z]{25}$/u; function writeStdout(message: string): void { process.stdout.write(`${message}\n`); @@ -30,7 +184,7 @@ function run(args: string[], cwd = apiDir): void { stdout: "inherit", }); if (result.exitCode !== 0) { - process.exit(result.exitCode); + throw new Error(`wrangler ${args.join(" ")} exited with ${result.exitCode}.`); } } @@ -42,72 +196,292 @@ function runVp(args: string[], cwd = repoRoot): void { stdout: "inherit", }); if (result.exitCode !== 0) { - process.exit(result.exitCode); + throw new Error(`vp ${args.join(" ")} exited with ${result.exitCode}.`); } } -function applyD1Migrations(): void { - run(["d1", "migrations", "apply", D1_BINDING, "--remote", "--env", ENV]); +function captureGit(args: string[]): string { + const result = Bun.spawnSync(["git", ...args], { cwd: repoRoot }); + if (result.exitCode !== 0) { + throw new Error( + `git ${args.join(" ")} exited with ${result.exitCode}: ${result.stderr.toString("utf8")}`, + ); + } + return result.stdout.toString("utf8"); } -const MIGRATION_META_DIR = `${repoRoot}/pkgs/db/drizzle/meta`; +function readLocalMigrationNames(): string[] { + return captureGit(["ls-files", "--", "pkgs/db/drizzle/*.sql"]) + .trim() + .split("\n") + .filter(Boolean) + .map((path) => path.slice("pkgs/db/drizzle/".length)); +} -/** - * Refuse to deploy a Worker whose schema references tables missing from prod. - * Runs AFTER `applyD1Migrations`, so on the correct append-only path every - * table is already present (DEPLOY-D1-001). - */ -async function loadExpectedMigrationTables(): Promise { - const journal = await Bun.file(`${MIGRATION_META_DIR}/_journal.json`).text(); - const snapshotFilename = getLatestSnapshotFilename(journal); - const snapshot = await Bun.file(`${MIGRATION_META_DIR}/${snapshotFilename}`).text(); +function readCleanReleaseTreeOid(): string { + return parseCleanGitTreeOid( + captureGit(["rev-parse", "HEAD^{tree}"]), + captureGit(["status", "--porcelain=v1", "--untracked-files=all", "--ignore-submodules=none"]), + `${captureGit(["ls-files", "-v"])}${captureGit([ + "submodule", + "foreach", + "--quiet", + "--recursive", + "git ls-files -v", + ])}`, + `${captureGit([ + "ls-files", + "--others", + "--ignored", + "--exclude-standard", + "--", + ".env", + ".env.*", + ".dev.vars", + ".dev.vars.*", + "apps/api/.env", + "apps/api/.env.*", + "apps/api/.dev.vars", + "apps/api/.dev.vars.*", + "apps/web/.env", + "apps/web/.env.*", + ])}${captureGit([ + "submodule", + "foreach", + "--quiet", + "--recursive", + "git ls-files --others --ignored --exclude-standard -- .env .env.* .dev.vars .dev.vars.*", + ])}`, + Object.keys(process.env), + ); +} - return parseExpectedTableNames(snapshot); +function assertReleaseTreeUnchanged(releaseTreeOid: string): void { + if (readCleanReleaseTreeOid() !== releaseTreeOid) { + throw new Error("Production release tree changed during deployment."); + } } -async function assertProdSchemaMatchesMigrations(expectedTables: readonly string[]): Promise { - const result = Bun.spawnSync( - [ - wranglerBin, - "d1", - "execute", - D1_BINDING, - "--remote", - "--env", - ENV, - "--json", - "--command", - "SELECT name FROM sqlite_master WHERE type='table'", - ], - { cwd: apiDir }, - ); +function captureWrangler(args: string[], timeout?: number): string { + const result = Bun.spawnSync([wranglerBin, ...args], { + cwd: apiDir, + ...(timeout === undefined ? {} : { timeout }), + }); const stdout = result.stdout.toString("utf8"); const stderr = result.stderr.toString("utf8"); if (result.exitCode !== 0) { throw new Error( - `wrangler d1 execute (schema check) exited with ${result.exitCode}\nstderr: ${stderr}\nstdout: ${stdout}`, + `wrangler ${args.join(" ")} exited with ${result.exitCode}\nstderr: ${stderr}\nstdout: ${stdout}`, ); } - const missingTables = findMissingProdTables(expectedTables, extractTableNames(stdout)); + return stdout; +} - if (missingTables.length > 0) { +function readTaggedContainerImageDigest( + application: ProtocolV3ContainerApplication, + workerVersionId: string, +): string { + const docker = process.env["WRANGLER_DOCKER_BIN"]?.trim() || "docker"; + const tag = protocolV3ContainerImageTag(application.imageRepository, workerVersionId); + const result = Bun.spawnSync([docker, "manifest", "inspect", "-v", tag], { + cwd: apiDir, + timeout: 60_000, + }); + if (result.exitCode !== 0) { throw new Error( + `Container manifest inspection failed for ${tag}: ${result.stderr.toString("utf8")}`, + ); + } + return parseProtocolV3ContainerManifestDigest(result.stdout.toString("utf8")); +} + +function executeRawProdD1Json(sql: string, timeout?: number): string { + return captureWrangler( + ["d1", "execute", D1_BINDING, "--remote", "--env", ENV, "--json", "--command", sql], + timeout, + ); +} + +let prodDeployLeaseOwner: string | null = null; + +function executeProdDeployLease(statements: readonly string[]): string { + return executeRawProdD1Json(`${statements.join(";\n")};`, REMOTE_MUTATION_TIMEOUT_MS); +} + +function executeIdempotentProdDeployLeaseWithRetry(statements: readonly string[]): string { + try { + return executeProdDeployLease(statements); + } catch { + return executeProdDeployLease(statements); + } +} + +function executeOwnedProdDeployLease(statements: readonly string[], owner: string): void { + assertProdDeployLeaseOwned(executeIdempotentProdDeployLeaseWithRetry(statements), owner); +} + +export function acquireProdDeployLease( + owner: string, + execute: (statements: readonly string[]) => string, +): void { + assertProdDeployLeaseOwned(execute(acquireProdDeployLeaseStatements(owner)), owner); + prodDeployLeaseOwner = owner; +} + +function verifyProdDeployLease(): void { + if (prodDeployLeaseOwner === null) { + throw new Error("Production remote mutation requires an acquired deploy lease."); + } + executeOwnedProdDeployLease(verifyProdDeployLeaseStatements(), prodDeployLeaseOwner); +} + +function releaseProdDeployLease(): void { + if (prodDeployLeaseOwner === null) return; + const owner = prodDeployLeaseOwner; + try { + assertProdDeployLeaseReleased( + executeIdempotentProdDeployLeaseWithRetry(releaseProdDeployLeaseStatements(owner)), + ); + } finally { + prodDeployLeaseOwner = null; + } +} + +function executeProdMutation(args: string[], inheritOutput = false): BunSpawnSyncResult { + verifyProdDeployLease(); + let result: BunSpawnSyncResult; + try { + result = Bun.spawnSync([wranglerBin, ...args], { + cwd: apiDir, + ...(inheritOutput + ? { stderr: "inherit" as const, stdin: "inherit" as const, stdout: "inherit" as const } + : {}), + timeout: REMOTE_MUTATION_TIMEOUT_MS, + }); + } catch (mutationError) { + try { + verifyProdDeployLease(); + } catch (verificationError) { + throw new AggregateError( + [mutationError, verificationError], + "Production mutation and deploy lease verification both failed.", + { cause: verificationError }, + ); + } + throw mutationError; + } + let verificationError: unknown = null; + try { + verifyProdDeployLease(); + } catch (error) { + verificationError = error; + } + if (verificationError !== null) { + throw new AggregateError( + [ + new Error( + `wrangler ${args.join(" ")} completed with ${result.exitCode} before deploy lease verification failed.`, + ), + verificationError, + ], + "Production mutation ownership could not be verified.", + ); + } + return result; +} + +function runProdMutation(args: string[]): void { + const result = executeProdMutation(args, true); + if (result.exitCode !== 0) { + throw new Error(`wrangler ${args.join(" ")} exited with ${result.exitCode}.`); + } +} + +async function runOwnedProdAsyncMutation(mutation: () => Promise): Promise { + verifyProdDeployLease(); + let result: T; + try { + result = await mutation(); + } catch (mutationError) { + try { + verifyProdDeployLease(); + } catch (verificationError) { + throw new AggregateError( + [mutationError, verificationError], + "Production mutation and deploy lease verification both failed.", + { cause: verificationError }, + ); + } + throw mutationError; + } + try { + verifyProdDeployLease(); + } catch (verificationError) { + throw new AggregateError( [ - "✗ Prod D1 schema drift: the latest migration snapshot defines tables absent from prod.", - ` Missing: ${missingTables.join(", ")}`, - " Cause: a migration expected to create a table was skipped, rewritten,", - " or incomplete (DEPLOY-D1-001). Fix the history with a new reviewed", - " migration; never rewrite an applied migration, then re-run the deploy.", - ].join("\n"), + new Error("Cloudflare mutation completed before deploy lease verification failed."), + verificationError, + ], + "Production mutation ownership could not be verified.", + { cause: verificationError }, ); } + return result; +} + +function executeProdD1Json(sql: string): string { + return executeRawProdD1Json(sql); +} - writeStdout(` prod schema OK (${expectedTables.length} migration tables present)`); +function executeProdD1(sql: string): void { + runProdMutation([ + "d1", + "execute", + D1_BINDING, + "--remote", + "--env", + ENV, + "--yes", + "--command", + sql, + ]); } -const REQUIRED_PROD_QUEUES: readonly string[] = ["environment-artifact-build"]; +function sleep(milliseconds: number): Promise { + return new Promise((resolve) => setTimeout(resolve, milliseconds)); +} + +function applyD1Migrations(): void { + runProdMutation(["d1", "migrations", "apply", D1_BINDING, "--remote", "--env", ENV]); +} + +function loadExpectedProdSchema(): ProdSchemaCatalog { + const result = Bun.spawnSync(["bun", "pkgs/db/scripts/check-schema.ts", "--catalog"], { + cwd: repoRoot, + }); + const stdout = result.stdout.toString("utf8"); + const stderr = result.stderr.toString("utf8"); + + if (result.exitCode !== 0) { + throw new Error( + `Drizzle schema freshness check exited with ${result.exitCode}\nstderr: ${stderr}\nstdout: ${stdout}`, + ); + } + return parseGeneratedProdSchemaCatalog(stdout); +} + +function assertProdSchemaMatchesSnapshot(expected: ProdSchemaCatalog): void { + const tableNames = expected.tables.map(({ name }) => name); + const statements = createProdSchemaIntrospectionStatements(tableNames); + const live = createProdSchemaCatalogFromIntrospectionRows( + parseD1JsonResults(executeProdD1Json(statements.join(";\n"))), + tableNames, + ); + assertProdSchemaMatches(expected, live); + writeStdout(` prod schema OK (${expected.tables.length} tables fully match)`); +} function listProdQueues(): string[] { const result = Bun.spawnSync([wranglerBin, "queues", "list"], { cwd: apiDir }); @@ -140,9 +514,7 @@ function ensureQueueExists(queueName: string, existingQueues: readonly string[]) return; } - const result = Bun.spawnSync([wranglerBin, "queues", "create", queueName], { - cwd: apiDir, - }); + const result = executeProdMutation(["queues", "create", queueName]); const stdout = result.stdout.toString("utf8"); const stderr = result.stderr.toString("utf8"); @@ -164,32 +536,1556 @@ function ensureQueueExists(queueName: string, existingQueues: readonly string[]) function ensureRequiredProdQueues(): void { const listing = listProdQueues(); - for (const queueName of REQUIRED_PROD_QUEUES) { + for (const queueName of PROTOCOL_V3_CUTOVER_QUEUE_NAMES) { ensureQueueExists(queueName, listing); } } -const expectedMigrationTables = await loadExpectedMigrationTables().catch((error: unknown) => { - writeStdout(error instanceof Error ? error.message : String(error)); - process.exit(1); -}); +function assertProtocolV3Artifacts(): void { + const protocolVersion: number = DRIVER_PROTOCOL_VERSION; + const eventSchemaVersion: string = RUNTIME_EVENT_SCHEMA_VERSION; + + if ( + protocolVersion !== EXPECTED_DRIVER_PROTOCOL_VERSION || + eventSchemaVersion !== EXPECTED_RUNTIME_EVENT_SCHEMA_VERSION + ) { + throw new Error( + `Production requires Driver protocol ${EXPECTED_DRIVER_PROTOCOL_VERSION} and event schema ${EXPECTED_RUNTIME_EVENT_SCHEMA_VERSION}; got ${protocolVersion} and ${eventSchemaVersion}.`, + ); + } +} + +function runLocalPreflight(releaseTreeOid: string): void { + assertProtocolV3Artifacts(); + + writeStdout("▶ Building Driver before any production mutation"); + runVp(["run", "--filter", "agent-driver", "build"]); + + writeStdout("▶ Dry-running the API Worker before any production mutation"); + run(apiWorkerDryRunArgs()); + + assertReleaseTreeUnchanged(releaseTreeOid); +} + +interface ProtocolV3SmokeConfig { + readonly agentId: string; + readonly token: string; +} + +interface ProdQueueApiConfig { + readonly accountId: string; + readonly apiToken: string; + readonly zoneId: string; +} + +function readProdQueueApiConfig(): ProdQueueApiConfig { + const accountId = process.env["CLOUDFLARE_ACCOUNT_ID"]?.trim() ?? ""; + const apiToken = process.env["CLOUDFLARE_API_TOKEN"]?.trim() ?? ""; + const zoneId = process.env["CLOUDFLARE_ZONE_ID"]?.trim() ?? ""; + + if ( + !CLOUDFLARE_ACCOUNT_ID_PATTERN.test(accountId) || + !CLOUDFLARE_ZONE_ID_PATTERN.test(zoneId) || + !apiToken + ) { + throw new Error( + "Production deploy requires exact CLOUDFLARE_ACCOUNT_ID, CLOUDFLARE_ZONE_ID, and CLOUDFLARE_API_TOKEN values.", + ); + } + + return { accountId, apiToken, zoneId }; +} + +function prodWfpInfrastructureConfig( + config: ProdQueueApiConfig, + requireProbeOnlyNamespace: boolean, + workflowRepairAllowed = false, +): ProdWfpInfrastructureConfig { + return { + accountId: config.accountId, + apiWorkerName: PROD_API_WORKER_NAME, + dispatchBinding: PROD_APP_DISPATCH_BINDING, + dispatchNamespace: PROD_APP_DISPATCH_NAMESPACE, + requireProbeOnlyNamespace, + wildcardDns: PROD_APP_WILDCARD_DNS, + wildcardRoute: PROD_APP_WILDCARD_ROUTE, + workflowBinding: PROD_APP_DEPLOYMENT_WORKFLOW_BINDING, + workflowClass: PROD_APP_DEPLOYMENT_WORKFLOW_CLASS, + workflowName: PROD_APP_DEPLOYMENT_WORKFLOW, + workflowRepairAllowed, + zoneId: config.zoneId, + zoneName: PROD_ZONE_NAME, + }; +} + +function readAppDeploymentScriptLedgerPresent(): boolean { + const row = parseD1JsonResults(executeProdD1Json(APP_DEPLOYMENT_SCRIPT_LEDGER_PROBE_SQL))[0]?.[0]; + const value = row?.["ledger_present"]; + if (value !== 0 && value !== 1) { + throw new Error("Production D1 did not return an exact App deployment script ledger probe."); + } + return value === 1; +} + +async function collectAsync(items: AsyncIterable): Promise { + const values: T[] = []; + for await (const item of items) values.push(item); + return values; +} + +function normalizeProdWfpNamespace( + namespace: Awaited< + ReturnType + >, +): WfpNamespaceInventory { + return { + id: namespace.namespace_id ?? null, + name: namespace.namespace_name ?? null, + scriptCount: namespace.script_count ?? null, + trustedWorkers: namespace.trusted_workers ?? null, + }; +} + +async function readProdWfpNamespace( + client: Cloudflare, + config: ProdQueueApiConfig, +): Promise { + const namespace = await client.workersForPlatforms.dispatch.namespaces.get( + PROD_APP_DISPATCH_NAMESPACE, + { account_id: config.accountId }, + ); + return normalizeProdWfpNamespace(namespace); +} + +async function readProdWfpProbeScript( + client: Cloudflare, + config: ProdQueueApiConfig, +): Promise { + try { + const script = await client.workersForPlatforms.dispatch.namespaces.scripts.get( + APP_DEPLOYMENT_PROBE_SCRIPT_NAME, + { + account_id: config.accountId, + dispatch_namespace: PROD_APP_DISPATCH_NAMESPACE, + }, + ); + return { + id: script.script?.id ?? null, + namespace: script.dispatch_namespace ?? null, + }; + } catch (error) { + if (!isCloudflareStatus(error, 404)) throw error; + return { id: null, namespace: null }; + } +} + +async function readProdAppDeploymentWorkflow( + client: Cloudflare, + config: ProdQueueApiConfig, +): Promise { + try { + const workflow = await client.workflows.get(PROD_APP_DEPLOYMENT_WORKFLOW, { + account_id: config.accountId, + }); + return { + className: workflow.class_name, + id: workflow.id, + name: workflow.name, + scriptName: workflow.script_name, + }; + } catch (error) { + if (!isCloudflareStatus(error, 404)) throw error; + return { className: null, id: null, name: null, scriptName: null }; + } +} + +type ProdWorkerSettings = Awaited< + ReturnType +>; + +function normalizeProdWorkerBindings( + bindings: ProdWorkerSettings["bindings"], +): WfpWorkerBindingInventory[] { + return (bindings ?? []).map((binding) => ({ + className: binding.type === "workflow" ? (binding.class_name ?? null) : null, + name: binding.name ?? null, + namespace: binding.type === "dispatch_namespace" ? binding.namespace : null, + scriptName: binding.type === "workflow" ? (binding.script_name ?? PROD_API_WORKER_NAME) : null, + type: binding.type ?? null, + workflowName: binding.type === "workflow" ? binding.workflow_name : null, + })); +} + +async function readProdAppDeploymentWorkflowBoundary( + client: Cloudflare, + config: ProdQueueApiConfig, +): Promise<{ + bindings: WfpWorkerBindingInventory[]; + workflow: WfpWorkflowInventory; +}> { + const [workflow, settings] = await Promise.all([ + readProdAppDeploymentWorkflow(client, config), + client.workers.scripts.scriptAndVersionSettings.get(PROD_API_WORKER_NAME, { + account_id: config.accountId, + }), + ]); + return { bindings: normalizeProdWorkerBindings(settings.bindings), workflow }; +} + +function isLegacyAppName(value: string | undefined): value is string { + return value !== undefined && LEGACY_APP_RESOURCE_NAME_PATTERN.test(value); +} + +function isLegacyAppHostname(value: string | undefined): value is string { + if (value === undefined) return false; + const suffix = `.${PROD_APP_DEPLOYMENT_DOMAIN}`; + return value.endsWith(suffix) && isLegacyAppName(value.slice(0, -suffix.length)); +} + +async function readProdWfpReadOnlyInventory( + client: Cloudflare, + config: ProdQueueApiConfig, +): Promise { + const [ + namespace, + probeScript, + workflow, + zone, + dnsRecords, + certificatePacks, + routes, + workerSettings, + pagesProjects, + workerScripts, + workerDomains, + ] = await Promise.all([ + readProdWfpNamespace(client, config), + readProdWfpProbeScript(client, config), + readProdAppDeploymentWorkflow(client, config), + client.zones.get({ zone_id: config.zoneId }), + collectAsync(client.dns.records.list({ zone_id: config.zoneId })), + collectAsync(client.ssl.certificatePacks.list({ zone_id: config.zoneId })), + collectAsync(client.workers.routes.list({ zone_id: config.zoneId })), + client.workers.scripts.scriptAndVersionSettings.get(PROD_API_WORKER_NAME, { + account_id: config.accountId, + }), + collectAsync(client.pages.projects.list({ account_id: config.accountId })), + collectAsync(client.workers.scripts.list({ account_id: config.accountId })), + collectAsync(client.workers.domains.list({ account_id: config.accountId })), + ]); + + const legacyResources: string[] = []; + for (const project of pagesProjects) { + if (isLegacyAppName(project.name)) { + legacyResources.push( + `Pages project ${project.name}: wrangler pages project delete ${project.name}`, + ); + } + for await (const domain of client.pages.projects.domains.list(project.name, { + account_id: config.accountId, + })) { + if (isLegacyAppHostname(domain.name)) { + legacyResources.push( + `Pages domain ${project.name}/${domain.name}: DELETE /accounts/$CLOUDFLARE_ACCOUNT_ID/pages/projects/${project.name}/domains/${domain.name}`, + ); + } + } + } + for (const script of workerScripts) { + if (isLegacyAppName(script.id)) { + legacyResources.push( + `classic Worker script ${script.id}: wrangler delete --name ${script.id}`, + ); + } + } + for (const domain of workerDomains) { + if (isLegacyAppHostname(domain.hostname) || isLegacyAppName(domain.service)) { + legacyResources.push( + `classic Worker domain ${domain.id} ${domain.hostname}: DELETE /accounts/$CLOUDFLARE_ACCOUNT_ID/workers/domains/${domain.id}`, + ); + } + } + for (const route of routes) { + const hostname = route.pattern.endsWith("/*") ? route.pattern.slice(0, -2) : undefined; + if (isLegacyAppHostname(hostname) || isLegacyAppName(route.script)) { + legacyResources.push( + `classic Worker route ${route.id} ${route.pattern}: DELETE /zones/$CLOUDFLARE_ZONE_ID/workers/routes/${route.id}`, + ); + } + } + + return { + apiWorkerBindings: normalizeProdWorkerBindings(workerSettings.bindings), + certificates: certificatePacks.map((pack) => ({ + certificates: pack.certificates.map((certificate) => ({ + expiresOn: certificate.expires_on ?? null, + hosts: certificate.hosts, + status: certificate.status, + })), + hosts: pack.hosts, + status: pack.status, + })), + dnsRecords: dnsRecords.map((record) => ({ + name: record.name ?? null, + proxied: record.proxied ?? null, + type: record.type ?? null, + })), + legacyResources: legacyResources.toSorted(), + namespace, + probeScript, + routes: routes.map((route) => ({ + pattern: route.pattern ?? null, + script: route.script ?? null, + })), + workflow, + zone: { + accountId: zone.account.id ?? null, + id: zone.id ?? null, + name: zone.name ?? null, + status: zone.status ?? null, + }, + }; +} + +function isCloudflareStatus(error: unknown, status: number): boolean { + return ( + typeof error === "object" && + error !== null && + "status" in error && + Reflect.get(error, "status") === status + ); +} + +function createProdWfpWriteCanaryClient( + client: Cloudflare, + config: ProdQueueApiConfig, +): WfpWriteCanaryClient { + const params = { + account_id: config.accountId, + dispatch_namespace: PROD_APP_DISPATCH_NAMESPACE, + } as const; + + return { + async deleteScript(scriptName) { + await runOwnedProdAsyncMutation(async () => { + try { + await client.workersForPlatforms.dispatch.namespaces.scripts.delete(scriptName, params); + } catch (error) { + if (!isCloudflareStatus(error, 404)) throw error; + } + }); + }, + async readScript(scriptName) { + try { + const script = await client.workersForPlatforms.dispatch.namespaces.scripts.get( + scriptName, + params, + ); + return { + id: script.script?.id ?? null, + namespace: script.dispatch_namespace ?? null, + }; + } catch (error) { + if (isCloudflareStatus(error, 404)) return null; + throw error; + } + }, + async uploadScript(scriptName) { + const result = await runOwnedProdAsyncMutation(() => + client.workersForPlatforms.dispatch.namespaces.scripts.update(scriptName, { + ...params, + files: [ + new File( + [ + 'export default { fetch() { return new Response("mosoo release write canary"); } };', + ], + "canary.mjs", + { type: "application/javascript+module" }, + ), + ], + metadata: { + compatibility_date: "2026-06-01", + main_module: "canary.mjs", + }, + }), + ); + return { id: result.id ?? null }; + }, + }; +} + +function readProtocolV3SmokeConfig(): ProtocolV3SmokeConfig { + const agentId = process.env["MOSOO_PROTOCOL_V3_SMOKE_AGENT_ID"]?.trim() ?? ""; + const token = process.env["MOSOO_PROTOCOL_V3_SMOKE_TOKEN"]?.trim() ?? ""; + + if (!PLATFORM_ID_PATTERN.test(agentId) || !token) { + throw new Error( + "Production deploy requires a dedicated MOSOO_PROTOCOL_V3_SMOKE_AGENT_ID and MOSOO_PROTOCOL_V3_SMOKE_TOKEN.", + ); + } + + return { agentId, token }; +} + +function probeProtocolV3Cutover() { + return parseProtocolV3CutoverProbe(executeProdD1Json(PROTOCOL_V3_CUTOVER_PROBE_SQL)); +} + +function readPendingProdMigrations(localMigrationNames: readonly string[]): string[] { + return findPendingProdMigrations( + executeProdD1Json(PROD_APPLIED_MIGRATIONS_SQL), + localMigrationNames, + ); +} + +function printProtocolV3LossyMigrationInventory( + inventory: ProtocolV3LossyMigrationInventory, +): void { + writeStdout( + ` migration 0013 loss candidates: total=${inventory.totalCandidates} orphan_effects=${inventory.orphanEffects} attempt_completion_time_fabrications=${inventory.attemptCompletionTimeFabrications} command_payload_conflicts=${inventory.commandPayloadConflicts} MCP_arguments=${inventory.mcpArgumentOmissions} input_text=${inventory.inputTextOmissions} input_results=${inventory.inputStartResultOmissions} control_reason=${inventory.controlReasonOmissions} permission_payload_rewrites=${inventory.permissionPayloadRewrites} MCP_result_omissions=${inventory.mcpResultOmissions} MCP_result_conflicts=${inventory.mcpResultConflicts} provider_receipts=${inventory.providerReceiptLosses} MCP_terminal_conflicts=${inventory.mcpCommandTerminalConflicts} command_errors=${inventory.commandErrorOmissions} Session_Run_errors=${inventory.sessionRunErrorOmissions}`, + ); + for (const candidate of inventory.candidateIds) { + writeStdout(` ${candidate.category}: ${candidate.id}`); + } + if (inventory.totalCandidates > inventory.candidateIds.length) { + writeStdout( + ` ... ${inventory.totalCandidates - inventory.candidateIds.length} more candidates`, + ); + } +} + +function verifyProdLossyMigrationInventory(): void { + const inventory = parseProtocolV3LossyMigrationInventory( + executeProdD1Json(PROTOCOL_V3_LOSSY_MIGRATION_INVENTORY_SQL), + ); + printProtocolV3LossyMigrationInventory(inventory); + assertProtocolV3LossyMigrationInventory(inventory); +} + +function printProtocolV3LegacyTerminalSourceInventory( + inventory: ProtocolV3LegacyTerminalSourceInventory, +): void { + for (const [kind, counts] of [ + ["cancelled", inventory.cancelled], + ["completed", inventory.completed], + ["failed", inventory.failed], + ] as const) { + writeStdout( + ` run.${kind}: total=${counts.total} canonical=${counts.canonical} rewrite_candidates=${counts.noncanonical} canonical_target_collisions=${counts.canonicalTargetCollisions}`, + ); + } + writeStdout( + ` invalid terminal links=${inventory.invalidTerminalLinks} status/kind mismatches=${inventory.mismatchedTerminalEvents} multiple terminal Runs=${inventory.multipleTerminalRuns}`, + ); +} + +function verifyProdLegacyTerminalSourceInventory(): void { + const inventory = parseProtocolV3LegacyTerminalSourceInventory( + executeProdD1Json(PROTOCOL_V3_LEGACY_TERMINAL_SOURCE_INVENTORY_SQL), + ); + printProtocolV3LegacyTerminalSourceInventory(inventory); + assertProtocolV3LegacyTerminalSourceInventory(inventory); +} + +interface LegacyRewriteProof { + readonly candidateCount: number; + readonly candidateManifestJson: string; +} + +function preflightProdLegacyTerminalIntegrity(): LegacyRewriteProof { + verifyProdLegacyTerminalSourceInventory(); + + const integrity = parseProtocolV3LegacyTerminalIntegrity( + executeProdD1Json(PROTOCOL_V3_LEGACY_TERMINAL_INTEGRITY_SQL), + ); + assertProtocolV3LegacyTerminalIntegrity(integrity); + writeStdout( + ` legacy terminal integrity is unambiguous; migration 0014 will normalize ${integrity.noncanonicalTerminalSources} terminal source identities and ${integrity.repairableFailedRuns} failed Run errors, label ${integrity.legacyMaterializedMessages} messages as materialized, backfill ${integrity.legacyStreamRows} row-local stream identities, and preserve ${integrity.legacyTerminalEvents} terminal events as semantic_hash=NULL legacy history`, + ); + return { + candidateCount: integrity.noncanonicalTerminalSources, + candidateManifestJson: integrity.rewriteCandidateManifestJson, + }; +} + +function authorizeProdLegacyTerminalRewrite( + proof: LegacyRewriteProof, + releaseTreeOid: string, + bookmark: string, +): void { + if (prodDeployLeaseOwner === null) { + throw new Error("Protocol v3 legacy rewrite requires the production deploy lease."); + } + const owner = prodDeployLeaseOwner; + executeProdD1( + authorizeProtocolV3LegacyRewriteSql(owner, proof.candidateCount, proof.candidateManifestJson), + ); + const authorization = parseProtocolV3LegacyRewriteAuthorization( + executeProdD1Json(PROTOCOL_V3_LEGACY_REWRITE_AUTHORIZATION_SQL), + ); + if ( + authorization.deployOwner !== owner || + authorization.bookmark !== bookmark || + authorization.candidateCount !== proof.candidateCount || + authorization.candidateManifestJson !== proof.candidateManifestJson || + authorization.releaseTreeOid !== releaseTreeOid || + authorization.expiresAt <= Math.floor(Date.now() / 1000) + ) { + throw new Error("Protocol v3 legacy rewrite authorization did not persist exactly."); + } +} + +function readProtocolV3CutoverState(): ProtocolV3CutoverState { + return parseProtocolV3CutoverState(executeProdD1Json(PROTOCOL_V3_COMMAND_FREEZE_SQL)); +} + +function installProtocolV3CutoverGate( + releaseTreeOid: string, + postRuntimeAuthorityMigration: boolean, +): ProtocolV3CutoverState { + executeProdD1( + postRuntimeAuthorityMigration + ? installProtocolV3PostMigrationCutoverSql(releaseTreeOid) + : installProtocolV3CutoverSql(releaseTreeOid), + ); + assertProtocolV3CutoverGateExact(); + const state = readProtocolV3CutoverState(); + assertProtocolV3Release(state, releaseTreeOid); + return state; +} + +function assertProtocolV3CutoverGateExact(): void { + const objects = parseProtocolV3CutoverObjects(executeProdD1Json(PROTOCOL_V3_CUTOVER_OBJECTS_SQL)); + + const validCount = + objects.objectCount === PROTOCOL_V3_CUTOVER_OBJECT_COUNT || + objects.objectCount === PROTOCOL_V3_POST_MIGRATION_CUTOVER_OBJECT_COUNT; + if (!validCount || objects.exactObjectCount !== objects.objectCount) { + throw new Error( + `Protocol v3 cutover gate is invalid (${objects.objectCount} observed, ${objects.exactObjectCount} exact objects).`, + ); + } +} + +function removeProtocolV3CutoverGate(): void { + executeProdD1(REMOVE_PROTOCOL_V3_CUTOVER_SQL); + const objects = parseProtocolV3CutoverObjects(executeProdD1Json(PROTOCOL_V3_CUTOVER_OBJECTS_SQL)); + if (objects.objectCount !== 0 || probeProtocolV3Cutover().gatePresent) { + throw new Error( + `Protocol v3 cutover gate cleanup is incomplete (${objects.objectCount} reserved objects remain).`, + ); + } +} + +function enableProtocolV3CommandFreeze(): void { + executeProdD1(ENABLE_PROTOCOL_V3_COMMAND_FREEZE_SQL); + if (!parseProtocolV3CommandFreeze(executeProdD1Json(PROTOCOL_V3_COMMAND_FREEZE_SQL))) { + throw new Error("Protocol v3 final Driver command freeze was not enabled."); + } +} + +function beginProtocolV3Migration(releaseTreeOid: string): ProtocolV3CutoverState { + executeProdD1(beginProtocolV3MigrationSql(releaseTreeOid)); + const state = readProtocolV3CutoverState(); + if (!state.migrationStarted) { + throw new Error("Protocol v3 migration intent did not persist exactly."); + } + return state; +} + +function enterProtocolV3QueuesResuming(): ProtocolV3CutoverState { + executeProdD1(ENTER_PROTOCOL_V3_QUEUES_RESUMING_SQL); + const state = readProtocolV3CutoverState(); + if (state.phase !== "queues_resuming" || !state.enabled || !state.commandFreeze) { + throw new Error("Protocol v3 queue-resume recovery phase was not persisted."); + } + return state; +} + +function acceptProtocolV3QueueResume(): void { + executeProdD1(ACCEPT_PROTOCOL_V3_QUEUE_RESUME_SQL); + const state = readProtocolV3CutoverState(); + if (state.phase !== "queues_resuming" || state.enabled || !state.commandFreeze) { + throw new Error("Protocol v3 queue-resume acceptance was not persisted."); + } +} + +function enterProtocolV3Drain(): void { + executeProdD1(ENTER_PROTOCOL_V3_DRAIN_SQL); + if (parseProtocolV3CommandFreeze(executeProdD1Json(PROTOCOL_V3_COMMAND_FREEZE_SQL))) { + throw new Error("Protocol v3 drain gate did not keep control commands available."); + } +} + +async function updateAndVerifyProdQueues( + config: ProdQueueApiConfig, + action: "pause" | "resume", +): Promise { + const client = new Cloudflare({ apiToken: config.apiToken }); + const queues: Array<{ id: string; name: string }> = []; + + for await (const queue of client.queues.list({ account_id: config.accountId })) { + if (queue.queue_name && queue.queue_id) { + queues.push({ id: queue.queue_id, name: queue.queue_name }); + } + } + + await updateAndVerifyProtocolV3QueueDelivery( + { + list: async () => queues, + mutate: (queueName, queueAction) => { + runProdMutation(["queues", `${queueAction}-delivery`, queueName]); + }, + read: async (queueId) => { + const queue = await client.queues.get(queueId, { account_id: config.accountId }); + return { + deliveryPaused: queue.settings?.delivery_paused, + name: queue.queue_name ?? "", + }; + }, + }, + action, + ); + for (const queueName of PROTOCOL_V3_CUTOVER_QUEUE_NAMES) { + writeStdout(` queue ${queueName} delivery is ${action}d`); + } +} + +function resumeAndVerifyProdQueues(config: ProdQueueApiConfig): Promise { + return updateAndVerifyProdQueues(config, "resume"); +} + +function pauseAndVerifyProdQueues(config: ProdQueueApiConfig): Promise { + return updateAndVerifyProdQueues(config, "pause"); +} + +async function waitForProtocolV3Drain( + postRuntimeAuthorityMigration: boolean, + requireCommands = true, +): Promise { + const deadline = Date.now() + CUTOVER_DRAIN_TIMEOUT_MS; + + while (true) { + const state = parseProtocolV3CutoverDrain( + executeProdD1Json( + postRuntimeAuthorityMigration + ? PROTOCOL_V3_POST_MIGRATION_CUTOVER_DRAIN_SQL + : PROTOCOL_V3_CUTOVER_DRAIN_SQL, + ), + ); + + writeStdout( + ` drain: runs=${state.activeRuns} appDeploymentRuns=${state.activeAppDeploymentRuns} drivers=${state.liveDrivers} effects=${state.unsettledEffects} driverCommands=${state.nonterminalCommands} apiCommands=${state.nonterminalApiCommands} sandboxes=${state.unsafeSandboxes} sandboxSessions=${state.unsafeSandboxSessions} backups=${state.unsafeSandboxBackups} backupStaging=${state.unsafeSandboxBackupStaging} environmentArtifactStaging=${state.unsafeEnvironmentArtifactBackupStaging} sessions=${state.unsafeSessions}`, + ); + if (requireCommands ? isProtocolV3CutoverDrained(state) : isProtocolV3RuntimeDrained(state)) { + return; + } + if (Date.now() >= deadline) { + const unsafeSandboxes = parseD1JsonResults( + executeProdD1Json( + postRuntimeAuthorityMigration + ? PROTOCOL_V3_POST_MIGRATION_UNSAFE_SANDBOXES_SQL + : PROTOCOL_V3_UNSAFE_SANDBOXES_SQL, + ), + ) + .flatMap((rows) => rows) + .map((row) => `${String(row.id)}(${String(row.status)})`) + .join(", "); + throw new Error( + `Protocol v3 ${requireCommands ? "final command" : "runtime"} drain did not finish before the 15-minute safety timeout.${unsafeSandboxes.length === 0 ? "" : ` Unsafe sandboxes: ${unsafeSandboxes}. Use the supported lifecycle hibernate/checkpoint path, then retry.`}`, + ); + } + + await sleep(POLL_INTERVAL_MS); + } +} + +function readStoredProtocolV3Bookmark(): string | null { + return parseStoredProtocolV3CutoverBookmark(executeProdD1Json(PROTOCOL_V3_CUTOVER_BOOKMARK_SQL)); +} + +function printProtocolV3EmergencyBookmark(bookmark: string): void { + writeStdout(` emergency destructive backup bookmark: ${bookmark}`); + writeStdout( + " After migration, automated recovery is roll-forward only: rerun this exact v3 release.", + ); + writeStdout( + " D1 Time Travel would discard every D1 write after this bookmark and does not restore Durable Object storage.", + ); + writeStdout( + " Manual v2 recovery requires a global maintenance write stop, a reconciled inventory of post-bookmark writes, and explicit human approval.", + ); +} + +function readExactProdWorkerVersion(releaseTreeOid: string): string { + const deployment = parseProtocolV3WorkerDeployment( + captureWrangler(["deployments", "status", "--env", ENV, "--json"]), + ); + assertProtocolV3WorkerVersion( + captureWrangler(["versions", "view", deployment.versionId, "--env", ENV, "--json"]), + deployment.versionId, + releaseTreeOid, + ); + return deployment.versionId; +} + +function ensureProtocolV3Bookmark(): string { + const stored = readStoredProtocolV3Bookmark(); + if (stored !== null) return stored; + + const bookmark = parseTimeTravelBookmark( + captureWrangler(["d1", "time-travel", "info", D1_BINDING, "--env", ENV, "--json"]), + ); + executeProdD1(storeProtocolV3CutoverBookmarkSql(bookmark)); + const persisted = readStoredProtocolV3Bookmark(); + + if (persisted !== bookmark) { + throw new Error( + "Failed to persist the pre-migration Time Travel bookmark in the cutover gate.", + ); + } + + return bookmark; +} + +async function readProdContainerApplication( + config: ProdQueueApiConfig, +): Promise { + const client = new Cloudflare({ apiToken: config.apiToken }); + const applications = await collectProtocolV3ContainerApplications((pageToken) => + client.get(`/accounts/${config.accountId}/containers/dash/applications`, { + query: { + per_page: 100, + ...(pageToken === null ? {} : { page_token: pageToken }), + }, + timeout: 10_000, + }), + ); + const matches = applications.filter( + (application) => application.name === PROD_CONTAINER_APPLICATION, + ); + if (matches.length !== 1) { + throw new Error( + `Expected exactly one Container application ${PROD_CONTAINER_APPLICATION}; found ${matches.length}.`, + ); + } + return matches[0]; +} + +function readProdContainerInstances(applicationId: string): Promise { + return collectProtocolV3ContainerInstances((pageToken) => + captureWrangler([ + "containers", + "instances", + applicationId, + "--json", + "--per-page", + "100", + ...(pageToken === null ? [] : ["--page-token", pageToken]), + "--env", + ENV, + ]), + ); +} + +async function waitForContainerRollout( + config: ProdQueueApiConfig, + expectedImageDigest: string, + previousVersion: number, + expectedVersion: number | null, +): Promise { + const deadline = Date.now() + CUTOVER_ROLLOUT_TIMEOUT_MS; + + while (true) { + const application = await readProdContainerApplication(config); + const instances = await readProdContainerInstances(application.id); + + if (expectedVersion !== null && application.version !== expectedVersion) { + throw new Error( + `Production Container application moved from release version ${expectedVersion} to ${application.version}.`, + ); + } + if ( + expectedVersion === null && + (application.version < previousVersion || application.version > previousVersion + 1) + ) { + throw new Error("Production Container application advanced outside this release rollout."); + } + if ( + application.imageDigest !== expectedImageDigest && + application.version !== previousVersion + ) { + throw new Error("Production Container application advanced to a foreign image."); + } + + if ( + application.imageDigest === expectedImageDigest && + isProtocolV3ContainerRolloutConverged(application, instances) + ) { + writeStdout( + ` container rollout converged at application version ${application.version} (${instances.length} known instances)`, + ); + return application; + } + if (Date.now() >= deadline) { + throw new Error( + "Protocol v3 Container rollout did not converge before the 10-minute timeout.", + ); + } + + writeStdout( + ` waiting for ${instances.length} Container instances to reach the target version`, + ); + await sleep(POLL_INTERVAL_MS); + } +} + +async function assertPublishedProtocolV3Release( + state: ProtocolV3CutoverState, + releaseTreeOid: string, + config: ProdQueueApiConfig, +): Promise { + assertProtocolV3Release(state, releaseTreeOid); + if ( + state.workerVersionId === null || + state.containerApplicationVersion === null || + state.containerImageDigest === null + ) { + throw new Error("Protocol v3 rollout metadata is not durably bound to the cutover marker."); + } + if (readExactProdWorkerVersion(releaseTreeOid) !== state.workerVersionId) { + throw new Error("Production Worker moved away from the bound protocol v3 version."); + } + const application = await readProdContainerApplication(config); + if ( + application.version !== state.containerApplicationVersion || + application.imageDigest !== state.containerImageDigest || + readTaggedContainerImageDigest(application, state.workerVersionId) !== + state.containerImageDigest + ) { + throw new Error( + "Production Container application moved away from the bound protocol v3 version.", + ); + } +} + +async function verifyProdHealth(): Promise { + let lastError: unknown = new Error("Production health check was not attempted."); + + for (let attempt = 1; attempt <= 12; attempt += 1) { + try { + const response = await fetch(PROD_HEALTH_URL, { signal: AbortSignal.timeout(10_000) }); + const body = (await response.json()) as { name?: unknown; ok?: unknown }; + + if (response.ok && body.name === "mosoo" && body.ok === true) { + writeStdout(" production Worker and D1 health check passed"); + return; + } + + lastError = new Error(`health returned HTTP ${response.status}`); + } catch (error) { + lastError = error; + } + + if (attempt < 12) await sleep(POLL_INTERVAL_MS); + } + + throw new Error("Production health check failed after deployment.", { cause: lastError }); +} + +function requireJsonRecord(value: unknown, label: string): Record { + if (typeof value !== "object" || value === null || Array.isArray(value)) { + throw new Error(`${label} must be a JSON object.`); + } + return value as Record; +} + +async function fetchProdMutation(input: string, init: RequestInit): Promise { + return runOwnedProdAsyncMutation(() => + fetch(input, { + ...init, + signal: init.signal ?? AbortSignal.timeout(REMOTE_MUTATION_TIMEOUT_MS), + }), + ); +} + +async function fetchProtocolV3SmokeJson( + config: ProtocolV3SmokeConfig, + path: string, + init: RequestInit, +): Promise> { + const headers = new Headers(init.headers); + headers.set("Accept", "application/json"); + headers.set("Authorization", `Bearer ${config.token}`); + if (init.body !== undefined) headers.set("Content-Type", "application/json"); + + const response = await fetchProdMutation(`${PROD_PUBLIC_API_URL}${path}`, { + ...init, + headers, + signal: AbortSignal.timeout(30_000), + }); + const body = requireJsonRecord(await response.json(), "Protocol v3 smoke response"); + + if (!response.ok) { + throw new Error( + `Protocol v3 smoke ${init.method ?? "GET"} ${path} returned HTTP ${response.status}.`, + ); + } + + return body; +} + +async function readProtocolV3SmokeAccountId(config: ProtocolV3SmokeConfig): Promise { + const response = await fetch(PROD_GRAPHQL_URL, { + body: JSON.stringify({ query: "query ProtocolV3SmokeViewer { viewer { account { id } } }" }), + headers: { + Accept: "application/json", + Authorization: `Bearer ${config.token}`, + "Content-Type": "application/json", + }, + method: "POST", + signal: AbortSignal.timeout(30_000), + }); + const body = requireJsonRecord(await response.json(), "Protocol v3 smoke viewer response"); + + if (!response.ok || (Array.isArray(body.errors) && body.errors.length > 0)) { + throw new Error(`Protocol v3 smoke viewer query returned HTTP ${response.status}.`); + } + + const data = requireJsonRecord(body.data, "Protocol v3 smoke viewer data"); + const viewer = requireJsonRecord(data.viewer, "Protocol v3 smoke viewer"); + const account = requireJsonRecord(viewer.account, "Protocol v3 smoke account"); + const accountId = account.id; + + if (typeof accountId !== "string" || !PLATFORM_ID_PATTERN.test(accountId)) { + throw new Error("Protocol v3 smoke PAT did not resolve to a valid account ID."); + } + + return accountId; +} + +async function createProtocolV3SmokeThread( + config: ProtocolV3SmokeConfig, + idempotencyKey: string, +): Promise { + let lastError: unknown = new Error("Protocol v3 smoke create was not attempted."); + + for (let attempt = 1; attempt <= 3; attempt += 1) { + try { + const body = await fetchProtocolV3SmokeJson( + config, + `/agents/${encodeURIComponent(config.agentId)}/threads`, + { + body: JSON.stringify({ userId: idempotencyKey }), + headers: { "Idempotency-Key": idempotencyKey }, + method: "POST", + }, + ); + const thread = requireJsonRecord(body.thread, "Protocol v3 smoke Thread"); + const threadId = thread.id; + + if (body.run !== null) { + throw new Error("Protocol v3 empty smoke Thread unexpectedly created a Run."); + } + if (typeof threadId !== "string" || !PLATFORM_ID_PATTERN.test(threadId)) { + throw new Error("Protocol v3 smoke create response is missing a valid Thread ID."); + } + + return threadId; + } catch (error) { + lastError = error; + if (attempt < 3) await sleep(POLL_INTERVAL_MS); + } + } + + throw new Error("Protocol v3 smoke Thread creation failed after idempotent retries.", { + cause: lastError, + }); +} + +async function waitForProtocolV3SmokeReady(sessionId: string): Promise { + const deadline = Date.now() + CUTOVER_SMOKE_TIMEOUT_MS; + + while (true) { + const status = parseProtocolV3SmokeStatus( + executeProdD1Json(protocolV3SmokeStatusSql(sessionId)), + ); + + if (isProtocolV3SmokeReady(status)) { + writeStdout( + ` live Driver ${status.driverVersion} completed protocol v3 boot, hello, and ready`, + ); + return; + } + if (status.driverStatus === "failed" || status.driverStatus === "stopped") { + throw new Error( + `Protocol v3 smoke Driver became ${status.driverStatus} before completing hello and ready.`, + ); + } + if (Date.now() >= deadline) { + throw new Error("Protocol v3 live Driver smoke did not reach ready within five minutes."); + } + + await sleep(POLL_INTERVAL_MS); + } +} + +async function deleteProtocolV3SmokeThread( + config: ProtocolV3SmokeConfig, + threadId: string, +): Promise { + let lastError: unknown = new Error("Protocol v3 smoke cleanup was not attempted."); + + for (let attempt = 1; attempt <= 3; attempt += 1) { + try { + const response = await fetchProdMutation( + `${PROD_PUBLIC_API_URL}/threads/${encodeURIComponent(threadId)}`, + { + headers: { + Accept: "application/json", + Authorization: `Bearer ${config.token}`, + }, + method: "DELETE", + signal: AbortSignal.timeout(30_000), + }, + ); + if (response.ok || response.status === 404) return; + throw new Error(`Protocol v3 smoke DELETE returned HTTP ${response.status}.`); + } catch (error) { + lastError = error; + if (attempt < 3) await sleep(POLL_INTERVAL_MS); + } + } + + throw new Error(`Protocol v3 smoke Thread ${threadId} cleanup failed after retries.`, { + cause: lastError, + }); +} + +function readStoredProtocolV3SmokeSession(): string | null { + return parseStoredProtocolV3SmokeSession(executeProdD1Json(PROTOCOL_V3_SMOKE_SESSION_SQL)); +} + +function readStoredProtocolV3SmokeRequestKey(): string | null { + return parseStoredProtocolV3SmokeRequestKey(executeProdD1Json(PROTOCOL_V3_SMOKE_REQUEST_KEY_SQL)); +} + +async function cleanInterruptedProtocolV3Smoke(config: ProtocolV3SmokeConfig): Promise { + let sessionId = readStoredProtocolV3SmokeSession(); + const requestKey = readStoredProtocolV3SmokeRequestKey(); + + if (sessionId === null && requestKey !== null) { + writeStdout(` recovering interrupted protocol v3 smoke request ${requestKey}`); + sessionId = await createProtocolV3SmokeThread(config, requestKey); + executeProdD1(storeProtocolV3SmokeSessionSql(sessionId)); + } + if (sessionId === null) return; + + writeStdout(` cleaning interrupted protocol v3 smoke Session ${sessionId}`); + await deleteProtocolV3SmokeThread(config, sessionId); + executeProdD1(CLOSE_PROTOCOL_V3_SMOKE_WINDOW_SQL); +} + +async function runProtocolV3LiveSmoke( + config: ProtocolV3SmokeConfig, + closedGate: boolean, +): Promise { + let threadId: string | null = null; + let failure: unknown = null; + const cleanupFailures: unknown[] = []; + + if (closedGate) { + const accountId = await readProtocolV3SmokeAccountId(config); + executeProdD1(openProtocolV3SmokeWindowSql(accountId)); + if ( + readStoredProtocolV3SmokeSession() !== null || + readStoredProtocolV3SmokeRequestKey() !== null + ) { + await cleanInterruptedProtocolV3Smoke(config); + executeProdD1(openProtocolV3SmokeWindowSql(accountId)); + } + } + + try { + const requestKey = `protocol-v3-cutover-${crypto.randomUUID()}`; + if (closedGate) executeProdD1(storeProtocolV3SmokeRequestKeySql(requestKey)); + threadId = await createProtocolV3SmokeThread(config, requestKey); + if (closedGate) executeProdD1(storeProtocolV3SmokeSessionSql(threadId)); + await waitForProtocolV3SmokeReady(threadId); + } catch (error) { + failure = error; + } finally { + if (threadId !== null) { + try { + await deleteProtocolV3SmokeThread(config, threadId); + } catch (error) { + cleanupFailures.push(error); + } + } + + if (closedGate && cleanupFailures.length === 0 && threadId !== null) { + try { + executeProdD1(CLOSE_PROTOCOL_V3_SMOKE_WINDOW_SQL); + if (!parseProtocolV3CommandFreeze(executeProdD1Json(PROTOCOL_V3_COMMAND_FREEZE_SQL))) { + cleanupFailures.push( + new Error("Protocol v3 command freeze did not close after live smoke."), + ); + } + } catch (error) { + cleanupFailures.push(error); + } + } else if (closedGate) { + writeStdout( + `✗ Retaining the smoke allowance for cleanup recovery${threadId === null ? "." : ` of Session ${threadId}.`}`, + ); + } + } + + if (failure !== null || cleanupFailures.length > 0) { + throw new AggregateError( + [...(failure === null ? [] : [failure]), ...cleanupFailures], + "Protocol v3 live Driver smoke or cleanup failed.", + ); + } +} + +async function deployWorkerAndVerify( + smokeConfig: ProtocolV3SmokeConfig, + queueApiConfig: ProdQueueApiConfig, + closedGate: boolean, + releaseTreeOid: string, + cutoverState: ProtocolV3CutoverState | null, + workflowMode: ProdWfpWorkflowMode, +): Promise { + let state = cutoverState; + let workerVersionId = state?.workerVersionId ?? null; + let containerApplicationVersion = state?.containerApplicationVersion ?? null; + let containerImageDigest = state?.containerImageDigest ?? null; + const cloudflare = new Cloudflare({ apiToken: queueApiConfig.apiToken }); + + if ( + workerVersionId === null || + containerApplicationVersion === null || + containerImageDigest === null + ) { + const previousApplication = await readProdContainerApplication(queueApiConfig); + writeStdout("▶ Deploying protocol v3 Worker and Driver image"); + assertReleaseTreeUnchanged(releaseTreeOid); + const namespace = await readProdWfpNamespace(cloudflare, queueApiConfig); + assertProdWfpNamespaceExact(namespace, PROD_APP_DISPATCH_NAMESPACE, false); + const workflowBoundary = await readProdAppDeploymentWorkflowBoundary( + cloudflare, + queueApiConfig, + ); + const currentWorkflowMode = assertProdWfpWorkflowBoundary( + workflowBoundary.workflow, + workflowBoundary.bindings, + prodWfpInfrastructureConfig(queueApiConfig, false, workflowMode === "repair"), + ); + if (currentWorkflowMode !== workflowMode) { + throw new Error("Production App deployment Workflow changed before Worker deployment."); + } + assertProdWfpWorkflowDeployGate(workflowMode, closedGate); + runProdMutation(apiWorkerDeployArgs(protocolV3ReleaseTag(releaseTreeOid))); + assertReleaseTreeUnchanged(releaseTreeOid); + workerVersionId = readExactProdWorkerVersion(releaseTreeOid); + containerImageDigest = readTaggedContainerImageDigest(previousApplication, workerVersionId); + const application = await waitForContainerRollout( + queueApiConfig, + containerImageDigest, + previousApplication.version, + null, + ); + containerApplicationVersion = application.version; + } else { + if (state === null) throw new Error("Stored protocol v3 rollout state is missing."); + writeStdout("▶ Reusing the exact protocol v3 Worker and Container rollout"); + await assertPublishedProtocolV3Release(state, releaseTreeOid, queueApiConfig); + await waitForContainerRollout( + queueApiConfig, + containerImageDigest, + containerApplicationVersion, + containerApplicationVersion, + ); + } + + const publishedWorkflowBoundary = await readProdAppDeploymentWorkflowBoundary( + cloudflare, + queueApiConfig, + ); + if ( + assertProdWfpWorkflowBoundary( + publishedWorkflowBoundary.workflow, + publishedWorkflowBoundary.bindings, + prodWfpInfrastructureConfig(queueApiConfig, false), + ) !== "exact" + ) { + throw new Error("Published Worker does not expose the exact App deployment Workflow."); + } + + writeStdout("▶ Verifying production Worker, D1, and local protocol contract"); + assertProtocolV3Artifacts(); + await verifyProdHealth(); + + writeStdout("▶ Re-probing the production WfP HTTP, streaming, cancellation, and WebSocket path"); + await verifyProdWfpGatewayProbe(PROD_APP_DEPLOYMENT_DOMAIN, { + fetch: globalThis.fetch, + probeWebSocket: probeProdWfpWebSocket, + }); + + writeStdout("▶ Running a live protocol v3 Driver boot, hello, and ready smoke"); + await runProtocolV3LiveSmoke(smokeConfig, closedGate); + + if (readExactProdWorkerVersion(releaseTreeOid) !== workerVersionId) { + throw new Error("Production Worker moved away from the published protocol v3 version."); + } + const application = await readProdContainerApplication(queueApiConfig); + if ( + application.version !== containerApplicationVersion || + application.imageDigest !== containerImageDigest || + readTaggedContainerImageDigest(application, workerVersionId) !== containerImageDigest + ) { + throw new Error("Production Container rollout changed before release binding."); + } + + if (state !== null && state.workerVersionId === null) { + executeProdD1( + storeProtocolV3RolloutSql( + releaseTreeOid, + workerVersionId, + containerApplicationVersion, + containerImageDigest, + ), + ); + state = readProtocolV3CutoverState(); + } + if (state !== null) { + await assertPublishedProtocolV3Release(state, releaseTreeOid, queueApiConfig); + } + return state; +} + +async function runProtocolV3Cutover( + initialPendingMigrations: readonly string[], + localMigrationNames: readonly string[], + smokeConfig: ProtocolV3SmokeConfig, + queueApiConfig: ProdQueueApiConfig, + expectedProdSchema: ProdSchemaCatalog, + releaseTreeOid: string, + workflowMode: ProdWfpWorkflowMode, +): Promise { + let bookmark: string | null = null; + let migrationStarted = true; + let queuesVerified = false; + const durableMcpMigrationPending = initialPendingMigrations.includes(PROTOCOL_V3_MIGRATION); + const sessionEventMigrationPending = initialPendingMigrations.includes( + PROTOCOL_V3_SESSION_EVENT_MIGRATION, + ); + const postRuntimeAuthorityMigration = !initialPendingMigrations.includes( + PROTOCOL_V3_RUNTIME_AUTHORITY_MIGRATION, + ); + + try { + writeStdout("▶ Installing the one-shot D1 admission gate"); + let cutoverState = installProtocolV3CutoverGate(releaseTreeOid, postRuntimeAuthorityMigration); + migrationStarted = cutoverState.migrationStarted; + if (cutoverState.phase === "queues_resuming") { + if (initialPendingMigrations.length > 0) { + throw new Error( + "Protocol v3 queue-resume phase exists before every migration was applied.", + ); + } + await assertPublishedProtocolV3Release(cutoverState, releaseTreeOid, queueApiConfig); + queuesVerified = !cutoverState.enabled; + writeStdout( + `▶ Recovering the durable production queue-resume ${cutoverState.enabled ? "pre-acceptance" : "accepted"} phase`, + ); + await completeProtocolV3QueueResume(cutoverState, { + commitAcceptance: acceptProtocolV3QueueResume, + removeMarker: removeProtocolV3CutoverGate, + resumeAndVerifyQueues: async () => { + await resumeAndVerifyProdQueues(queueApiConfig); + queuesVerified = true; + }, + }); + return; + } + + writeStdout("▶ Pausing production queue delivery for protocol v3 cutover"); + await pauseAndVerifyProdQueues(queueApiConfig); + + if (initialPendingMigrations.length > 0) { + enterProtocolV3Drain(); + + writeStdout("▶ Letting every already-admitted API command lane reach a terminal state"); + await resumeAndVerifyProdQueues(queueApiConfig); + + writeStdout("▶ Draining protocol v2 runtime state while control commands remain available"); + await waitForProtocolV3Drain(postRuntimeAuthorityMigration, false); + + writeStdout("▶ Freezing all new Driver commands at the final zero-state boundary"); + enableProtocolV3CommandFreeze(); + await waitForProtocolV3Drain(postRuntimeAuthorityMigration); + + writeStdout("▶ Re-pausing every API command lane after all admitted work is terminal"); + await pauseAndVerifyProdQueues(queueApiConfig); + await waitForProtocolV3Drain(postRuntimeAuthorityMigration); + } else { + writeStdout("▶ Recovering any interrupted protocol v3 smoke before roll-forward"); + await cleanInterruptedProtocolV3Smoke(smokeConfig); + executeProdD1(CLOSE_PROTOCOL_V3_SMOKE_WINDOW_SQL); + enableProtocolV3CommandFreeze(); + writeStdout("▶ Re-proving the complete closed runtime boundary before roll-forward"); + await waitForProtocolV3Drain(postRuntimeAuthorityMigration); + } + + if (durableMcpMigrationPending) { + writeStdout("▶ Preflighting migration 0013 for lossy historical rewrites"); + verifyProdLossyMigrationInventory(); + } + let legacyRewriteProof: LegacyRewriteProof | null = null; + if (sessionEventMigrationPending) { + writeStdout("▶ Preflighting legacy terminal integrity before migration 0014"); + legacyRewriteProof = preflightProdLegacyTerminalIntegrity(); + } + if (initialPendingMigrations.includes(PROTOCOL_V3_RUNTIME_AUTHORITY_MIGRATION)) { + writeStdout("▶ Preflighting runtime authority identities and terminal backups"); + assertProtocolV3RuntimeAuthorityPreflight( + parseProtocolV3RuntimeAuthorityPreflight( + executeProdD1Json( + protocolV3RuntimeAuthorityPreflightSql( + initialPendingMigrations.includes(PROTOCOL_V3_SESSION_CLEANUP_MIGRATION), + ), + ), + ), + ); + } + + bookmark = + initialPendingMigrations.length > 0 + ? (readStoredProtocolV3Bookmark() ?? ensureProtocolV3Bookmark()) + : readStoredProtocolV3Bookmark(); + if (bookmark === null) { + writeStdout( + " No emergency backup bookmark is available; this does not block roll-forward of the exact v3 release.", + ); + } else { + printProtocolV3EmergencyBookmark(bookmark); + } + + writeStdout("▶ Re-pausing and independently verifying every production queue before migration"); + await pauseAndVerifyProdQueues(queueApiConfig); + writeStdout("▶ Re-verifying the exact admission gate before migration"); + assertProtocolV3CutoverGateExact(); + assertReleaseTreeUnchanged(releaseTreeOid); + + if (initialPendingMigrations.length > 0) { + writeStdout("▶ Persisting the irreversible D1 migration intent"); + migrationStarted = true; + cutoverState = beginProtocolV3Migration(releaseTreeOid); + } + if (sessionEventMigrationPending) { + if (bookmark === null) { + throw new Error("Migration 0014 requires a persisted pre-migration D1 bookmark."); + } + writeStdout("▶ Authorizing the exact drained legacy terminal rewrite set"); + if (legacyRewriteProof === null) { + throw new Error("Migration 0014 legacy rewrite preflight evidence is missing."); + } + authorizeProdLegacyTerminalRewrite(legacyRewriteProof, releaseTreeOid, bookmark); + } + writeStdout("▶ Applying pending D1 migrations behind the closed gate"); + applyD1Migrations(); + assertReleaseTreeUnchanged(releaseTreeOid); + const remainingMigrations = readPendingProdMigrations(localMigrationNames); + if (remainingMigrations.length > 0) { + throw new Error( + `Production D1 migrations remain pending behind the closed gate: ${remainingMigrations.join(", ")}.`, + ); + } + + assertProtocolV3CutoverGateExact(); + + writeStdout("▶ Verifying prod D1 schema matches the latest migration snapshot"); + assertProdSchemaMatchesSnapshot(expectedProdSchema); + + cutoverState = + (await deployWorkerAndVerify( + smokeConfig, + queueApiConfig, + true, + releaseTreeOid, + readProtocolV3CutoverState(), + workflowMode, + )) ?? cutoverState; + + writeStdout("▶ Rechecking the closed runtime boundary"); + await waitForProtocolV3Drain(true); + + writeStdout("▶ Persisting the durable production queue-resume phase"); + const queueResumeState = enterProtocolV3QueuesResuming(); + + await completeProtocolV3QueueResume(queueResumeState, { + commitAcceptance: acceptProtocolV3QueueResume, + removeMarker: removeProtocolV3CutoverGate, + resumeAndVerifyQueues: async () => { + writeStdout("▶ Resuming and verifying production queues"); + await resumeAndVerifyProdQueues(queueApiConfig); + queuesVerified = true; + }, + }); + } catch (originalError) { + return recoverProtocolV3CutoverFailure( + { bookmark, initialPendingMigrations, migrationStarted, originalError, queuesVerified }, + { + commitQueueAcceptance: acceptProtocolV3QueueResume, + pauseAndVerifyQueues: () => pauseAndVerifyProdQueues(queueApiConfig), + printBookmark: printProtocolV3EmergencyBookmark, + probe: probeProtocolV3Cutover, + readBookmark: readStoredProtocolV3Bookmark, + readPendingMigrations: () => readPendingProdMigrations(localMigrationNames), + removeMarker: removeProtocolV3CutoverGate, + resumeAndVerifyQueues: () => resumeAndVerifyProdQueues(queueApiConfig), + write: writeStdout, + }, + ); + } +} + +async function deployProduction(): Promise { + const expectedProdSchema = loadExpectedProdSchema(); + const localMigrationNames = readLocalMigrationNames(); + assertCutoverMigrationJournalAudited(localMigrationNames); + const releaseTreeOid = readCleanReleaseTreeOid(); + + runLocalPreflight(releaseTreeOid); + const smokeConfig = readProtocolV3SmokeConfig(); + const queueApiConfig = readProdQueueApiConfig(); + const initialCutover = probeProtocolV3Cutover(); + if (initialCutover.gatePresent) { + assertProtocolV3CutoverGateExact(); + assertProtocolV3Release(readProtocolV3CutoverState(), releaseTreeOid); + } + + writeStdout("▶ Verifying pre-provisioned production WfP, DNS, route, binding, and TLS"); + const appDeploymentScriptLedgerPresent = readAppDeploymentScriptLedgerPresent(); + const cloudflare = new Cloudflare({ apiToken: queueApiConfig.apiToken }); + const workflowMode = assertProdWfpReadOnlyInfrastructure( + await readProdWfpReadOnlyInventory(cloudflare, queueApiConfig), + prodWfpInfrastructureConfig( + queueApiConfig, + !appDeploymentScriptLedgerPresent, + initialCutover.gatePresent, + ), + Date.now(), + ); + if (workflowMode !== "exact") { + writeStdout( + `▶ App deployment Workflow needs closed-gate ${workflowMode}; forcing exact roll-forward`, + ); + } + + if (shouldProbeProdWfpBeforeMutation(workflowMode)) { + writeStdout("▶ Probing the pre-existing production WfP data path before any mutation"); + await verifyProdWfpGatewayProbe(PROD_APP_DEPLOYMENT_DOMAIN, { + fetch: globalThis.fetch, + probeWebSocket: probeProdWfpWebSocket, + }); + } else { + writeStdout("▶ Deferring the first WfP data-path probe until the closed-gate candidate exists"); + } -writeStdout("▶ Applying pending D1 migrations"); -applyD1Migrations(); + writeStdout("▶ Verifying the dedicated production smoke Agent is published cattle"); + assertProtocolV3SmokeAgent(executeProdD1Json(protocolV3SmokeAgentSql(smokeConfig.agentId))); -writeStdout("▶ Verifying prod D1 schema matches the latest migration snapshot"); -await assertProdSchemaMatchesMigrations(expectedMigrationTables).catch((error: unknown) => { - writeStdout(error instanceof Error ? error.message : String(error)); - process.exit(1); -}); + writeStdout("▶ Acquiring the durable production deploy lease"); + const deployOwner = crypto.randomUUID(); + writeStdout(` production deploy owner: ${deployOwner}`); + try { + acquireProdDeployLease(deployOwner, executeProdDeployLease); + } catch (error) { + writeStdout( + `✗ Production deploy lease ownership was not proven for ${deployOwner}; a timed-out acquisition may still commit, so verify it is quiescent before any exact-owner manual release.`, + ); + throw error; + } + + try { + writeStdout("▶ Proving Workers Scripts Write with the fixed unrouted WfP canary"); + await verifyProdWfpWriteCanary( + createProdWfpWriteCanaryClient(cloudflare, queueApiConfig), + PROD_APP_DISPATCH_NAMESPACE, + PROD_WFP_WRITE_CANARY_SCRIPT, + ); -writeStdout("▶ Ensuring required production queues exist"); -ensureRequiredProdQueues(); + const cutover = probeProtocolV3Cutover(); + const pendingMigrations = readPendingProdMigrations(localMigrationNames); + if (cutover.gatePresent) { + assertProtocolV3CutoverGateExact(); + } -writeStdout("▶ Building driver"); -runVp(["run", "--filter", "agent-driver", "build"]); + writeStdout("▶ Ensuring required production queues exist"); + ensureRequiredProdQueues(); -writeStdout("▶ Deploying worker"); -run(["deploy", "--env", ENV, "--minify", "--containers-rollout", "immediate"]); + if (cutover.gatePresent || pendingMigrations.length > 0 || workflowMode !== "exact") { + await runProtocolV3Cutover( + pendingMigrations, + localMigrationNames, + smokeConfig, + queueApiConfig, + expectedProdSchema, + releaseTreeOid, + workflowMode, + ); + } else { + writeStdout("▶ Verifying prod D1 schema matches the latest migration snapshot"); + assertProdSchemaMatchesSnapshot(expectedProdSchema); + + await deployWorkerAndVerify( + smokeConfig, + queueApiConfig, + false, + releaseTreeOid, + null, + workflowMode, + ); + } + } catch (error) { + writeStdout( + `✗ Retaining production deploy lease ${deployOwner}; verify that every remote mutation is quiescent before an exact-owner manual release.`, + ); + throw error; + } -writeStdout("✓ deploy complete"); + releaseProdDeployLease(); + writeStdout("✓ deploy complete"); +} + +async function main(): Promise { + const args = process.argv.slice(2); + if (args.length === 0) { + return deployProduction(); + } + if (args.length === 1 && args[0] === "--protocol-v3-legacy-inventory") { + writeStdout("▶ Auditing legacy production terminal source identities (read-only)"); + verifyProdLegacyTerminalSourceInventory(); + writeStdout("✓ legacy production terminal sources can be normalized deterministically"); + return; + } + if (args.length === 1 && args[0] === "--protocol-v3-lossy-migration-inventory") { + writeStdout("▶ Auditing migration 0013 production history (read-only)"); + verifyProdLossyMigrationInventory(); + writeStdout("✓ migration 0013 has no lossy production candidates"); + return; + } + throw new Error( + "Usage: bun apps/api/bin/deploy-prod.ts [--protocol-v3-legacy-inventory|--protocol-v3-lossy-migration-inventory]", + ); +} + +if (import.meta.main) { + await main().catch((error: unknown) => { + writeStdout(error instanceof Error ? error.message : String(error)); + process.exitCode = 1; + }); +} diff --git a/apps/api/bin/prod-deploy-lease.ts b/apps/api/bin/prod-deploy-lease.ts new file mode 100644 index 00000000..59d5f80c --- /dev/null +++ b/apps/api/bin/prod-deploy-lease.ts @@ -0,0 +1,79 @@ +import { parseD1JsonResults } from "./d1-json"; + +export const PROD_DEPLOY_LEASE_TABLE = "__production_deploy_lease"; + +export const PROD_DEPLOY_LEASE_TABLE_SQL = `CREATE TABLE "${PROD_DEPLOY_LEASE_TABLE}" ( + "id" integer PRIMARY KEY CHECK ("id" = 1), + "owner" text NOT NULL +)`; + +function ownerSql(owner: string): string { + if (!/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/u.test(owner)) { + throw new Error("Production deploy lease owner must be a UUID v4."); + } + return `'${owner}'`; +} + +const LEASE_READBACK_SQL = `(SELECT "sql" FROM "sqlite_master" + WHERE "type" = 'table' AND "name" = '${PROD_DEPLOY_LEASE_TABLE}')`; +const LEASE_TRIGGER_COUNT_SQL = `(SELECT count(*) FROM "sqlite_master" + WHERE "type" = 'trigger' AND "tbl_name" = '${PROD_DEPLOY_LEASE_TABLE}' COLLATE BINARY)`; +const LEASE_SCHEMA_PREDICATE_SQL = `${LEASE_READBACK_SQL} = '${PROD_DEPLOY_LEASE_TABLE_SQL.replaceAll("'", "''")}' COLLATE BINARY + AND ${LEASE_TRIGGER_COUNT_SQL} = 0`; +const LEASE_FINAL_READ_SQL = `SELECT + "lease"."owner", + ${LEASE_READBACK_SQL} AS "table_sql", + ${LEASE_TRIGGER_COUNT_SQL} AS "trigger_count" +FROM (SELECT 1) AS "singleton" +LEFT JOIN "${PROD_DEPLOY_LEASE_TABLE}" AS "lease" ON "lease"."id" = 1`; + +export function acquireProdDeployLeaseStatements(owner: string): readonly string[] { + const quotedOwner = ownerSql(owner); + return [ + PROD_DEPLOY_LEASE_TABLE_SQL.replace("CREATE TABLE", "CREATE TABLE IF NOT EXISTS"), + `INSERT INTO "${PROD_DEPLOY_LEASE_TABLE}" ("id", "owner") +SELECT 1, ${quotedOwner} +WHERE ${LEASE_SCHEMA_PREDICATE_SQL} +ON CONFLICT ("id") DO NOTHING`, + LEASE_FINAL_READ_SQL, + ]; +} + +export function verifyProdDeployLeaseStatements(): readonly string[] { + return [LEASE_FINAL_READ_SQL]; +} + +export function releaseProdDeployLeaseStatements(owner: string): readonly string[] { + return [ + `DELETE FROM "${PROD_DEPLOY_LEASE_TABLE}" +WHERE "id" = 1 AND "owner" = ${ownerSql(owner)} + AND ${LEASE_SCHEMA_PREDICATE_SQL}`, + `SELECT "owner" FROM "${PROD_DEPLOY_LEASE_TABLE}" WHERE "id" = 1`, + ]; +} + +export function assertProdDeployLeaseOwned(raw: string, owner: string): void { + ownerSql(owner); + const statements = parseD1JsonResults(raw); + const readback = statements.length === 1 || statements.length === 3 ? statements.at(-1) : null; + const row = readback?.length === 1 ? readback[0] : undefined; + if (row?.table_sql !== PROD_DEPLOY_LEASE_TABLE_SQL) { + throw new Error("Production deploy lease table schema is invalid."); + } + if (row.trigger_count !== 0) { + throw new Error("Production deploy lease table must not have triggers."); + } + if (row.owner !== owner) throw new Error("Production deploy lease is held by another owner."); +} + +function releaseReadback(raw: string) { + const statements = parseD1JsonResults(raw); + return statements.length === 2 ? statements[1] : undefined; +} + +export function assertProdDeployLeaseReleased(raw: string): void { + const remaining = releaseReadback(raw); + if (!remaining || remaining.length !== 0) { + throw new Error("Production deploy lease release did not remove this owner."); + } +} diff --git a/apps/api/bin/prod-schema-guard.ts b/apps/api/bin/prod-schema-guard.ts index 243bacbf..bc63d170 100644 --- a/apps/api/bin/prod-schema-guard.ts +++ b/apps/api/bin/prod-schema-guard.ts @@ -1,97 +1 @@ -/** - * Fail-fast guard against the DEPLOY-D1-001 hazard. - * - * `wrangler d1 migrations apply` records applied migrations by filename. This - * guard detects damage from skipped, rewritten, or incomplete migrations by - * comparing the live database with the latest Drizzle schema snapshot. Applied - * migrations are immutable; every production schema change must use a new file. - * - * These functions are pure (no I/O) so the deploy script can stay thin and the - * detection logic stays unit-testable. Table-level only: this catches a missing - * table — the catastrophic case where every query against it fails — not an - * added column on an existing table. - */ - -interface DrizzleJournal { - entries?: unknown; -} - -interface DrizzleSnapshot { - tables?: unknown; -} - -/** Latest snapshot filename recorded by Drizzle's migration journal. */ -export function getLatestSnapshotFilename(rawJournal: string): string { - const journal = JSON.parse(rawJournal) as DrizzleJournal | null; - const entries: readonly unknown[] = Array.isArray(journal?.entries) ? journal.entries : []; - - if (entries.length === 0) { - throw new Error("Drizzle migration journal has no valid latest entry."); - } - - for (const [expectedIndex, entry] of entries.entries()) { - const index = - typeof entry === "object" && entry !== null && "idx" in entry ? entry.idx : undefined; - - if (index !== expectedIndex) { - throw new Error("Drizzle migration journal indexes must be contiguous from zero."); - } - } - - return `${String(entries.length - 1).padStart(4, "0")}_snapshot.json`; -} - -/** Table names declared by the latest Drizzle schema snapshot. */ -export function parseExpectedTableNames(rawSnapshot: string): string[] { - const snapshot = JSON.parse(rawSnapshot) as DrizzleSnapshot | null; - - if ( - snapshot === null || - typeof snapshot.tables !== "object" || - snapshot.tables === null || - Array.isArray(snapshot.tables) - ) { - throw new Error("Drizzle schema snapshot has no valid tables object."); - } - - const tableNames = Object.keys(snapshot.tables).toSorted(); - - if (tableNames.length === 0) { - throw new Error("Drizzle schema snapshot has no tables."); - } - - return tableNames; -} - -/** Expected tables that are not present in the live database. */ -export function findMissingProdTables( - expectedTableNames: readonly string[], - liveTableNames: readonly string[], -): string[] { - const live = new Set(liveTableNames); - return expectedTableNames.filter((name) => !live.has(name)); -} - -interface D1ExecuteResult { - results?: ReadonlyArray<{ name?: unknown }>; -} - -/** - * Extract `name` rows from `wrangler d1 execute --json` output. The `--json` - * flag prints the result array on stdout; we slice from the first `[` to the - * last `]` to tolerate any leading log lines, then fail loudly on an - * unrecognized shape rather than silently reporting zero tables. - */ -export function extractTableNames(rawStdout: string): string[] { - const start = rawStdout.indexOf("["); - const end = rawStdout.lastIndexOf("]"); - - if (start === -1 || end === -1 || end < start) { - throw new Error(`Could not locate JSON array in \`d1 execute\` output:\n${rawStdout}`); - } - - const parsed = JSON.parse(rawStdout.slice(start, end + 1)) as D1ExecuteResult[]; - const rows = parsed[0]?.results ?? []; - - return rows.map((row) => row.name).filter((name): name is string => typeof name === "string"); -} +export * from "@mosoo/db/deploy-schema-guard"; diff --git a/apps/api/bin/prod-wfp-preflight.ts b/apps/api/bin/prod-wfp-preflight.ts new file mode 100644 index 00000000..59850749 --- /dev/null +++ b/apps/api/bin/prod-wfp-preflight.ts @@ -0,0 +1,622 @@ +import { + APP_DEPLOYMENT_PROBE_HOST_LABEL, + APP_DEPLOYMENT_PROBE_PATH_PREFIX, + APP_DEPLOYMENT_PROBE_SCRIPT_NAME, +} from "../src/modules/apps/application/app-deployment-gateway"; + +export const PROD_APP_DEPLOYMENT_DOMAIN = "apps.mosoo.ai"; +export const PROD_APP_DEPLOYMENT_WORKFLOW = "mosoo-app-deployment-prod"; +export const PROD_APP_DEPLOYMENT_WORKFLOW_BINDING = "APP_DEPLOYMENT_WORKFLOW"; +export const PROD_APP_DEPLOYMENT_WORKFLOW_CLASS = "AppDeploymentWorkflow"; +export const PROD_APP_DISPATCH_NAMESPACE = "mosoo-app-deployments-prod"; +export const PROD_APP_WILDCARD_DNS = `*.${PROD_APP_DEPLOYMENT_DOMAIN}`; +export const PROD_APP_WILDCARD_ROUTE = `${PROD_APP_WILDCARD_DNS}/*`; +export const PROD_API_WORKER_NAME = "mosoo-api-prod"; +export const PROD_WFP_WRITE_CANARY_SCRIPT = "mosoo-release-write-canary"; +export const WRANGLER_DISABLE_AUTO_PROVISION = "--experimental-provision=false"; + +const CERTIFICATE_MINIMUM_VALIDITY_MS = 30 * 24 * 60 * 60 * 1_000; +const PROBE_TIMEOUT_MS = 10_000; + +export interface WfpNamespaceInventory { + id: string | null; + name: string | null; + scriptCount: number | null; + trustedWorkers: boolean | null; +} + +export interface WfpZoneInventory { + accountId: string | null; + id: string | null; + name: string | null; + status: string | null; +} + +export interface WfpDnsRecordInventory { + name: string | null; + proxied: boolean | null; + type: string | null; +} + +export interface WfpCertificateInventory { + certificates: readonly { + expiresOn: string | null; + hosts: readonly string[]; + status: string | null; + }[]; + hosts: readonly string[]; + status: string | null; +} + +export interface WfpWorkflowInventory { + className: string | null; + id: string | null; + name: string | null; + scriptName: string | null; +} + +export interface WfpScriptInventory { + id: string | null; + namespace: string | null; +} + +export interface WfpWorkerBindingInventory { + className: string | null; + name: string | null; + namespace: string | null; + scriptName: string | null; + type: string | null; + workflowName: string | null; +} + +export type ProdWfpWorkflowMode = "bootstrap" | "exact" | "repair"; + +export interface ProdWfpReadOnlyInventory { + apiWorkerBindings: readonly WfpWorkerBindingInventory[]; + certificates: readonly WfpCertificateInventory[]; + dnsRecords: readonly WfpDnsRecordInventory[]; + legacyResources: readonly string[]; + namespace: WfpNamespaceInventory; + probeScript: WfpScriptInventory; + routes: readonly { + pattern: string | null; + script: string | null; + }[]; + workflow: WfpWorkflowInventory; + zone: WfpZoneInventory; +} + +export interface ProdWfpInfrastructureConfig { + accountId: string; + apiWorkerName: string; + dispatchBinding: string; + dispatchNamespace: string; + requireProbeOnlyNamespace: boolean; + wildcardDns: string; + wildcardRoute: string; + workflowBinding: string; + workflowClass: string; + workflowName: string; + workflowRepairAllowed: boolean; + zoneId: string; + zoneName: string; +} + +export interface WfpWriteCanaryClient { + deleteScript(scriptName: string): Promise; + readScript(scriptName: string): Promise<{ id: string | null; namespace: string | null } | null>; + uploadScript(scriptName: string): Promise<{ id: string | null }>; +} + +export function apiWorkerDryRunArgs(): string[] { + return [ + "deploy", + "--env", + "prod", + "--minify", + "--strict", + "--dry-run", + WRANGLER_DISABLE_AUTO_PROVISION, + ]; +} + +export function apiWorkerDeployArgs(releaseTag: string): string[] { + return [ + "deploy", + "--env", + "prod", + "--minify", + "--strict", + WRANGLER_DISABLE_AUTO_PROVISION, + "--containers-rollout", + "immediate", + "--tag", + releaseTag, + ]; +} + +export function assertProdWfpReadOnlyInfrastructure( + inventory: ProdWfpReadOnlyInventory, + config: ProdWfpInfrastructureConfig, + nowMs: number, +): ProdWfpWorkflowMode { + if ( + inventory.zone.id !== config.zoneId || + inventory.zone.name !== config.zoneName || + inventory.zone.status !== "active" || + inventory.zone.accountId !== config.accountId + ) { + throw new Error("Cloudflare production zone identity or status is not exact."); + } + + assertProdWfpNamespaceExact( + inventory.namespace, + config.dispatchNamespace, + config.requireProbeOnlyNamespace, + ); + assertProdWfpProbeScriptExact(inventory.probeScript, config.dispatchNamespace); + const workflowMode = assertProdWfpWorkflowBoundary( + inventory.workflow, + inventory.apiWorkerBindings, + config, + ); + + const wildcardRoutes = inventory.routes.filter((route) => route.pattern === config.wildcardRoute); + if (wildcardRoutes.length !== 1 || wildcardRoutes[0]?.script !== config.apiWorkerName) { + throw new Error( + `Cloudflare must route ${config.wildcardRoute} exactly once to ${config.apiWorkerName}.`, + ); + } + const deploymentDomain = readWildcardDomain(config.wildcardDns); + const shadowingRoutes = inventory.routes.filter( + (route) => + route.pattern !== config.wildcardRoute && + routeMayMatchDeploymentDomain(route.pattern, deploymentDomain), + ); + if (shadowingRoutes.length > 0) { + throw new Error( + `Cloudflare routes must not shadow ${config.wildcardRoute}: ${shadowingRoutes + .map((route) => `${String(route.pattern)} -> ${route.script ?? "no script"}`) + .join(", ")}.`, + ); + } + + const dispatcherBindings = inventory.apiWorkerBindings.filter( + (binding) => binding.name === config.dispatchBinding, + ); + if ( + dispatcherBindings.length !== 1 || + dispatcherBindings[0]?.type !== "dispatch_namespace" || + dispatcherBindings[0]?.namespace !== config.dispatchNamespace + ) { + throw new Error( + `${config.apiWorkerName} must bind ${config.dispatchBinding} to the exact ${config.dispatchNamespace} dispatch namespace.`, + ); + } + + if (inventory.dnsRecords.some((record) => record.name === null)) { + throw new Error("Cloudflare returned a DNS record without a hostname."); + } + const shadowingRecords = inventory.dnsRecords.filter( + (record) => + record.name !== config.wildcardDns && + record.name !== null && + normalizeDnsName(record.name).endsWith(`.${deploymentDomain}`), + ); + if (shadowingRecords.length > 0) { + throw new Error( + `Cloudflare DNS records must not shadow ${config.wildcardDns}: ${shadowingRecords + .map((record) => `${String(record.type)} ${String(record.name)}`) + .join(", ")}.`, + ); + } + const trafficRecords = inventory.dnsRecords.filter( + (record) => + record.name === config.wildcardDns && + (record.type === "A" || record.type === "AAAA" || record.type === "CNAME"), + ); + if (trafficRecords.length === 0 || trafficRecords.some((record) => record.proxied !== true)) { + throw new Error( + `Cloudflare DNS must contain only proxied traffic records for ${config.wildcardDns}.`, + ); + } + + const validAfter = nowMs + CERTIFICATE_MINIMUM_VALIDITY_MS; + const hasExactCertificate = inventory.certificates.some( + (pack) => + pack.status === "active" && + pack.hosts.includes(config.wildcardDns) && + pack.certificates.some( + (certificate) => + certificate.status === "active" && + certificate.hosts.includes(config.wildcardDns) && + parseExpiry(certificate.expiresOn) >= validAfter, + ), + ); + if (!hasExactCertificate) { + throw new Error( + `Cloudflare requires an active certificate for ${config.wildcardDns} valid for at least 30 more days.`, + ); + } + + if (inventory.legacyResources.length > 0) { + throw new Error( + `Legacy Mosoo Pages/classic Worker resources must be removed manually before WfP cutover:\n${inventory.legacyResources.map((resource) => `- ${resource}`).join("\n")}`, + ); + } + + return workflowMode; +} + +function readWildcardDomain(wildcardDns: string): string { + if (!wildcardDns.startsWith("*.") || wildcardDns.length === 2) { + throw new Error("Cloudflare WfP wildcard DNS configuration is invalid."); + } + return wildcardDns.slice(2).toLowerCase(); +} + +function normalizeDnsName(name: string): string { + return name.toLowerCase().replace(/\.$/u, ""); +} + +function routeMayMatchDeploymentDomain(pattern: string | null, domain: string): boolean { + if (pattern === null) return true; + const withoutScheme = pattern.replace(/^https?:\/\//iu, ""); + const slash = withoutScheme.indexOf("/"); + const hostname = (slash === -1 ? withoutScheme : withoutScheme.slice(0, slash)) + .toLowerCase() + .replace(/\.$/u, ""); + if (hostname.length === 0 || /[:?#\s]/u.test(hostname)) return true; + if (hostname.endsWith(`.${domain}`)) return true; + return hostname.startsWith("*") && `probe.${domain}`.endsWith(hostname.slice(1)); +} + +export function assertProdWfpNamespaceExact( + namespace: WfpNamespaceInventory, + expectedName: string, + requireProbeOnly: boolean, +): void { + if ( + namespace.id === null || + namespace.id.length === 0 || + namespace.name !== expectedName || + namespace.scriptCount === null || + !Number.isSafeInteger(namespace.scriptCount) || + namespace.scriptCount < 1 || + namespace.trustedWorkers !== false + ) { + throw new Error( + `Workers for Platforms namespace ${expectedName} is missing, mismatched, or trusted. Provision the exact untrusted namespace outside this deploy.`, + ); + } + + if (requireProbeOnly && namespace.scriptCount !== 1) { + throw new Error( + `Bootstrap requires ${expectedName} to contain only the pre-provisioned ${APP_DEPLOYMENT_PROBE_SCRIPT_NAME} script; found ${namespace.scriptCount} scripts. Inspect and clean it manually before retrying.`, + ); + } +} + +export function assertProdWfpProbeScriptExact( + script: WfpScriptInventory, + expectedNamespace: string, +): void { + if (script.id !== APP_DEPLOYMENT_PROBE_SCRIPT_NAME || script.namespace !== expectedNamespace) { + throw new Error( + `Cloudflare requires the pre-provisioned ${APP_DEPLOYMENT_PROBE_SCRIPT_NAME} operational probe in the exact ${expectedNamespace} namespace.`, + ); + } +} + +export function assertProdWfpWorkflowExact( + workflow: WfpWorkflowInventory, + expectedName: string, + expectedClass: string, + expectedScript: string, +): void { + if ( + workflow.id === null || + workflow.id.length === 0 || + workflow.name !== expectedName || + workflow.className !== expectedClass || + workflow.scriptName !== expectedScript + ) { + throw new Error( + `Cloudflare Workflow ${expectedName} is missing or mismatched. Bootstrap the exact ${expectedClass} Workflow on ${expectedScript} outside this deploy.`, + ); + } +} + +export function assertProdWfpWorkflowBoundary( + workflow: WfpWorkflowInventory, + bindings: readonly WfpWorkerBindingInventory[], + expected: Pick< + ProdWfpInfrastructureConfig, + "apiWorkerName" | "workflowBinding" | "workflowClass" | "workflowName" | "workflowRepairAllowed" + >, +): ProdWfpWorkflowMode { + const workflowBindings = bindings.filter((binding) => binding.type === "workflow"); + const workflowAbsent = + workflow.className === null && + workflow.id === null && + workflow.name === null && + workflow.scriptName === null; + + if (workflowAbsent && workflowBindings.length === 0) { + return "bootstrap"; + } + + const workflowExact = + workflow.id !== null && + workflow.id.length > 0 && + workflow.name === expected.workflowName && + workflow.className === expected.workflowClass && + workflow.scriptName === expected.apiWorkerName; + if (workflowExact && workflowBindings.length === 0 && expected.workflowRepairAllowed) { + return "repair"; + } + + assertProdWfpWorkflowExact( + workflow, + expected.workflowName, + expected.workflowClass, + expected.apiWorkerName, + ); + + const exactBindings = workflowBindings.filter( + (binding) => binding.name === expected.workflowBinding, + ); + if ( + workflowBindings.length !== 1 || + exactBindings.length !== 1 || + exactBindings[0]?.workflowName !== expected.workflowName || + exactBindings[0]?.className !== expected.workflowClass || + exactBindings[0]?.scriptName !== expected.apiWorkerName + ) { + throw new Error( + `${expected.apiWorkerName} must bind ${expected.workflowBinding} to the exact ${expected.workflowName} Workflow class ${expected.workflowClass}.`, + ); + } + + return "exact"; +} + +export function assertProdWfpWorkflowDeployGate( + mode: ProdWfpWorkflowMode, + closedGate: boolean, +): void { + if (mode !== "exact" && !closedGate) { + throw new Error( + "App deployment Workflow bootstrap or repair requires the closed production gate.", + ); + } +} + +export function shouldProbeProdWfpBeforeMutation(mode: ProdWfpWorkflowMode): boolean { + return mode === "exact"; +} + +function parseExpiry(value: string | null): number { + if (value === null) return Number.NaN; + return Date.parse(value); +} + +export async function verifyProdWfpWriteCanary( + client: WfpWriteCanaryClient, + dispatchNamespace: string, + scriptName: string, +): Promise { + try { + await client.deleteScript(scriptName); + if ((await client.readScript(scriptName)) !== null) { + throw new Error("The previous WfP write canary still exists after deletion."); + } + } catch (error) { + throw canaryFailure(scriptName, dispatchNamespace, [error]); + } + + let verificationError: unknown = null; + + try { + const uploaded = await client.uploadScript(scriptName); + if (uploaded.id !== scriptName) { + throw new Error( + `WfP write canary uploaded as ${String(uploaded.id)}, expected ${scriptName}.`, + ); + } + + const stored = await client.readScript(scriptName); + if (stored?.id !== scriptName || stored.namespace !== dispatchNamespace) { + throw new Error("WfP write canary readback did not match its exact script and namespace."); + } + } catch (error) { + verificationError = error; + } + + let cleanupError: unknown = null; + try { + await client.deleteScript(scriptName); + if ((await client.readScript(scriptName)) !== null) { + throw new Error("WfP write canary still exists after deletion."); + } + } catch (error) { + cleanupError = error; + } + + if (verificationError !== null || cleanupError !== null) { + throw canaryFailure(scriptName, dispatchNamespace, [ + ...(verificationError === null ? [] : [verificationError]), + ...(cleanupError === null ? [] : [cleanupError]), + ]); + } +} + +function canaryFailure( + scriptName: string, + dispatchNamespace: string, + errors: readonly unknown[], +): AggregateError { + const endpoint = + `https://api.cloudflare.com/client/v4/accounts/$CLOUDFLARE_ACCOUNT_ID/workers/dispatch/` + + `namespaces/${dispatchNamespace}/scripts/${scriptName}`; + return new AggregateError( + errors, + `WfP write canary ${scriptName} failed. Remove it manually with: curl -X DELETE '${endpoint}' -H 'Authorization: Bearer $CLOUDFLARE_API_TOKEN'`, + ); +} + +interface WfpGatewayProbeDependencies { + fetch: typeof fetch; + probeWebSocket(url: string, nonce: string): Promise; +} + +export async function verifyProdWfpGatewayProbe( + appDomain: string, + dependencies: WfpGatewayProbeDependencies, + createNonce: () => string = () => crypto.randomUUID(), +): Promise { + const hostname = `${APP_DEPLOYMENT_PROBE_HOST_LABEL}.${appDomain}`; + const nonce = createNonce(); + + await verifyHttpBodyProbe(hostname, nonce, dependencies.fetch); + await verifyStreamProbe(hostname, nonce, dependencies.fetch); + await verifyCancellationProbe(hostname, nonce, dependencies.fetch); + await verifyHttpBodyProbe(hostname, `${nonce}-after-cancel`, dependencies.fetch); + await dependencies.probeWebSocket( + `wss://${hostname}${APP_DEPLOYMENT_PROBE_PATH_PREFIX}websocket?nonce=${encodeURIComponent(nonce)}`, + nonce, + ); +} + +async function verifyHttpBodyProbe( + hostname: string, + nonce: string, + fetcher: typeof fetch, +): Promise { + const pathname = `${APP_DEPLOYMENT_PROBE_PATH_PREFIX}http`; + const body = `body:${nonce}`; + const response = await fetcher( + `https://${hostname}${pathname}?nonce=${encodeURIComponent(nonce)}`, + { + body, + headers: { "X-Mosoo-Wfp-Probe-Nonce": nonce }, + method: "POST", + signal: AbortSignal.timeout(PROBE_TIMEOUT_MS), + }, + ); + if (!response.ok || response.headers.get("Cache-Control") !== "no-store") { + throw new Error("WfP HTTP/body probe did not return an uncached success response."); + } + + const actual: unknown = await response.json(); + if ( + !isJsonRecord(actual) || + actual["body"] !== body || + actual["hostname"] !== hostname || + actual["method"] !== "POST" || + actual["nonce"] !== nonce || + actual["pathname"] !== pathname || + actual["search"] !== `?nonce=${encodeURIComponent(nonce)}` + ) { + throw new Error("WfP HTTP/body probe did not preserve the original request."); + } +} + +function isJsonRecord(value: unknown): value is Record { + return typeof value === "object" && value !== null && !Array.isArray(value); +} + +async function verifyStreamProbe( + hostname: string, + nonce: string, + fetcher: typeof fetch, +): Promise { + const response = await fetcher( + `https://${hostname}${APP_DEPLOYMENT_PROBE_PATH_PREFIX}stream?nonce=${encodeURIComponent(nonce)}`, + { signal: AbortSignal.timeout(PROBE_TIMEOUT_MS) }, + ); + const reader = response.body?.getReader(); + if (!response.ok || reader === undefined) { + throw new Error("WfP stream probe did not return a readable success response."); + } + + const decoder = new TextDecoder(); + const first = await reader.read(); + if (first.done || decoder.decode(first.value) !== `start:${nonce}\n`) { + throw new Error("WfP stream probe buffered or changed its first chunk."); + } + + let remainder = ""; + while (true) { + const part = await reader.read(); + if (part.done) break; + remainder += decoder.decode(part.value, { stream: true }); + } + remainder += decoder.decode(); + if (remainder !== `end:${nonce}\n`) { + throw new Error("WfP stream probe changed its final chunk."); + } +} + +async function verifyCancellationProbe( + hostname: string, + nonce: string, + fetcher: typeof fetch, +): Promise { + const response = await fetcher( + `https://${hostname}${APP_DEPLOYMENT_PROBE_PATH_PREFIX}cancel?nonce=${encodeURIComponent(nonce)}`, + { signal: AbortSignal.timeout(PROBE_TIMEOUT_MS) }, + ); + const reader = response.body?.getReader(); + if (!response.ok || reader === undefined) { + throw new Error("WfP cancellation probe did not return a readable success response."); + } + + const first = await reader.read(); + if (first.done || new TextDecoder().decode(first.value) !== `start:${nonce}\n`) { + throw new Error("WfP cancellation probe did not stream its first chunk."); + } + await reader.cancel(`cancel:${nonce}`); +} + +export function probeProdWfpWebSocket( + url: string, + nonce: string, + createSocket: (url: string) => WebSocket = (socketUrl) => new WebSocket(socketUrl), +): Promise { + return new Promise((resolve, reject) => { + const socket = createSocket(url); + let settled = false; + const timeout = setTimeout(() => { + socket.close(); + finish(new Error("WfP WebSocket probe timed out.")); + }, PROBE_TIMEOUT_MS); + const finish = (error?: Error): void => { + if (settled) return; + settled = true; + clearTimeout(timeout); + if (error === undefined) resolve(); + else reject(error); + }; + + socket.addEventListener("open", () => { + socket.send(`echo:${nonce}`); + }); + socket.addEventListener("message", (event) => { + if (event.data !== `echo:${nonce}`) { + socket.close(); + finish(new Error("WfP WebSocket probe changed the echo payload.")); + } + }); + socket.addEventListener("close", (event) => { + if (event.code !== 1000 || event.reason !== `verified:${nonce}`) { + finish(new Error("WfP WebSocket probe did not close normally.")); + return; + } + finish(); + }); + socket.addEventListener("error", () => { + finish(new Error("WfP WebSocket probe failed.")); + }); + }); +} diff --git a/apps/api/bin/protocol-v3-cutover.ts b/apps/api/bin/protocol-v3-cutover.ts new file mode 100644 index 00000000..37eea6ea --- /dev/null +++ b/apps/api/bin/protocol-v3-cutover.ts @@ -0,0 +1,3138 @@ +import type { D1JsonRow } from "./d1-json"; +import { parseD1JsonResults, requireSingleD1Row } from "./d1-json"; +import { PROD_DEPLOY_LEASE_TABLE } from "./prod-deploy-lease"; +import { + MANAGED_PROD_SCHEMA_TRIGGERS, + PROTOCOL_V3_MIGRATION_INTENT_TABLE, + PROTOCOL_V3_MIGRATION_INTENT_TABLE_SQL, +} from "./prod-schema-guard"; + +export { PROTOCOL_V3_MIGRATION_INTENT_TABLE } from "./prod-schema-guard"; + +export const PROTOCOL_V3_MIGRATION = "0013_durable-mcp-effect-v3.sql"; +export const PROTOCOL_V3_SESSION_EVENT_MIGRATION = "0014_session-event-stream-identity.sql"; +export const PROTOCOL_V3_SESSION_CLEANUP_MIGRATION = "0015_session-cleanup-operation.sql"; +export const PROTOCOL_V3_RUNTIME_AUTHORITY_MIGRATION = + "0019_runtime-subject-operation-authority.sql"; +export const LAST_CUTOVER_AUDITED_MIGRATION = "0020_sandbox-backup-object-authority.sql"; +export const AUDITED_MIGRATION_NAMES = [ + "0000_baseline.sql", + "0001_bound-capability-run-provenance.sql", + "0002_bound-agent-call-idempotency.sql", + "0003_collapse-sandbox-error-state.sql", + "0004_usage-rollup-receipt.sql", + "0005_public-thread-end-user.sql", + "0006_runtime_subject_quota_scope.sql", + "0007_app-deployment-secrets.sql", + "0008_public-thread-tool-call-identity.sql", + "0009_terminal_event_lookup_index.sql", + "0010_external-tool-effects.sql", + "0011_cattle-terminal-checkpoints.sql", + "0012_agent-task-snapshot-state.sql", + PROTOCOL_V3_MIGRATION, + PROTOCOL_V3_SESSION_EVENT_MIGRATION, + PROTOCOL_V3_SESSION_CLEANUP_MIGRATION, + "0016_durable-event-side-effects.sql", + "0017_terminal-reconciliation-scheduling.sql", + "0018_runtime-operation-ready-authority.sql", + PROTOCOL_V3_RUNTIME_AUTHORITY_MIGRATION, + LAST_CUTOVER_AUDITED_MIGRATION, +] as const; +export const PROTOCOL_V3_CUTOVER_TABLE = "__protocol_v3_cutover"; +export const PROD_APPLIED_MIGRATIONS_SQL = `SELECT "name" FROM "d1_migrations" ORDER BY "id";`; + +const GIT_TREE_OID_PATTERN = /^(?:[0-9a-f]{40}|[0-9a-f]{64})$/u; +const SHA256_DIGEST_PATTERN = /^[0-9a-f]{64}$/u; +const WORKER_VERSION_ID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/u; + +export type ProtocolV3CutoverPhase = "draining" | "queues_resuming"; + +const ACTIVE_RUN_STATUSES = "'queued', 'booting', 'running', 'waiting_input'"; +const ACTIVE_APP_DEPLOYMENT_RUN_STATUSES = + "'queued', 'preparing', 'building', 'submitting', 'submitted', 'activating'"; +const CUTOVER_BLOCKED_API_COMMAND_KINDS = + "'session_run_dispatch', 'app_deployment_run_dispatch', 'environment_package_artifact_build'"; + +export const PROTOCOL_V3_CUTOVER_QUEUE_NAMES = [ + "api-command", + "api-command-dlq", + "environment-artifact-build", +] as const; + +export interface ProtocolV3QueueDeliveryControl { + list(): Promise; + mutate(queueName: string, action: "pause" | "resume"): Promise | void; + read(queueId: string): Promise<{ + readonly deliveryPaused: boolean | undefined; + readonly name: string; + }>; +} + +export async function updateAndVerifyProtocolV3QueueDelivery( + control: ProtocolV3QueueDeliveryControl, + action: "pause" | "resume", +): Promise { + const queueIds = new Map((await control.list()).map(({ id, name }) => [name, id])); + const failures: unknown[] = []; + + for (const queueName of PROTOCOL_V3_CUTOVER_QUEUE_NAMES) { + const queueId = queueIds.get(queueName); + if (queueId === undefined) { + failures.push(new Error(`Production queue ${queueName} was not found.`)); + continue; + } + + try { + await control.mutate(queueName, action); + const queue = await control.read(queueId); + if (queue.name !== queueName || queue.deliveryPaused !== (action === "pause")) { + throw new Error(`Production queue ${queueName} delivery did not ${action}.`); + } + } catch (error) { + failures.push(error); + } + } + + if (failures.length > 0) { + throw new AggregateError(failures, `Production queue ${action} and readback failed.`); + } +} +const LIVE_DRIVER_STATUSES = "'provisioning', 'connecting', 'ready', 'stopping'"; + +function exactSmokeSessionSql(sessionId: string, sandboxId?: string): string { + const sandboxJoin = + sandboxId === undefined + ? "" + : ` + INNER JOIN "sandbox" AS "smoke_sandbox" + ON "smoke_sandbox"."id" = ${sandboxId} + AND "smoke_sandbox"."subject_kind" = 'session' + AND "smoke_sandbox"."subject_id" = "smoke_session"."id"`; + return `EXISTS ( + SELECT 1 + FROM "${PROTOCOL_V3_CUTOVER_TABLE}" AS "gate" + INNER JOIN "session" AS "smoke_session" + ON "smoke_session"."id" = ${sessionId} + AND "smoke_session"."creator_account_id" = "gate"."smoke_account_id" + AND "smoke_session"."end_user_id" = "gate"."smoke_request_key"${sandboxJoin} + WHERE "gate"."enabled" = 1 + AND "gate"."smoke_request_key" IS NOT NULL + AND ("gate"."smoke_session_id" IS NULL OR "gate"."smoke_session_id" = "smoke_session"."id") + )`; +} + +const EXACT_NEW_SESSION_SMOKE_SQL = `EXISTS ( + SELECT 1 + FROM "${PROTOCOL_V3_CUTOVER_TABLE}" AS "gate" + WHERE "gate"."enabled" = 1 + AND "gate"."smoke_request_key" IS NOT NULL + AND "gate"."smoke_session_id" IS NULL + AND "gate"."smoke_account_id" = NEW."creator_account_id" + AND "gate"."smoke_request_key" = NEW."end_user_id" + )`; + +function postMigrationSessionStaticSql(row: string): string { + return `${row}."status" IN ('IDLE', 'TERMINATED') + AND ${row}."status_operation_id" IS NULL + AND (${row}."cleanup_operation_kind" IS NULL + OR (${row}."cleanup_operation_kind" = 'archive' + AND ${row}."status" = 'IDLE' + AND ${row}."archived_at" IS NOT NULL)) + AND ${row}."runtime_provisioning_operation_id" IS NULL + AND ${row}."runtime_provisioning_run_id" IS NULL + AND ${row}."runtime_provisioning_sandbox_id" IS NULL + AND ${row}."runtime_provisioning_sandbox_session_id" IS NULL + AND ${row}."runtime_provisioning_sandbox_incarnation" IS NULL + AND ${row}."runtime_provisioning_heartbeat_at" IS NULL`; +} + +interface ProtocolV3CutoverObject { + readonly name: string; + readonly sql: string; + readonly tableName: string; + readonly type: "table" | "trigger"; +} + +const PROTOCOL_V3_CUTOVER_OBJECTS: readonly ProtocolV3CutoverObject[] = [ + { + name: PROTOCOL_V3_CUTOVER_TABLE, + sql: `CREATE TABLE "${PROTOCOL_V3_CUTOVER_TABLE}" ( + "id" integer PRIMARY KEY CHECK ("id" = 1), + "command_freeze" integer NOT NULL DEFAULT 0 CHECK ("command_freeze" IN (0, 1)), + "enabled" integer NOT NULL DEFAULT 1 CHECK ("enabled" IN (0, 1)), + "phase" text NOT NULL DEFAULT 'draining' CHECK ("phase" IN ('draining', 'queues_resuming')), + "pre_migration_bookmark" text, + "release_tree_oid" text NOT NULL CHECK ((length("release_tree_oid") = 40 OR length("release_tree_oid") = 64) AND "release_tree_oid" = lower("release_tree_oid") AND "release_tree_oid" NOT GLOB '*[^0-9a-f]*'), + "smoke_account_id" text, + "smoke_request_key" text, + "smoke_session_id" text, + "target_container_application_version" integer, + "target_container_image_digest" text, + "target_worker_version_id" text, + "started_at" integer NOT NULL DEFAULT (CAST(unixepoch('subsec') * 1000 AS INTEGER)), + CONSTRAINT "protocol_v3_cutover_phase_check" CHECK ( + ("phase" = 'draining' AND "enabled" = 1) + OR ("phase" = 'queues_resuming' AND "command_freeze" = 1) + ), + CONSTRAINT "protocol_v3_cutover_rollout_check" CHECK ( + ("target_container_application_version" IS NULL AND "target_container_image_digest" IS NULL AND "target_worker_version_id" IS NULL) + OR ("target_container_application_version" >= 0 AND length("target_container_image_digest") = 64 AND "target_container_image_digest" = lower("target_container_image_digest") AND "target_container_image_digest" NOT GLOB '*[^0-9a-f]*' AND length(trim("target_worker_version_id")) > 0) + ) +)`.trim(), + tableName: PROTOCOL_V3_CUTOVER_TABLE, + type: "table", + }, + { + name: "__protocol_v3_cutover_session_run_insert", + sql: `CREATE TRIGGER "__protocol_v3_cutover_session_run_insert" +BEFORE INSERT ON "session_run" +WHEN NEW."status" IN (${ACTIVE_RUN_STATUSES}) + AND EXISTS (SELECT 1 FROM "${PROTOCOL_V3_CUTOVER_TABLE}" WHERE "enabled" = 1) + AND NOT ${exactSmokeSessionSql('NEW."session_id"')} +BEGIN + SELECT RAISE(ABORT, 'protocol v3 cutover blocks new active Session Runs'); +END`, + tableName: "session_run", + type: "trigger", + }, + { + name: "__protocol_v3_cutover_session_run_update", + sql: `CREATE TRIGGER "__protocol_v3_cutover_session_run_update" +BEFORE UPDATE OF "status" ON "session_run" +WHEN NEW."status" IN (${ACTIVE_RUN_STATUSES}) + AND OLD."status" NOT IN (${ACTIVE_RUN_STATUSES}) + AND EXISTS (SELECT 1 FROM "${PROTOCOL_V3_CUTOVER_TABLE}" WHERE "enabled" = 1) + AND NOT ${exactSmokeSessionSql('NEW."session_id"')} +BEGIN + SELECT RAISE(ABORT, 'protocol v3 cutover blocks Session Run reactivation'); +END`, + tableName: "session_run", + type: "trigger", + }, + { + name: "__protocol_v3_cutover_app_deployment_run_insert", + sql: `CREATE TRIGGER "__protocol_v3_cutover_app_deployment_run_insert" +BEFORE INSERT ON "app_deployment_run" +WHEN NEW."status" IN (${ACTIVE_APP_DEPLOYMENT_RUN_STATUSES}) + AND EXISTS (SELECT 1 FROM "${PROTOCOL_V3_CUTOVER_TABLE}" WHERE "enabled" = 1) +BEGIN + SELECT RAISE(ABORT, 'protocol v3 cutover blocks new active App deployment Runs'); +END`, + tableName: "app_deployment_run", + type: "trigger", + }, + { + name: "__protocol_v3_cutover_app_deployment_run_update", + sql: `CREATE TRIGGER "__protocol_v3_cutover_app_deployment_run_update" +BEFORE UPDATE OF "status" ON "app_deployment_run" +WHEN NEW."status" IN (${ACTIVE_APP_DEPLOYMENT_RUN_STATUSES}) + AND OLD."status" NOT IN (${ACTIVE_APP_DEPLOYMENT_RUN_STATUSES}) + AND EXISTS (SELECT 1 FROM "${PROTOCOL_V3_CUTOVER_TABLE}" WHERE "enabled" = 1) +BEGIN + SELECT RAISE(ABORT, 'protocol v3 cutover blocks App deployment Run reactivation'); +END`, + tableName: "app_deployment_run", + type: "trigger", + }, + { + name: "__protocol_v3_cutover_driver_insert", + sql: `CREATE TRIGGER "__protocol_v3_cutover_driver_insert" +BEFORE INSERT ON "driver_instance" +WHEN NEW."status" IN (${LIVE_DRIVER_STATUSES}) + AND EXISTS (SELECT 1 FROM "${PROTOCOL_V3_CUTOVER_TABLE}" WHERE "enabled" = 1) + AND NOT ${exactSmokeSessionSql('NEW."sandbox_session_id"', 'NEW."sandbox_id"')} +BEGIN + SELECT RAISE(ABORT, 'protocol v3 cutover blocks new live Driver instances'); +END`, + tableName: "driver_instance", + type: "trigger", + }, + { + name: "__protocol_v3_cutover_driver_update", + sql: `CREATE TRIGGER "__protocol_v3_cutover_driver_update" +BEFORE UPDATE OF "status" ON "driver_instance" +WHEN NEW."status" IN (${LIVE_DRIVER_STATUSES}) + AND OLD."status" NOT IN (${LIVE_DRIVER_STATUSES}) + AND EXISTS (SELECT 1 FROM "${PROTOCOL_V3_CUTOVER_TABLE}" WHERE "enabled" = 1) + AND NOT ${exactSmokeSessionSql('NEW."sandbox_session_id"', 'NEW."sandbox_id"')} +BEGIN + SELECT RAISE(ABORT, 'protocol v3 cutover blocks Driver reactivation'); +END`, + tableName: "driver_instance", + type: "trigger", + }, + { + name: "__protocol_v3_cutover_command_insert", + sql: `CREATE TRIGGER "__protocol_v3_cutover_command_insert" +BEFORE INSERT ON "driver_command" +WHEN EXISTS ( + SELECT 1 FROM "${PROTOCOL_V3_CUTOVER_TABLE}" + WHERE "enabled" = 1 + AND ("command_freeze" = 1 OR NEW."kind" IN ('input.start', 'mcp.execute')) + ) + AND NOT ( + NEW."kind" = 'session.stop' + AND EXISTS ( + SELECT 1 + FROM "driver_instance" AS "smoke_driver" + WHERE "smoke_driver"."id" = NEW."driver_instance_id" + AND ${exactSmokeSessionSql('"smoke_driver"."sandbox_session_id"', '"smoke_driver"."sandbox_id"')} + ) + ) +BEGIN + SELECT RAISE(ABORT, 'protocol v3 cutover blocks new Driver commands'); +END`, + tableName: "driver_command", + type: "trigger", + }, + { + name: "__protocol_v3_cutover_sandbox_insert", + sql: `CREATE TRIGGER "__protocol_v3_cutover_sandbox_insert" +BEFORE INSERT ON "sandbox" +WHEN (NEW."status" <> 'cold' + OR NEW."status_operation_id" IS NOT NULL + OR NEW."claim_owner" IS NOT NULL + OR NEW."claim_expires_at" IS NOT NULL) + AND EXISTS (SELECT 1 FROM "${PROTOCOL_V3_CUTOVER_TABLE}" WHERE "enabled" = 1) + AND NOT (NEW."subject_kind" = 'session' AND ${exactSmokeSessionSql('NEW."subject_id"')}) +BEGIN + SELECT RAISE(ABORT, 'protocol v3 cutover blocks new active sandboxes'); +END`, + tableName: "sandbox", + type: "trigger", + }, + { + name: "__protocol_v3_cutover_sandbox_update", + sql: `CREATE TRIGGER "__protocol_v3_cutover_sandbox_update" +BEFORE UPDATE OF "status", "status_operation_id", "claim_owner", "claim_expires_at" ON "sandbox" +WHEN OLD."status" = 'cold' + AND OLD."status_operation_id" IS NULL + AND OLD."claim_owner" IS NULL + AND OLD."claim_expires_at" IS NULL + AND (NEW."status" <> 'cold' + OR NEW."status_operation_id" IS NOT NULL + OR NEW."claim_owner" IS NOT NULL + OR NEW."claim_expires_at" IS NOT NULL) + AND EXISTS (SELECT 1 FROM "${PROTOCOL_V3_CUTOVER_TABLE}" WHERE "enabled" = 1) + AND NOT (NEW."subject_kind" = 'session' AND ${exactSmokeSessionSql('NEW."subject_id"')}) +BEGIN + SELECT RAISE(ABORT, 'protocol v3 cutover blocks sandbox activation'); +END`, + tableName: "sandbox", + type: "trigger", + }, + { + name: "__protocol_v3_cutover_sandbox_session_insert", + sql: `CREATE TRIGGER "__protocol_v3_cutover_sandbox_session_insert" +BEFORE INSERT ON "sandbox_session" +WHEN NEW."status" NOT IN ('closed', 'error') + AND EXISTS (SELECT 1 FROM "${PROTOCOL_V3_CUTOVER_TABLE}" WHERE "enabled" = 1) + AND NOT ${exactSmokeSessionSql('NEW."session_id"', 'NEW."sandbox_id"')} +BEGIN + SELECT RAISE(ABORT, 'protocol v3 cutover blocks new active sandbox Sessions'); +END`, + tableName: "sandbox_session", + type: "trigger", + }, + { + name: "__protocol_v3_cutover_sandbox_session_update", + sql: `CREATE TRIGGER "__protocol_v3_cutover_sandbox_session_update" +BEFORE UPDATE OF "status" ON "sandbox_session" +WHEN OLD."status" IN ('closed', 'error') + AND NEW."status" NOT IN ('closed', 'error') + AND EXISTS (SELECT 1 FROM "${PROTOCOL_V3_CUTOVER_TABLE}" WHERE "enabled" = 1) + AND NOT ${exactSmokeSessionSql('NEW."session_id"', 'NEW."sandbox_id"')} +BEGIN + SELECT RAISE(ABORT, 'protocol v3 cutover blocks sandbox Session reactivation'); +END`, + tableName: "sandbox_session", + type: "trigger", + }, + { + name: "__protocol_v3_cutover_sandbox_backup_insert", + sql: `CREATE TRIGGER "__protocol_v3_cutover_sandbox_backup_insert" +BEFORE INSERT ON "sandbox_backup" +WHEN NEW."status" NOT IN ('ready', 'pruned') + AND EXISTS (SELECT 1 FROM "${PROTOCOL_V3_CUTOVER_TABLE}" WHERE "enabled" = 1) +BEGIN + SELECT RAISE(ABORT, 'protocol v3 cutover blocks new sandbox backup work'); +END`, + tableName: "sandbox_backup", + type: "trigger", + }, + { + name: "__protocol_v3_cutover_sandbox_backup_update", + sql: `CREATE TRIGGER "__protocol_v3_cutover_sandbox_backup_update" +BEFORE UPDATE OF "status" ON "sandbox_backup" +WHEN OLD."status" IN ('ready', 'pruned') + AND NEW."status" NOT IN ('ready', 'pruned') + AND EXISTS (SELECT 1 FROM "${PROTOCOL_V3_CUTOVER_TABLE}" WHERE "enabled" = 1) +BEGIN + SELECT RAISE(ABORT, 'protocol v3 cutover blocks sandbox backup reactivation'); +END`, + tableName: "sandbox_backup", + type: "trigger", + }, + { + name: "__protocol_v3_cutover_session_insert", + sql: `CREATE TRIGGER "__protocol_v3_cutover_session_insert" +BEFORE INSERT ON "session" +WHEN (NEW."status" NOT IN ('IDLE', 'TERMINATED') OR NEW."status_operation_id" IS NOT NULL) + AND EXISTS (SELECT 1 FROM "${PROTOCOL_V3_CUTOVER_TABLE}" WHERE "enabled" = 1) + AND NOT ${EXACT_NEW_SESSION_SMOKE_SQL} +BEGIN + SELECT RAISE(ABORT, 'protocol v3 cutover blocks new Session operations'); +END`, + tableName: "session", + type: "trigger", + }, + { + name: "__protocol_v3_cutover_session_update", + sql: `CREATE TRIGGER "__protocol_v3_cutover_session_update" +BEFORE UPDATE OF "status", "status_operation_id" ON "session" +WHEN OLD."status" IN ('IDLE', 'TERMINATED') + AND OLD."status_operation_id" IS NULL + AND (NEW."status" NOT IN ('IDLE', 'TERMINATED') OR NEW."status_operation_id" IS NOT NULL) + AND EXISTS (SELECT 1 FROM "${PROTOCOL_V3_CUTOVER_TABLE}" WHERE "enabled" = 1) + AND NOT ${exactSmokeSessionSql('NEW."id"')} +BEGIN + SELECT RAISE(ABORT, 'protocol v3 cutover blocks Session operation acquisition'); +END`, + tableName: "session", + type: "trigger", + }, + { + name: "__protocol_v3_cutover_api_command_insert", + sql: `CREATE TRIGGER "__protocol_v3_cutover_api_command_insert" +BEFORE INSERT ON "api_command" +WHEN NEW."status" IN ('queued', 'running') + AND EXISTS ( + SELECT 1 FROM "${PROTOCOL_V3_CUTOVER_TABLE}" + WHERE "enabled" = 1 + AND ("command_freeze" = 1 OR NEW."kind" IN (${CUTOVER_BLOCKED_API_COMMAND_KINDS})) + ) +BEGIN + SELECT RAISE(ABORT, 'protocol v3 cutover blocks new nonterminal API commands'); +END`, + tableName: "api_command", + type: "trigger", + }, + { + name: "__protocol_v3_cutover_api_command_update", + sql: `CREATE TRIGGER "__protocol_v3_cutover_api_command_update" +BEFORE UPDATE OF "kind", "status", "claim_owner", "claim_expires_at" ON "api_command" +WHEN NEW."status" IN ('queued', 'running') + AND (OLD."status" NOT IN ('queued', 'running') OR NEW."kind" IS NOT OLD."kind") + AND EXISTS ( + SELECT 1 FROM "${PROTOCOL_V3_CUTOVER_TABLE}" + WHERE "enabled" = 1 + AND ("command_freeze" = 1 OR NEW."kind" IN (${CUTOVER_BLOCKED_API_COMMAND_KINDS})) + ) +BEGIN + SELECT RAISE(ABORT, 'protocol v3 cutover blocks API command admission'); +END`, + tableName: "api_command", + type: "trigger", + }, +]; + +export const PROTOCOL_V3_CUTOVER_OBJECT_COUNT = PROTOCOL_V3_CUTOVER_OBJECTS.length; + +const POST_MIGRATION_CUTOVER_REPLACEMENTS: readonly ProtocolV3CutoverObject[] = [ + { + name: "__protocol_v3_cutover_sandbox_insert", + sql: `CREATE TRIGGER "__protocol_v3_cutover_sandbox_insert" +BEFORE INSERT ON "sandbox" +WHEN (NEW."status" <> 'cold' + OR NEW."operation_kind" IS NOT NULL + OR NEW."status_operation_id" IS NOT NULL + OR NEW."claim_owner" IS NOT NULL + OR NEW."claim_expires_at" IS NOT NULL) + AND EXISTS (SELECT 1 FROM "${PROTOCOL_V3_CUTOVER_TABLE}" WHERE "enabled" = 1) + AND NOT (NEW."subject_kind" = 'session' AND ${exactSmokeSessionSql('NEW."subject_id"')}) +BEGIN + SELECT RAISE(ABORT, 'protocol v3 cutover blocks new active sandboxes'); +END`, + tableName: "sandbox", + type: "trigger", + }, + { + name: "__protocol_v3_cutover_sandbox_update", + sql: `CREATE TRIGGER "__protocol_v3_cutover_sandbox_update" +BEFORE UPDATE OF "status", "operation_kind", "status_operation_id", "claim_owner", "claim_expires_at" ON "sandbox" +WHEN OLD."status" = 'cold' + AND OLD."operation_kind" IS NULL + AND OLD."status_operation_id" IS NULL + AND OLD."claim_owner" IS NULL + AND OLD."claim_expires_at" IS NULL + AND (NEW."status" <> 'cold' + OR NEW."operation_kind" IS NOT NULL + OR NEW."status_operation_id" IS NOT NULL + OR NEW."claim_owner" IS NOT NULL + OR NEW."claim_expires_at" IS NOT NULL) + AND EXISTS (SELECT 1 FROM "${PROTOCOL_V3_CUTOVER_TABLE}" WHERE "enabled" = 1) + AND NOT (NEW."subject_kind" = 'session' AND ${exactSmokeSessionSql('NEW."subject_id"')}) +BEGIN + SELECT RAISE(ABORT, 'protocol v3 cutover blocks sandbox activation'); +END`, + tableName: "sandbox", + type: "trigger", + }, + { + name: "__protocol_v3_cutover_session_insert", + sql: `CREATE TRIGGER "__protocol_v3_cutover_session_insert" +BEFORE INSERT ON "session" +WHEN NOT (${postMigrationSessionStaticSql("NEW")}) + AND EXISTS (SELECT 1 FROM "${PROTOCOL_V3_CUTOVER_TABLE}" WHERE "enabled" = 1) + AND NOT ${EXACT_NEW_SESSION_SMOKE_SQL} +BEGIN + SELECT RAISE(ABORT, 'protocol v3 cutover blocks new Session operations'); +END`, + tableName: "session", + type: "trigger", + }, + { + name: "__protocol_v3_cutover_session_update", + sql: `CREATE TRIGGER "__protocol_v3_cutover_session_update" +BEFORE UPDATE OF "status", "status_operation_id", "archived_at", "cleanup_operation_kind", "runtime_provisioning_operation_id", "runtime_provisioning_run_id", "runtime_provisioning_sandbox_id", "runtime_provisioning_sandbox_session_id", "runtime_provisioning_sandbox_incarnation", "runtime_provisioning_heartbeat_at" ON "session" +WHEN (${postMigrationSessionStaticSql("OLD")}) + AND NOT (${postMigrationSessionStaticSql("NEW")}) + AND EXISTS (SELECT 1 FROM "${PROTOCOL_V3_CUTOVER_TABLE}" WHERE "enabled" = 1) + AND NOT ${exactSmokeSessionSql('NEW."id"')} +BEGIN + SELECT RAISE(ABORT, 'protocol v3 cutover blocks Session operation acquisition'); +END`, + tableName: "session", + type: "trigger", + }, + { + name: "__protocol_v3_cutover_api_command_update", + sql: `CREATE TRIGGER "__protocol_v3_cutover_api_command_update" +BEFORE UPDATE OF "kind", "status", "claim_owner", "claim_expires_at", "delivery_generation" ON "api_command" +WHEN EXISTS ( + SELECT 1 FROM "${PROTOCOL_V3_CUTOVER_TABLE}" + WHERE "enabled" = 1 + AND ( + ("command_freeze" = 1 + AND ( + NEW."delivery_generation" IS NOT OLD."delivery_generation" + OR (NEW."status" IN ('queued', 'running') + AND (OLD."status" NOT IN ('queued', 'running') OR NEW."kind" IS NOT OLD."kind")) + )) + OR ("command_freeze" = 0 + AND NEW."kind" IN (${CUTOVER_BLOCKED_API_COMMAND_KINDS}) + AND NEW."status" IN ('queued', 'running') + AND (OLD."status" NOT IN ('queued', 'running') OR NEW."kind" IS NOT OLD."kind")) + ) + ) +BEGIN + SELECT RAISE(ABORT, 'protocol v3 cutover blocks API command admission'); +END`, + tableName: "api_command", + type: "trigger", + }, +]; + +const POST_MIGRATION_CUTOVER_REPLACEMENT_BY_NAME = new Map( + POST_MIGRATION_CUTOVER_REPLACEMENTS.map((object) => [object.name, object]), +); +const PROTOCOL_V3_POST_MIGRATION_CUTOVER_OBJECTS: readonly ProtocolV3CutoverObject[] = [ + ...PROTOCOL_V3_CUTOVER_OBJECTS.map( + (object) => POST_MIGRATION_CUTOVER_REPLACEMENT_BY_NAME.get(object.name) ?? object, + ), + { + name: "__protocol_v3_cutover_sandbox_backup_staging_insert", + sql: `CREATE TRIGGER "__protocol_v3_cutover_sandbox_backup_staging_insert" +BEFORE INSERT ON "sandbox_backup_staging" +WHEN EXISTS (SELECT 1 FROM "${PROTOCOL_V3_CUTOVER_TABLE}" WHERE "enabled" = 1) + AND NOT ${exactSmokeSessionSql('NEW."workspace_session_id"', 'NEW."sandbox_id"')} +BEGIN + SELECT RAISE(ABORT, 'protocol v3 cutover blocks new sandbox backup staging'); +END`, + tableName: "sandbox_backup_staging", + type: "trigger", + }, + { + name: "__protocol_v3_cutover_environment_artifact_backup_staging_insert", + sql: `CREATE TRIGGER "__protocol_v3_cutover_environment_artifact_backup_staging_insert" +BEFORE INSERT ON "environment_package_artifact_backup_staging" +WHEN EXISTS (SELECT 1 FROM "${PROTOCOL_V3_CUTOVER_TABLE}" WHERE "enabled" = 1) + AND NOT EXISTS ( + SELECT 1 + FROM "${PROTOCOL_V3_CUTOVER_TABLE}" AS "gate" + INNER JOIN "api_command" AS "command" + ON "command"."id" = NEW."command_id" + AND "command"."created_at" <= "gate"."started_at" + AND "command"."kind" = 'environment_package_artifact_build' + AND "command"."status" = 'running' + AND "command"."delivery_generation" = NEW."delivery_generation" + AND "command"."attempt_count" = NEW."attempt_count" + AND "command"."claim_owner" = NEW."claim_owner" + AND typeof("command"."claim_expires_at") = 'integer' + AND "command"."claim_expires_at" > unixepoch('subsec') * 1000 + AND json_valid("command"."payload_json") = 1 + AND json_extract("command"."payload_json", '$.appId') = NEW."app_id" + AND json_extract("command"."payload_json", '$.inputDigest') = NEW."input_digest" + WHERE "gate"."enabled" = 1 + AND "gate"."command_freeze" = 0 + ) +BEGIN + SELECT RAISE(ABORT, 'protocol v3 cutover blocks new environment artifact backup staging'); +END`, + tableName: "environment_package_artifact_backup_staging", + type: "trigger", + }, +]; + +export const PROTOCOL_V3_POST_MIGRATION_CUTOVER_OBJECT_COUNT = + PROTOCOL_V3_POST_MIGRATION_CUTOVER_OBJECTS.length; + +function installableCutoverObjectSql(object: ProtocolV3CutoverObject): string { + const create = object.type === "table" ? "CREATE TABLE" : "CREATE TRIGGER"; + return object.sql.replace(create, `${create} IF NOT EXISTS`); +} + +function sqlString(value: string): string { + return `'${value.replaceAll("'", "''")}'`; +} + +const CUTOVER_RESERVED_NAMES_SQL = [ + ...new Set(PROTOCOL_V3_POST_MIGRATION_CUTOVER_OBJECTS.map((object) => object.name)), +] + .map(sqlString) + .join(", "); +const PROTOCOL_V3_LEGACY_REWRITE_AUTHORIZATION_TABLE = "__protocol_v3_legacy_rewrite_authorization"; +const PROTOCOL_V3_LEGACY_REWRITE_GATE_UPDATE_TRIGGER = "__protocol_v3_legacy_rewrite_gate_update"; +export const PROTOCOL_V3_LEGACY_REWRITE_GATE_UPDATE_TRIGGER_SQL = `CREATE TRIGGER "${PROTOCOL_V3_LEGACY_REWRITE_GATE_UPDATE_TRIGGER}" +AFTER UPDATE ON "${PROTOCOL_V3_CUTOVER_TABLE}" +BEGIN + DELETE FROM "${PROTOCOL_V3_LEGACY_REWRITE_AUTHORIZATION_TABLE}" WHERE "id" = 1; +END`; +const PROTOCOL_V3_LEGACY_REWRITE_GATE_UPDATE_TRIGGER_MATCH_SQL = `( + "type" = 'trigger' + AND "name" = ${sqlString(PROTOCOL_V3_LEGACY_REWRITE_GATE_UPDATE_TRIGGER)} COLLATE BINARY + AND "tbl_name" = ${sqlString(PROTOCOL_V3_CUTOVER_TABLE)} COLLATE BINARY + AND "sql" = ${sqlString(PROTOCOL_V3_LEGACY_REWRITE_GATE_UPDATE_TRIGGER_SQL)} COLLATE BINARY +)`; +const CUTOVER_PROTECTED_TABLES = [ + PROD_DEPLOY_LEASE_TABLE, + PROTOCOL_V3_CUTOVER_TABLE, + PROTOCOL_V3_MIGRATION_INTENT_TABLE, + PROTOCOL_V3_LEGACY_REWRITE_AUTHORIZATION_TABLE, + "api_command", + "app_deployment_run", + "driver_command", + "driver_instance", + "environment_package_artifact_backup", + "environment_package_artifact_backup_staging", + "sandbox", + "sandbox_backup", + "sandbox_backup_delete_intent", + "sandbox_backup_staging", + "sandbox_session", + "session", + "session_run", +] as const; +const CUTOVER_PROTECTED_TABLES_SQL = CUTOVER_PROTECTED_TABLES.map(sqlString).join(", "); +const CUTOVER_MANAGED_TRIGGER_NAMES = [ + "app_deployment_run_target_script_insert_authority", + "app_deployment_run_target_script_retire", + "app_deployment_run_target_script_update_authority", + "app_deployment_run_terminal_script_retire", + "environment_package_artifact_backup_staging_authority", + "environment_package_artifact_backup_staging_immutable", + "environment_package_artifact_backup_authority", + "environment_package_artifact_backup_retirement_authority", + "environment_package_artifact_backup_retirement_tombstone", + "environment_package_artifact_backup_rotation_authority", + "environment_package_artifact_backup_rotation_tombstone", + "sandbox_backup_delete_intent_attempt_clock", + "sandbox_backup_delete_intent_authority", + "sandbox_backup_delete_intent_blocks_environment_commit", + "sandbox_backup_delete_intent_blocks_environment_stage_insert", + "sandbox_backup_delete_intent_blocks_environment_stage_update", + "sandbox_backup_delete_intent_blocks_runtime_record", + "sandbox_backup_delete_intent_blocks_runtime_record_update", + "sandbox_backup_delete_intent_blocks_runtime_stage_insert", + "sandbox_backup_delete_intent_blocks_runtime_stage_update", + "sandbox_backup_delete_intent_completion_monotonic", + "sandbox_backup_delete_intent_identity_immutable", + "sandbox_backup_delete_intent_permanent", + "sandbox_backup_identity_immutable", + "sandbox_backup_permanent", + "sandbox_backup_staging_identity_immutable", + "sandbox_backup_status_monotonic", + "sandbox_identity_immutable", +] as const; +const CUTOVER_MANAGED_TRIGGER_MATCH_SQL = CUTOVER_MANAGED_TRIGGER_NAMES.map((name) => { + const trigger = MANAGED_PROD_SCHEMA_TRIGGERS.find((candidate) => candidate.name === name); + if ( + trigger === undefined || + !CUTOVER_PROTECTED_TABLES.some((tableName) => tableName === trigger.tableName) + ) { + throw new Error(`Managed cutover trigger ${name} is missing or protects the wrong table.`); + } + const canonicalSql = trigger.sql.replaceAll("`", '"'); + return `( + "type" = 'trigger' + AND "name" = ${sqlString(trigger.name)} COLLATE BINARY + AND "tbl_name" = ${sqlString(trigger.tableName)} COLLATE BINARY + AND replace("sql", char(96), '"') = ${sqlString(canonicalSql)} COLLATE BINARY + )`; +}).join("\n OR "); + +export function installProtocolV3CutoverSql(releaseTreeOid: string): string { + const release = requireGitTreeOid(releaseTreeOid, "Protocol v3 release tree OID"); + return ` +${PROTOCOL_V3_CUTOVER_OBJECTS.map((object) => `${installableCutoverObjectSql(object)};`).join("\n")} +${PROTOCOL_V3_MIGRATION_INTENT_TABLE_SQL.replace("CREATE TABLE", "CREATE TABLE IF NOT EXISTS")}; +INSERT INTO "${PROTOCOL_V3_CUTOVER_TABLE}" ("id", "enabled", "release_tree_oid") +VALUES (1, 1, '${release}') +ON CONFLICT ("id") DO NOTHING; +`; +} + +const PROTOCOL_V3_CUTOVER_TRIGGER_NAMES = [ + ...new Set( + PROTOCOL_V3_POST_MIGRATION_CUTOVER_OBJECTS.filter((object) => object.type === "trigger").map( + (object) => object.name, + ), + ), +]; + +export const DROP_PROTOCOL_V3_CUTOVER_TRIGGERS_SQL = PROTOCOL_V3_CUTOVER_TRIGGER_NAMES.toReversed() + .map((name) => `DROP TRIGGER IF EXISTS "${name}";`) + .join("\n"); + +export const INSTALL_PROTOCOL_V3_POST_MIGRATION_CUTOVER_SQL = ` +${installableCutoverObjectSql(PROTOCOL_V3_POST_MIGRATION_CUTOVER_OBJECTS[0])}; +${PROTOCOL_V3_MIGRATION_INTENT_TABLE_SQL.replace("CREATE TABLE", "CREATE TABLE IF NOT EXISTS")}; +${DROP_PROTOCOL_V3_CUTOVER_TRIGGERS_SQL} +${PROTOCOL_V3_POST_MIGRATION_CUTOVER_OBJECTS.slice(1) + .map((object) => `${installableCutoverObjectSql(object)};`) + .join("\n")} +`; + +export function installProtocolV3PostMigrationCutoverSql(releaseTreeOid: string): string { + const release = requireGitTreeOid(releaseTreeOid, "Protocol v3 release tree OID"); + return `${INSTALL_PROTOCOL_V3_POST_MIGRATION_CUTOVER_SQL} +INSERT INTO "${PROTOCOL_V3_CUTOVER_TABLE}" ("id", "enabled", "release_tree_oid") +VALUES (1, 1, '${release}') +ON CONFLICT ("id") DO NOTHING; +`; +} + +export const REMOVE_PROTOCOL_V3_CUTOVER_SQL = ` +${DROP_PROTOCOL_V3_CUTOVER_TRIGGERS_SQL} +DROP TABLE IF EXISTS "${PROTOCOL_V3_MIGRATION_INTENT_TABLE}"; +DROP TABLE IF EXISTS "${PROTOCOL_V3_CUTOVER_TABLE}"; +`; + +export const PROTOCOL_V3_CUTOVER_PROBE_SQL = ` +SELECT EXISTS( + SELECT 1 FROM "sqlite_master" + WHERE ("name" COLLATE NOCASE IN (${CUTOVER_RESERVED_NAMES_SQL}) + OR "name" = '${PROTOCOL_V3_MIGRATION_INTENT_TABLE}' COLLATE NOCASE) + OR ( + "type" = 'trigger' + AND "tbl_name" COLLATE NOCASE IN (${CUTOVER_PROTECTED_TABLES_SQL}) + AND NOT ( + ${PROTOCOL_V3_LEGACY_REWRITE_GATE_UPDATE_TRIGGER_MATCH_SQL} + OR ${CUTOVER_MANAGED_TRIGGER_MATCH_SQL} + ) + ) + ) AS "gate_present"; +`; + +export const PROTOCOL_V3_LEGACY_TERMINAL_SOURCE_INVENTORY_SQL = ` +WITH "terminal_events" AS ( + SELECT + "event_type", + "id", + "run_id", + "session_id", + "source_event_id", + 'session-run-terminal:' || "run_id" || ':' || "event_type" AS "canonical_source_event_id" + FROM "session_event" + WHERE "event_type" IN ('run.cancelled', 'run.completed', 'run.failed') +), +"source_inventory" AS ( + SELECT + "terminal"."event_type", + count(*) AS "total", + sum("terminal"."source_event_id" = "terminal"."canonical_source_event_id") AS "canonical", + sum("terminal"."source_event_id" <> "terminal"."canonical_source_event_id") AS "noncanonical", + sum(EXISTS ( + SELECT 1 + FROM "session_event" AS "other" + WHERE "other"."session_id" = "terminal"."session_id" + AND "other"."source_event_id" = "terminal"."canonical_source_event_id" + AND "other"."id" <> "terminal"."id" + )) AS "canonical_target_collisions" + FROM "terminal_events" AS "terminal" + GROUP BY "terminal"."event_type" +), +"multiple_terminal_runs" AS ( + SELECT 1 + FROM "terminal_events" + WHERE "run_id" IS NOT NULL + GROUP BY "session_id", "run_id" + HAVING count(*) > 1 +) +SELECT + coalesce(max(CASE WHEN "event_type" = 'run.cancelled' THEN "total" END), 0) AS "cancelled_total", + coalesce(max(CASE WHEN "event_type" = 'run.cancelled' THEN "canonical" END), 0) AS "cancelled_canonical", + coalesce(max(CASE WHEN "event_type" = 'run.cancelled' THEN "noncanonical" END), 0) AS "cancelled_noncanonical", + coalesce(max(CASE WHEN "event_type" = 'run.cancelled' THEN "canonical_target_collisions" END), 0) AS "cancelled_canonical_target_collisions", + coalesce(max(CASE WHEN "event_type" = 'run.completed' THEN "total" END), 0) AS "completed_total", + coalesce(max(CASE WHEN "event_type" = 'run.completed' THEN "canonical" END), 0) AS "completed_canonical", + coalesce(max(CASE WHEN "event_type" = 'run.completed' THEN "noncanonical" END), 0) AS "completed_noncanonical", + coalesce(max(CASE WHEN "event_type" = 'run.completed' THEN "canonical_target_collisions" END), 0) AS "completed_canonical_target_collisions", + coalesce(max(CASE WHEN "event_type" = 'run.failed' THEN "total" END), 0) AS "failed_total", + coalesce(max(CASE WHEN "event_type" = 'run.failed' THEN "canonical" END), 0) AS "failed_canonical", + coalesce(max(CASE WHEN "event_type" = 'run.failed' THEN "noncanonical" END), 0) AS "failed_noncanonical", + coalesce(max(CASE WHEN "event_type" = 'run.failed' THEN "canonical_target_collisions" END), 0) AS "failed_canonical_target_collisions", + ( + SELECT count(*) + FROM "terminal_events" AS "event" + LEFT JOIN "session_run" AS "run" ON "run"."id" = "event"."run_id" + WHERE "event"."run_id" IS NULL + OR "run"."id" IS NULL + OR "run"."session_id" <> "event"."session_id" + ) AS "invalid_terminal_links", + ( + SELECT count(*) + FROM "terminal_events" AS "event" + INNER JOIN "session_run" AS "run" + ON "run"."id" = "event"."run_id" + AND "run"."session_id" = "event"."session_id" + WHERE NOT ( + ("run"."status" = 'completed' AND "event"."event_type" = 'run.completed') + OR ("run"."status" = 'failed' AND "event"."event_type" = 'run.failed') + OR ("run"."status" IN ('cancelled', 'expired') AND "event"."event_type" = 'run.cancelled') + ) + ) AS "mismatched_terminal_events", + (SELECT count(*) FROM "multiple_terminal_runs") AS "multiple_terminal_runs" +FROM "source_inventory"; +`; + +export const PROTOCOL_V3_LOSSY_MIGRATION_INVENTORY_SQL = ` +WITH "effect_source" AS ( + SELECT + "effect"."id" AS "effect_id", + "effect"."status" AS "effect_status", + "effect"."provider_receipt_json" AS "effect_provider_receipt_json", + "effect"."result_json" AS "effect_result_json", + "command"."error_json" AS "command_error_json", + "command"."payload_json" AS "command_payload_json", + "command"."result_json" AS "command_result_json", + "command"."status" AS "command_status", + max( + coalesce(length(CAST("effect"."result_json" AS BLOB)), 0), + coalesce(( + SELECT max(length(CAST("attempt"."result_json" AS BLOB))) + FROM "external_tool_effect_attempt" AS "attempt" + WHERE "attempt"."effect_id" = "effect"."id" + AND "attempt"."status" = 'succeeded' + ), 0), + coalesce(length(CAST("command"."result_json" AS BLOB)), 0) + ) AS "original_result_bytes" + FROM "external_tool_effect" AS "effect" + INNER JOIN "driver_command" AS "command" ON "command"."id" = "effect"."command_id" +), +"effect_result_classified" AS ( + SELECT + "effect_source".*, + "effect_status" = 'succeeded' AND ( + "original_result_bytes" > 1044480 + OR length(CAST('{"kind":"succeeded","result":' || "effect_result_json" || '}' AS BLOB)) > 1044480 + ) AS "result_omitted" + FROM "effect_source" +), +"effect_result_target" AS ( + SELECT + "effect_result_classified".*, + CASE + WHEN "result_omitted" THEN + '{"isError":true,"outputText":' || json_quote('Stored MCP result omitted because it contained ' || "original_result_bytes" || ' UTF-8 bytes.') || + ',"requestId":' || json_quote(json_extract("command_payload_json", '$.requestId')) || + ',"serverId":' || json_quote(json_extract("command_payload_json", '$.serverId')) || + ',"toolName":' || json_quote(json_extract("command_payload_json", '$.toolName')) || '}' + WHEN "effect_status" = 'succeeded' THEN "effect_result_json" + ELSE NULL + END AS "normalized_result_json" + FROM "effect_result_classified" +), +"effect_target" AS ( + SELECT + "effect_result_target".*, + CASE + WHEN "effect_status" <> 'succeeded' THEN NULL + WHEN "effect_provider_receipt_json" IS NULL THEN NULL + WHEN length(CAST( + '{"kind":"succeeded","providerReceiptJson":' || json_quote("effect_provider_receipt_json") || + ',"result":' || "normalized_result_json" || '}' + AS BLOB)) > 1044480 THEN NULL + ELSE "effect_provider_receipt_json" + END AS "normalized_provider_receipt_json" + FROM "effect_result_target" +), +"loss_candidates" ("category", "id") AS ( + SELECT "category"."value", "command"."id" + FROM "driver_command" AS "command" + CROSS JOIN json_each(json_array( + 'command_payload_conflict', + 'mcp_argument_omission', + 'input_text_omission', + 'input_start_result_omission', + 'control_reason_omission', + 'permission_payload_rewrite', + 'command_error_omission' + )) AS "category" + WHERE CASE "category"."value" + WHEN 'command_payload_conflict' THEN + EXISTS ( + SELECT "key" + FROM json_each("command"."payload_json") + GROUP BY "key" + HAVING count(*) > 1 + ) + OR ( + "command"."kind" = 'input.start' + AND EXISTS ( + SELECT "key" + FROM json_each("command"."payload_json", '$.input') + GROUP BY "key" + HAVING count(*) > 1 + ) + ) + WHEN 'mcp_argument_omission' THEN + "command"."kind" = 'mcp.execute' + AND length(CAST(json_set( + "command"."payload_json", + '$.commandId', "command"."id", + '$.runId', ( + SELECT "effect"."session_run_id" + FROM "external_tool_effect" AS "effect" + WHERE "effect"."command_id" = "command"."id" + LIMIT 1 + ) + ) AS BLOB)) > 824448 + WHEN 'input_text_omission' THEN + "command"."kind" = 'input.start' + AND length(CAST("command"."payload_json" AS BLOB)) > 824448 + WHEN 'input_start_result_omission' THEN + "command"."kind" = 'input.start' + AND "command"."result_json" IS NOT NULL + AND json_type("command"."result_json") <> 'null' + AND ( + length(CAST("command"."payload_json" AS BLOB)) > 824448 + OR length(CAST("command"."result_json" AS BLOB)) > 1044480 + ) + WHEN 'control_reason_omission' THEN + "command"."kind" IN ('turn.cancel', 'session.stop') + AND length(CAST("command"."payload_json" AS BLOB)) > 824448 + WHEN 'permission_payload_rewrite' THEN + "command"."kind" = 'permission.resolve' + AND length(CAST("command"."payload_json" AS BLOB)) > 824448 + WHEN 'command_error_omission' THEN + "command"."error_json" IS NOT NULL + AND length(CAST("command"."error_json" AS BLOB)) > 1044480 + AND NOT EXISTS ( + SELECT 1 + FROM "external_tool_effect" AS "effect" + WHERE "effect"."command_id" = "command"."id" AND "effect"."status" = 'succeeded' + ) + ELSE 0 + END + UNION ALL + SELECT "category"."value", "target"."effect_id" + FROM "effect_target" AS "target" + CROSS JOIN json_each(json_array( + 'mcp_result_omission', + 'mcp_result_conflict', + 'provider_receipt_loss', + 'mcp_command_terminal_conflict' + )) AS "category" + WHERE CASE "category"."value" + WHEN 'mcp_result_omission' THEN "target"."result_omitted" + WHEN 'mcp_result_conflict' THEN + ( + NOT "target"."result_omitted" + AND ( + ("target"."effect_status" <> 'succeeded' AND "target"."effect_result_json" IS NOT NULL) + OR ( + "target"."effect_status" = 'succeeded' + AND "target"."command_result_json" IS NOT NULL + AND json_type("target"."command_result_json") <> 'null' + AND "target"."command_result_json" IS NOT "target"."effect_result_json" + ) + OR EXISTS ( + SELECT 1 + FROM "external_tool_effect_attempt" AS "attempt" + WHERE "attempt"."effect_id" = "target"."effect_id" + AND "attempt"."result_json" IS NOT NULL + AND ( + "attempt"."status" <> 'succeeded' + OR "target"."normalized_result_json" IS NULL + OR "attempt"."result_json" IS NOT "target"."effect_result_json" + ) + ) + ) + ) + OR ( + "target"."effect_result_json" IS NOT NULL + AND EXISTS ( + SELECT "key" + FROM json_each("target"."effect_result_json") + GROUP BY "key" + HAVING count(*) > 1 + ) + ) + OR ( + "target"."command_result_json" IS NOT NULL + AND EXISTS ( + SELECT "key" + FROM json_each("target"."command_result_json") + GROUP BY "key" + HAVING count(*) > 1 + ) + ) + OR EXISTS ( + SELECT 1 + FROM "external_tool_effect_attempt" AS "attempt" + WHERE "attempt"."effect_id" = "target"."effect_id" + AND "attempt"."result_json" IS NOT NULL + AND EXISTS ( + SELECT "key" + FROM json_each("attempt"."result_json") + GROUP BY "key" + HAVING count(*) > 1 + ) + ) + WHEN 'provider_receipt_loss' THEN + "target"."effect_provider_receipt_json" IS NOT "target"."normalized_provider_receipt_json" + OR EXISTS ( + SELECT 1 + FROM "external_tool_effect_attempt" AS "attempt" + WHERE "attempt"."effect_id" = "target"."effect_id" + AND "attempt"."provider_receipt_json" IS NOT NULL + AND "attempt"."provider_receipt_json" IS NOT CASE + WHEN "attempt"."status" = 'succeeded' THEN "target"."normalized_provider_receipt_json" + ELSE NULL + END + ) + WHEN 'mcp_command_terminal_conflict' THEN + "target"."effect_status" = 'succeeded' + AND ( + "target"."command_status" <> 'completed' OR "target"."command_error_json" IS NOT NULL + ) + ELSE 0 + END + UNION ALL + SELECT 'orphan_effect', "effect"."id" + FROM "external_tool_effect" AS "effect" + WHERE NOT EXISTS ( + SELECT 1 FROM "driver_command" AS "command" WHERE "command"."id" = "effect"."command_id" + ) + UNION ALL + SELECT DISTINCT 'attempt_completion_time_fabrication', "attempt"."effect_id" + FROM "external_tool_effect_attempt" AS "attempt" + WHERE "attempt"."status" IN ('succeeded', 'unknown') + AND "attempt"."completed_at" IS NULL + UNION ALL + SELECT 'session_run_error_omission', "run"."id" + FROM "session_run" AS "run" + WHERE "run"."error_code" IS NOT NULL + AND "run"."error_message" IS NOT NULL + AND length(CAST( + '{"code":' || json_quote("run"."error_code") || + ',"details":' || coalesce(nullif("run"."error_details_json", ''), '{}') || + ',"message":' || json_quote("run"."error_message") || + ',"retryable":false}' + AS BLOB)) > 1044480 +) +SELECT + (SELECT count(*) FROM "loss_candidates" WHERE "category" = 'attempt_completion_time_fabrication') AS "attempt_completion_time_fabrications", + (SELECT count(*) FROM "loss_candidates" WHERE "category" = 'command_payload_conflict') AS "command_payload_conflicts", + (SELECT count(*) FROM "loss_candidates" WHERE "category" = 'mcp_argument_omission') AS "mcp_argument_omissions", + (SELECT count(*) FROM "loss_candidates" WHERE "category" = 'input_text_omission') AS "input_text_omissions", + (SELECT count(*) FROM "loss_candidates" WHERE "category" = 'input_start_result_omission') AS "input_start_result_omissions", + (SELECT count(*) FROM "loss_candidates" WHERE "category" = 'control_reason_omission') AS "control_reason_omissions", + (SELECT count(*) FROM "loss_candidates" WHERE "category" = 'permission_payload_rewrite') AS "permission_payload_rewrites", + (SELECT count(*) FROM "loss_candidates" WHERE "category" = 'mcp_result_omission') AS "mcp_result_omissions", + (SELECT count(*) FROM "loss_candidates" WHERE "category" = 'orphan_effect') AS "orphan_effects", + (SELECT count(*) FROM "loss_candidates" WHERE "category" = 'mcp_result_conflict') AS "mcp_result_conflicts", + (SELECT count(*) FROM "loss_candidates" WHERE "category" = 'provider_receipt_loss') AS "provider_receipt_losses", + (SELECT count(*) FROM "loss_candidates" WHERE "category" = 'mcp_command_terminal_conflict') AS "mcp_command_terminal_conflicts", + (SELECT count(*) FROM "loss_candidates" WHERE "category" = 'command_error_omission') AS "command_error_omissions", + (SELECT count(*) FROM "loss_candidates" WHERE "category" = 'session_run_error_omission') AS "session_run_error_omissions", + (SELECT count(*) FROM "loss_candidates") AS "total_candidates", + coalesce(( + SELECT json_group_array(json_array("category", "id")) + FROM ( + SELECT "category", "id" + FROM "loss_candidates" + ORDER BY "category", "id" + LIMIT 50 + ) + ), json('[]')) AS "candidate_ids_json"; +`; + +export const PROTOCOL_V3_LEGACY_TERMINAL_INTEGRITY_SQL = ` +WITH "terminal_events" AS ( + SELECT "event_type", "id", "run_id", "seq", "session_id", "source_event_id" + FROM "session_event" + WHERE "event_type" IN ('run.cancelled', 'run.completed', 'run.failed') +), +"assistant_runs" AS ( + SELECT "session_id", "session_run_id", count(*) AS "assistant_count" + FROM "session_message" + WHERE "role" = 'assistant' AND "session_run_id" IS NOT NULL + GROUP BY "session_id", "session_run_id" +) +SELECT + ( + SELECT count(*) + FROM ( + SELECT 1 + FROM "terminal_events" + WHERE "run_id" IS NOT NULL + GROUP BY "session_id", "run_id" + HAVING count(*) > 1 + ) + ) AS "duplicate_terminal_runs", + ( + SELECT count(*) + FROM "terminal_events" AS "event" + INNER JOIN "session_run" AS "run" + ON "run"."id" = "event"."run_id" + AND "run"."session_id" = "event"."session_id" + WHERE NOT ( + ("run"."status" = 'completed' AND "event"."event_type" = 'run.completed') + OR ("run"."status" = 'failed' AND "event"."event_type" = 'run.failed') + OR ("run"."status" IN ('cancelled', 'expired') AND "event"."event_type" = 'run.cancelled') + ) + ) AS "mismatched_terminal_events", + ( + SELECT count(*) + FROM "terminal_events" AS "event" + LEFT JOIN "session_run" AS "run" ON "run"."id" = "event"."run_id" + WHERE "event"."run_id" IS NULL + OR "run"."id" IS NULL + OR "run"."session_id" <> "event"."session_id" + ) AS "invalid_terminal_links", + ( + SELECT count(*) + FROM "terminal_events" + WHERE "run_id" IS NULL + OR "source_event_id" <> 'session-run-terminal:' || "run_id" || ':' || "event_type" + ) AS "noncanonical_terminal_sources", + ( + SELECT json_group_array(json_array( + "id", "session_id", "run_id", "event_type", "source_event_id", "seq" + )) + FROM ( + SELECT "id", "session_id", "run_id", "event_type", "source_event_id", "seq" + FROM "terminal_events" + WHERE "run_id" IS NULL + OR "source_event_id" <> + 'session-run-terminal:' || "run_id" || ':' || "event_type" + ORDER BY "id" COLLATE BINARY + ) + ) AS "rewrite_candidate_manifest_json", + ( + SELECT count(*) + FROM "session_run" AS "run" + WHERE "run"."status" IN ('cancelled', 'completed', 'expired', 'failed') + AND NOT EXISTS ( + SELECT 1 + FROM "terminal_events" AS "event" + WHERE "event"."session_id" = "run"."session_id" + AND "event"."run_id" = "run"."id" + ) + ) AS "missing_terminal_events", + ( + SELECT count(*) + FROM "terminal_events" AS "event" + INNER JOIN "session_run" AS "run" + ON "run"."id" = "event"."run_id" + AND "run"."session_id" = "event"."session_id" + LEFT JOIN "session" ON "session"."id" = "run"."session_id" + WHERE "session"."id" IS NULL + OR "run"."completed_at" IS NULL + OR "run"."status_event" <> CASE "run"."status" + WHEN 'completed' THEN 'run.complete' + WHEN 'failed' THEN 'run.fail' + WHEN 'cancelled' THEN 'run.cancel' + WHEN 'expired' THEN 'run.expire' + END + OR "event"."seq" > "session"."runtime_event_seq_cursor" + OR ( + "session"."last_run_id" = "run"."id" + AND ( + "session"."status" NOT IN ('IDLE', 'TERMINATED') + OR "session"."status_operation_id" IS NOT NULL + ) + ) + OR EXISTS ( + SELECT 1 + FROM "session_permission_request" AS "permission" + WHERE "permission"."session_id" = "run"."session_id" + AND "permission"."run_id" = "run"."id" + ) + ) AS "partial_terminal_projections", + ( + SELECT count(*) + FROM "assistant_runs" AS "assistant" + INNER JOIN "session_run" AS "run" + ON "run"."id" = "assistant"."session_run_id" + AND "run"."session_id" = "assistant"."session_id" + WHERE "run"."status" = 'completed' AND "assistant"."assistant_count" > 1 + ) AS "ambiguous_assistant_runs", + ( + SELECT count(*) + FROM "session_message" AS "message" + LEFT JOIN "session_run" AS "run" ON "run"."id" = "message"."session_run_id" + LEFT JOIN "session" ON "session"."id" = "message"."session_id" + WHERE "message"."role" = 'assistant' + AND "message"."session_run_id" IS NOT NULL + AND ( + "run"."id" IS NULL + OR "run"."session_id" <> "message"."session_id" + OR "session"."id" IS NULL + OR "message"."seq" > "session"."message_seq_cursor" + OR "run"."status" IN ('cancelled', 'expired', 'failed') + OR CASE + WHEN "message"."plan_json" IS NULL OR "message"."plan_json" = '' THEN 0 + WHEN json_valid("message"."plan_json") = 0 THEN 1 + WHEN json_type("message"."plan_json") <> 'array' THEN 1 + ELSE 0 + END = 1 + OR CASE + WHEN "message"."segments_json" IS NULL OR "message"."segments_json" = '' THEN 0 + WHEN json_valid("message"."segments_json") = 0 THEN 1 + WHEN json_type("message"."segments_json") <> 'array' THEN 1 + ELSE 0 + END = 1 + ) + ) AS "partial_assistant_projections", + ( + SELECT count(*) + FROM "session_run" + WHERE "status" = 'failed' + AND ( + "error_code" IS NULL + OR trim("error_code") = '' + OR "error_message" IS NULL + OR trim("error_message") = '' + OR CASE + WHEN "error_details_json" IS NULL THEN 0 + WHEN json_valid("error_details_json") = 0 THEN 1 + WHEN json_type("error_details_json") <> 'object' THEN 1 + ELSE 0 + END = 1 + ) + ) AS "invalid_failed_runs", + ( + SELECT count(*) + FROM "session_run" + WHERE "status" IN ('cancelled', 'completed', 'expired') + AND ( + "error_code" IS NOT NULL + OR "error_details_json" IS NOT NULL + OR "error_message" IS NOT NULL + ) + ) AS "invalid_nonfailed_run_errors", + ( + SELECT count(*) + FROM "session_run" + WHERE "status" = 'failed' + AND "error_code" IS NOT NULL + AND trim("error_code") <> '' + AND "error_message" IS NOT NULL + AND trim("error_message") <> '' + AND CASE + WHEN "error_details_json" IS NULL THEN 1 + WHEN json_valid("error_details_json") = 0 THEN 0 + WHEN json_type("error_details_json") = 'object' THEN 1 + ELSE 0 + END = 1 + ) AS "repairable_failed_runs", + ( + SELECT count(*) + FROM "driver_command" + WHERE "status" IN ('queued', 'delivered', 'accepted') + ) AS "nonterminal_commands", + ( + SELECT count(*) + FROM "external_tool_effect" + WHERE "status" IN ('executing', 'claimed') + ) AS "unsettled_effects", + (SELECT count(*) FROM "session_message") AS "legacy_materialized_messages", + (SELECT count(*) FROM "terminal_events") AS "legacy_terminal_events", + ( + SELECT count(*) + FROM "session_event" + WHERE "event_type" LIKE 'message.%' OR "event_type" LIKE 'thought.%' + ) AS "legacy_stream_rows"; +`; + +export const PROTOCOL_V3_LEGACY_REWRITE_AUTHORIZATION_TABLE_SQL = `CREATE TABLE "${PROTOCOL_V3_LEGACY_REWRITE_AUTHORIZATION_TABLE}" ( + "id" integer PRIMARY KEY CHECK ("id" = 1), + "gate_id" integer NOT NULL CHECK ("gate_id" = 1) REFERENCES "${PROTOCOL_V3_CUTOVER_TABLE}" ("id") ON DELETE CASCADE, + "bookmark" text NOT NULL CHECK (length(trim("bookmark")) > 0), + "candidate_count" integer NOT NULL CHECK ("candidate_count" >= 0), + "candidate_manifest_json" text NOT NULL CHECK (json_valid("candidate_manifest_json") AND json_type("candidate_manifest_json") = 'array'), + "deploy_owner" text NOT NULL, + "expires_at" integer NOT NULL, + "release_tree_oid" text NOT NULL CHECK ((length("release_tree_oid") = 40 OR length("release_tree_oid") = 64) AND "release_tree_oid" = lower("release_tree_oid") AND "release_tree_oid" NOT GLOB '*[^0-9a-f]*') +)`; + +const PROTOCOL_V3_LEGACY_REWRITE_MANIFEST_SQL = ` +SELECT + count(*) AS "candidate_count", + json_group_array(json_array( + "id", "session_id", "run_id", "event_type", "source_event_id", "seq" + )) AS "candidate_manifest_json" +FROM ( + SELECT "id", "session_id", "run_id", "event_type", "source_event_id", "seq" + FROM "session_event" + WHERE "event_type" IN ('run.cancelled', 'run.completed', 'run.failed') + AND "source_event_id" <> + 'session-run-terminal:' || "run_id" || ':' || "event_type" + ORDER BY "id" COLLATE BINARY +)`; + +export function authorizeProtocolV3LegacyRewriteSql( + owner: string, + candidateCount: number, + candidateManifestJson: string, +): string { + if (!/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/u.test(owner)) { + throw new Error("Protocol v3 legacy rewrite owner must be a UUID v4."); + } + let candidateManifest: unknown; + try { + candidateManifest = JSON.parse(candidateManifestJson); + } catch { + throw new Error("Protocol v3 legacy rewrite manifest must be valid JSON."); + } + if ( + !Number.isSafeInteger(candidateCount) || + candidateCount < 0 || + !Array.isArray(candidateManifest) || + candidateManifest.length !== candidateCount + ) { + throw new Error("Protocol v3 legacy rewrite manifest count is invalid."); + } + const quotedOwner = sqlString(owner); + const quotedManifest = sqlString(candidateManifestJson); + return ` +${PROTOCOL_V3_LEGACY_REWRITE_AUTHORIZATION_TABLE_SQL.replace("CREATE TABLE", "CREATE TABLE IF NOT EXISTS")}; +${PROTOCOL_V3_LEGACY_REWRITE_GATE_UPDATE_TRIGGER_SQL.replace("CREATE TRIGGER", "CREATE TRIGGER IF NOT EXISTS")}; +DELETE FROM "${PROTOCOL_V3_LEGACY_REWRITE_AUTHORIZATION_TABLE}" WHERE "id" = 1; +WITH "manifest" AS (${PROTOCOL_V3_LEGACY_REWRITE_MANIFEST_SQL}) +INSERT INTO "${PROTOCOL_V3_LEGACY_REWRITE_AUTHORIZATION_TABLE}" ( + "id", "gate_id", "bookmark", "candidate_count", "candidate_manifest_json", "deploy_owner", "expires_at", "release_tree_oid" +) +SELECT + 1, + "gate"."id", + "gate"."pre_migration_bookmark", + "manifest"."candidate_count", + "manifest"."candidate_manifest_json", + "lease"."owner", + unixepoch() + 600, + "gate"."release_tree_oid" +FROM "manifest" +INNER JOIN "${PROTOCOL_V3_CUTOVER_TABLE}" AS "gate" + ON "gate"."id" = 1 + AND "gate"."enabled" = 1 + AND "gate"."command_freeze" = 1 + AND "gate"."phase" = 'draining' + AND length(trim("gate"."pre_migration_bookmark")) > 0 + AND "gate"."smoke_account_id" IS NULL + AND "gate"."smoke_request_key" IS NULL + AND "gate"."smoke_session_id" IS NULL +INNER JOIN "${PROD_DEPLOY_LEASE_TABLE}" AS "lease" + ON "lease"."id" = 1 + AND "lease"."owner" = ${quotedOwner} +WHERE NOT EXISTS ( + SELECT 1 FROM "session_run" + WHERE "status" IN (${ACTIVE_RUN_STATUSES}) +) +AND NOT EXISTS ( + SELECT 1 FROM "driver_instance" + WHERE "status" IN (${LIVE_DRIVER_STATUSES}) +) +AND NOT EXISTS ( + SELECT 1 FROM "driver_command" + WHERE "status" IN ('queued', 'delivered', 'accepted') +) +AND NOT EXISTS ( + SELECT 1 FROM "external_tool_effect" + WHERE "status" IN ('executing', 'claimed') +) +AND NOT EXISTS ( + SELECT 1 FROM "api_command" + WHERE "status" IN ('queued', 'running') +) +AND NOT EXISTS ( + SELECT 1 FROM "app_deployment_run" + WHERE "status" IN (${ACTIVE_APP_DEPLOYMENT_RUN_STATUSES}) +) +AND NOT EXISTS ( + SELECT 1 FROM "sandbox" + WHERE "status" <> 'cold' + OR "status_operation_id" IS NOT NULL + OR "claim_owner" IS NOT NULL + OR "claim_expires_at" IS NOT NULL +) +AND NOT EXISTS ( + SELECT 1 FROM "sandbox_session" + WHERE "status" NOT IN ('closed', 'error') +) +AND NOT EXISTS ( + SELECT 1 FROM "sandbox_backup" + WHERE "status" NOT IN ('ready', 'pruned') OR "error_message" IS NOT NULL +) +AND NOT EXISTS ( + SELECT 1 FROM "session" + WHERE "status" NOT IN ('IDLE', 'TERMINATED') OR "status_operation_id" IS NOT NULL +) +AND "manifest"."candidate_count" = ${candidateCount} +AND "manifest"."candidate_manifest_json" = ${quotedManifest} COLLATE BINARY; +`; +} + +export const PROTOCOL_V3_LEGACY_REWRITE_AUTHORIZATION_SQL = ` +SELECT + "authorization"."bookmark", + "authorization"."candidate_count", + "authorization"."candidate_manifest_json", + "authorization"."deploy_owner", + "authorization"."expires_at", + "authorization"."release_tree_oid", + (SELECT "sql" FROM "sqlite_master" + WHERE "type" = 'table' AND "name" = '${PROTOCOL_V3_LEGACY_REWRITE_AUTHORIZATION_TABLE}') AS "authorization_table_sql", + (SELECT "sql" FROM "sqlite_master" + WHERE "type" = 'trigger' AND "name" = '${PROTOCOL_V3_LEGACY_REWRITE_GATE_UPDATE_TRIGGER}') AS "gate_update_trigger_sql" +FROM "${PROTOCOL_V3_LEGACY_REWRITE_AUTHORIZATION_TABLE}" AS "authorization" +WHERE "authorization"."id" = 1; +`; + +function protocolV3CutoverDrainSql(postRuntimeAuthorityMigration: boolean): string { + const sandboxOperation = postRuntimeAuthorityMigration + ? '\n OR "operation_kind" IS NOT NULL' + : ""; + const unsafeSessions = postRuntimeAuthorityMigration + ? `NOT (${postMigrationSessionStaticSql('"session"')})` + : `"status" NOT IN ('IDLE', 'TERMINATED') OR "status_operation_id" IS NOT NULL`; + const stagingCount = postRuntimeAuthorityMigration + ? '(SELECT count(*) FROM "sandbox_backup_staging")' + : "0"; + const environmentArtifactStagingCount = postRuntimeAuthorityMigration + ? '(SELECT count(*) FROM "environment_package_artifact_backup_staging")' + : "0"; + return ` +SELECT + (SELECT count(*) FROM "session_run" + WHERE "status" IN (${ACTIVE_RUN_STATUSES})) AS "active_runs", + (SELECT count(*) FROM "app_deployment_run" + WHERE "status" IN (${ACTIVE_APP_DEPLOYMENT_RUN_STATUSES})) AS "active_app_deployment_runs", + (SELECT count(*) FROM "driver_instance" + WHERE "status" IN (${LIVE_DRIVER_STATUSES})) AS "live_drivers", + (SELECT count(*) FROM "external_tool_effect" + WHERE "status" IN ('executing', 'claimed')) AS "unsettled_effects", + (SELECT count(*) FROM "driver_command" + WHERE "status" IN ('queued', 'delivered', 'accepted')) AS "nonterminal_commands", + (SELECT count(*) FROM "api_command" + WHERE "status" IN ('queued', 'running')) AS "nonterminal_api_commands", + (SELECT count(*) FROM "sandbox" + WHERE "status" <> 'cold' + ${sandboxOperation} + OR "status_operation_id" IS NOT NULL + OR "claim_owner" IS NOT NULL + OR "claim_expires_at" IS NOT NULL) AS "unsafe_sandboxes", + (SELECT count(*) FROM "sandbox_session" + WHERE "status" NOT IN ('closed', 'error')) AS "unsafe_sandbox_sessions", + (SELECT count(*) FROM "sandbox_backup" + WHERE "status" NOT IN ('ready', 'pruned')) AS "unsafe_sandbox_backups", + ${stagingCount} AS "unsafe_sandbox_backup_staging", + ${environmentArtifactStagingCount} AS "unsafe_environment_artifact_backup_staging", + (SELECT count(*) FROM "session" WHERE ${unsafeSessions}) AS "unsafe_sessions"; +`; +} + +export const PROTOCOL_V3_CUTOVER_DRAIN_SQL = protocolV3CutoverDrainSql(false); +export const PROTOCOL_V3_POST_MIGRATION_CUTOVER_DRAIN_SQL = protocolV3CutoverDrainSql(true); + +function protocolV3UnsafeSandboxesSql(postRuntimeAuthorityMigration: boolean): string { + const operationColumn = postRuntimeAuthorityMigration ? ', "operation_kind"' : ""; + const operationPredicate = postRuntimeAuthorityMigration + ? '\n OR "operation_kind" IS NOT NULL' + : ""; + return ` +SELECT "id", "status", "status_operation_id", "claim_owner", "claim_expires_at"${operationColumn} +FROM "sandbox" +WHERE "status" <> 'cold' + ${operationPredicate} + OR "status_operation_id" IS NOT NULL + OR "claim_owner" IS NOT NULL + OR "claim_expires_at" IS NOT NULL +ORDER BY "id" +LIMIT 50; +`; +} + +export const PROTOCOL_V3_UNSAFE_SANDBOXES_SQL = protocolV3UnsafeSandboxesSql(false); +export const PROTOCOL_V3_POST_MIGRATION_UNSAFE_SANDBOXES_SQL = protocolV3UnsafeSandboxesSql(true); + +export function protocolV3RuntimeAuthorityPreflightSql( + sessionCleanupMigrationPending: boolean, +): string { + const nonstaticSessionsSql = sessionCleanupMigrationPending + ? `(SELECT count(*) + FROM "session" + WHERE "status" NOT IN ('IDLE', 'TERMINATED') + OR "status_operation_id" IS NOT NULL + )` + : `(SELECT count(*) + FROM "session" + WHERE "status" NOT IN ('IDLE', 'TERMINATED') + OR "status_operation_id" IS NOT NULL + OR NOT ( + "cleanup_operation_kind" IS NULL + OR ( + "cleanup_operation_kind" = 'archive' + AND "status" = 'IDLE' + AND "archived_at" IS NOT NULL + ) + ) + OR "runtime_provisioning_operation_id" IS NOT NULL + OR "runtime_provisioning_run_id" IS NOT NULL + OR "runtime_provisioning_sandbox_id" IS NOT NULL + OR "runtime_provisioning_heartbeat_at" IS NOT NULL + )`; + return ` +WITH "runtime_subject_identity" ( + "sandbox_id", "kind", "subject_kind", "subject_id", "agent_id", "app_id", "owner_account_id" +) AS ( + SELECT + "sandbox"."id", 'pet', 'agent', "sandbox"."subject_id", + "agent"."id", "agent"."app_id", "agent"."owner_account_id" + FROM "sandbox" + INNER JOIN "agent" + ON "sandbox"."kind" = 'pet' + AND "sandbox"."subject_kind" = 'agent' + AND "agent"."id" = "sandbox"."subject_id" + AND "agent"."kind" = 'pet' + INNER JOIN "app" ON "app"."id" = "agent"."app_id" + UNION ALL + SELECT + "sandbox"."id", 'cattle', 'session', "sandbox"."subject_id", + "agent"."id", "session"."app_id", "agent"."owner_account_id" + FROM "sandbox" + INNER JOIN "session" + ON "sandbox"."kind" = 'cattle' + AND "sandbox"."subject_kind" = 'session' + AND "session"."id" = "sandbox"."subject_id" + AND "session"."kind" = 'cattle' + INNER JOIN "agent" + ON "agent"."id" = "session"."agent_id" + AND "agent"."app_id" = "session"."app_id" + AND "agent"."kind" = 'cattle' + INNER JOIN "app" ON "app"."id" = "session"."app_id" +) +SELECT + (SELECT count(*) + FROM "sandbox" AS "sandbox" + LEFT JOIN "runtime_subject_identity" AS "identity" + ON "identity"."sandbox_id" = "sandbox"."id" + WHERE "identity"."sandbox_id" IS NULL + OR NOT ( + ("sandbox"."agent_id" IS NULL + AND "sandbox"."app_id" IS NULL + AND "sandbox"."owner_account_id" IS NULL) + OR ("sandbox"."agent_id" IS "identity"."agent_id" + AND "sandbox"."app_id" IS "identity"."app_id" + AND "sandbox"."owner_account_id" IS "identity"."owner_account_id") + ) + ) AS "invalid_sandbox_identities", + (SELECT count(*) + FROM "sandbox_session" AS "workspace" + LEFT JOIN "runtime_subject_identity" AS "identity" + ON "identity"."sandbox_id" = "workspace"."sandbox_id" + LEFT JOIN "session" AS "session" ON "session"."id" = "workspace"."session_id" + WHERE "identity"."sandbox_id" IS NULL + OR "session"."id" IS NULL + OR "session"."kind" IS NOT "identity"."kind" + OR "session"."agent_id" IS NOT "identity"."agent_id" + OR "session"."app_id" IS NOT "identity"."app_id" + OR ("identity"."subject_kind" = 'session' + AND "workspace"."session_id" IS NOT "identity"."subject_id") + OR ("identity"."subject_kind" = 'agent' + AND "session"."agent_id" IS NOT "identity"."subject_id") + ) AS "invalid_sandbox_session_authorities", + (SELECT count(*) + FROM "driver_instance" + WHERE typeof("generation") <> 'integer' + OR "generation" NOT BETWEEN 0 AND 9007199254740991 + ) AS "invalid_driver_generations", + ((SELECT count(*) + FROM "sandbox" + WHERE "last_backup_id" IS NOT NULL + AND NOT EXISTS ( + SELECT 1 FROM "sandbox_backup" + WHERE "sandbox_backup"."id" = "sandbox"."last_backup_id" + AND "sandbox_backup"."sandbox_id" = "sandbox"."id" + AND "sandbox_backup"."status" = 'ready' + )) + + + (SELECT count(*) + FROM "sandbox" + WHERE "last_restore_backup_id" IS NOT NULL + AND NOT EXISTS ( + SELECT 1 FROM "sandbox_backup" + WHERE "sandbox_backup"."id" = "sandbox"."last_restore_backup_id" + AND "sandbox_backup"."sandbox_id" = "sandbox"."id" + AND "sandbox_backup"."status" IN ('ready', 'pruned') + ))) AS "invalid_sandbox_backup_pointers", + (SELECT count(*) + FROM "app_deployment" + WHERE "last_successful_url" IS NOT NULL + ) AS "legacy_app_deployment_traffic", + ${nonstaticSessionsSql} AS "nonstatic_sessions", + (SELECT count(*) FROM pragma_foreign_key_check) AS "foreign_key_violations", + (SELECT count(*) + FROM "sandbox_backup" AS "backup" + LEFT JOIN "runtime_subject_identity" AS "identity" + ON "identity"."sandbox_id" = "backup"."sandbox_id" + LEFT JOIN "session_run" AS "run" ON "run"."id" = "backup"."session_run_id" + LEFT JOIN "session" AS "run_session" ON "run_session"."id" = "run"."session_id" + WHERE "backup"."status" NOT IN ('ready', 'pruned') + OR "backup"."error_message" IS NOT NULL + OR typeof("backup"."dir") <> 'text' + OR length("backup"."dir") = 0 + OR typeof("backup"."keep") <> 'integer' + OR "backup"."keep" NOT IN (0, 1) + OR typeof("backup"."ttl_seconds") <> 'integer' + OR "backup"."ttl_seconds" NOT BETWEEN 1 AND 9007199254740991 + OR typeof("backup"."created_at") <> 'integer' + OR "backup"."created_at" NOT BETWEEN 0 AND 9007199254740991 + OR typeof("backup"."updated_at") <> 'integer' + OR "backup"."updated_at" NOT BETWEEN "backup"."created_at" AND 9007199254740991 + OR ("backup"."session_run_id" IS NOT NULL AND ( + "identity"."sandbox_id" IS NULL + OR "run"."id" IS NULL + OR "run"."status" IS NOT 'completed' + OR "run"."agent_id" IS NOT "identity"."agent_id" + OR "run_session"."id" IS NULL + OR "run_session"."kind" IS NOT "identity"."kind" + OR NOT EXISTS ( + SELECT 1 + FROM "sandbox_session" AS "workspace" + WHERE "workspace"."session_id" = "run"."session_id" + AND "workspace"."sandbox_id" = "backup"."sandbox_id" + AND "workspace"."cwd" = "backup"."dir" + ) + OR ("identity"."subject_kind" = 'session' + AND "run_session"."id" IS NOT "identity"."subject_id") + OR ("identity"."subject_kind" = 'agent' AND ( + "run_session"."agent_id" IS NOT "identity"."agent_id" + OR "run_session"."app_id" IS NOT "identity"."app_id" + )) + )) + ) AS "invalid_sandbox_backups", + (SELECT count(*) + FROM ( + SELECT 1 + FROM "sandbox_backup" + WHERE "session_run_id" IS NOT NULL + GROUP BY "sandbox_id", "dir", "session_run_id" + HAVING count(*) > 1 + ) + ) AS "duplicate_sandbox_backups"; +`; +} + +export const PROTOCOL_V3_RUNTIME_AUTHORITY_PREFLIGHT_SQL = + protocolV3RuntimeAuthorityPreflightSql(false); + +export const PROTOCOL_V3_CUTOVER_BOOKMARK_SQL = ` +SELECT "pre_migration_bookmark" AS "bookmark" +FROM "${PROTOCOL_V3_CUTOVER_TABLE}" +WHERE "id" = 1; +`; + +export const PROTOCOL_V3_SMOKE_SESSION_SQL = ` +SELECT "smoke_session_id" AS "smoke_session_id" +FROM "${PROTOCOL_V3_CUTOVER_TABLE}" +WHERE "id" = 1; +`; + +export const PROTOCOL_V3_SMOKE_REQUEST_KEY_SQL = ` +SELECT "smoke_request_key" AS "smoke_request_key" +FROM "${PROTOCOL_V3_CUTOVER_TABLE}" +WHERE "id" = 1; +`; + +export const ENTER_PROTOCOL_V3_DRAIN_SQL = ` +UPDATE "${PROTOCOL_V3_CUTOVER_TABLE}" +SET + "command_freeze" = 0, + "smoke_account_id" = NULL, + "smoke_request_key" = NULL, + "smoke_session_id" = NULL +WHERE "id" = 1 AND "enabled" = 1 AND "phase" = 'draining'; +`; + +export const ENABLE_PROTOCOL_V3_COMMAND_FREEZE_SQL = ` +UPDATE "${PROTOCOL_V3_CUTOVER_TABLE}" +SET "command_freeze" = 1 +WHERE "id" = 1 + AND "enabled" = 1 + AND "phase" = 'draining' + AND NOT EXISTS ( + SELECT 1 FROM "api_command" WHERE "status" IN ('queued', 'running') + ) + AND NOT EXISTS ( + SELECT 1 FROM "app_deployment_run" + WHERE "status" IN (${ACTIVE_APP_DEPLOYMENT_RUN_STATUSES}) + ); +`; + +export const PROTOCOL_V3_COMMAND_FREEZE_SQL = ` +SELECT + "command_freeze" AS "command_freeze", + "release_tree_oid" AS "release_tree_oid", + "enabled" AS "enabled", + "phase" AS "phase", + "target_container_application_version" AS "target_container_application_version", + "target_container_image_digest" AS "target_container_image_digest", + "target_worker_version_id" AS "target_worker_version_id", + EXISTS ( + SELECT 1 + FROM "${PROTOCOL_V3_MIGRATION_INTENT_TABLE}" AS "intent" + WHERE "intent"."id" = "gate"."id" + ) AS "migration_started", + (SELECT "sql" FROM "sqlite_master" + WHERE "type" = 'table' AND "name" = '${PROTOCOL_V3_MIGRATION_INTENT_TABLE}') AS "migration_intent_table_sql", + (SELECT count(*) FROM "sqlite_master" + WHERE "type" = 'trigger' AND "tbl_name" = '${PROTOCOL_V3_MIGRATION_INTENT_TABLE}' COLLATE BINARY) AS "migration_intent_trigger_count" +FROM "${PROTOCOL_V3_CUTOVER_TABLE}" AS "gate" +WHERE "gate"."id" = 1; +`; + +export function beginProtocolV3MigrationSql(releaseTreeOid: string): string { + const release = requireGitTreeOid(releaseTreeOid, "Protocol v3 release tree OID"); + return `INSERT INTO "${PROTOCOL_V3_MIGRATION_INTENT_TABLE}" ("id") +SELECT "id" +FROM "${PROTOCOL_V3_CUTOVER_TABLE}" +WHERE "id" = 1 + AND "enabled" = 1 + AND "command_freeze" = 1 + AND "phase" = 'draining' + AND "release_tree_oid" = '${release}' +ON CONFLICT ("id") DO NOTHING;`; +} + +export function storeProtocolV3RolloutSql( + releaseTreeOid: string, + workerVersionId: string, + containerApplicationVersion: number, + containerImageDigest: string, +): string { + const release = requireGitTreeOid(releaseTreeOid, "Protocol v3 release tree OID"); + const workerVersion = requireWorkerVersionId(workerVersionId); + if (!Number.isSafeInteger(containerApplicationVersion) || containerApplicationVersion < 0) { + throw new Error("Protocol v3 Container application version must be a non-negative integer."); + } + const imageDigest = requireSha256Digest( + containerImageDigest, + "Protocol v3 Container image digest", + ); + return `UPDATE "${PROTOCOL_V3_CUTOVER_TABLE}" +SET "target_container_application_version" = ${containerApplicationVersion}, + "target_container_image_digest" = '${imageDigest}', + "target_worker_version_id" = '${workerVersion}' +WHERE "id" = 1 + AND "enabled" = 1 + AND "phase" = 'draining' + AND "release_tree_oid" = '${release}' + AND ( + ("target_container_application_version" IS NULL AND "target_container_image_digest" IS NULL AND "target_worker_version_id" IS NULL) + OR ( + "target_container_application_version" = ${containerApplicationVersion} + AND "target_container_image_digest" = '${imageDigest}' + AND "target_worker_version_id" = '${workerVersion}' + ) + );`; +} + +export const ENTER_PROTOCOL_V3_QUEUES_RESUMING_SQL = ` +UPDATE "${PROTOCOL_V3_CUTOVER_TABLE}" +SET + "command_freeze" = 1, + "enabled" = 1, + "phase" = 'queues_resuming', + "smoke_account_id" = NULL, + "smoke_request_key" = NULL, + "smoke_session_id" = NULL +WHERE "id" = 1 AND "enabled" = 1 AND "phase" = 'draining'; +`; + +export const ACCEPT_PROTOCOL_V3_QUEUE_RESUME_SQL = ` +UPDATE "${PROTOCOL_V3_CUTOVER_TABLE}" +SET "enabled" = 0 +WHERE "id" = 1 + AND "enabled" = 1 + AND "command_freeze" = 1 + AND "phase" = 'queues_resuming'; +`; + +export const CLOSE_PROTOCOL_V3_SMOKE_WINDOW_SQL = ` +UPDATE "${PROTOCOL_V3_CUTOVER_TABLE}" +SET + "command_freeze" = 1, + "smoke_account_id" = NULL, + "smoke_request_key" = NULL, + "smoke_session_id" = NULL +WHERE "id" = 1 AND "enabled" = 1 AND "phase" = 'draining'; +`; + +export const PROTOCOL_V3_CUTOVER_OBJECTS_SQL = ` +WITH "post_schema" ("value") AS ( + SELECT EXISTS ( + SELECT 1 FROM "sqlite_master" + WHERE "type" = 'table' AND "name" = 'sandbox_backup_staging' COLLATE BINARY + ) +), +"pre_migration_expected" ("type", "name", "table_name", "sql") AS ( + VALUES ${PROTOCOL_V3_CUTOVER_OBJECTS.map( + (object) => + `(${sqlString(object.type)}, ${sqlString(object.name)}, ${sqlString(object.tableName)}, ${sqlString(object.sql)})`, + ).join(",\n ")} +), +"post_migration_expected" ("type", "name", "table_name", "sql") AS ( + VALUES ${PROTOCOL_V3_POST_MIGRATION_CUTOVER_OBJECTS.map( + (object) => + `(${sqlString(object.type)}, ${sqlString(object.name)}, ${sqlString(object.tableName)}, ${sqlString(object.sql)})`, + ).join(",\n ")} +), +"expected" AS ( + SELECT * FROM "pre_migration_expected" WHERE (SELECT "value" FROM "post_schema") = 0 + UNION ALL + SELECT * FROM "post_migration_expected" WHERE (SELECT "value" FROM "post_schema") = 1 +), +"actual" AS ( + SELECT "type", "name", "tbl_name" AS "table_name", "sql" + FROM "sqlite_master" + WHERE "name" COLLATE NOCASE IN (${CUTOVER_RESERVED_NAMES_SQL}) + OR ( + "type" = 'trigger' + AND "tbl_name" COLLATE NOCASE IN (${CUTOVER_PROTECTED_TABLES_SQL}) + AND NOT ( + ${PROTOCOL_V3_LEGACY_REWRITE_GATE_UPDATE_TRIGGER_MATCH_SQL} + OR ${CUTOVER_MANAGED_TRIGGER_MATCH_SQL} + ) + ) +) +SELECT + count(*) AS "object_count", + CASE WHEN count(*) = (SELECT count(*) FROM "expected") + THEN coalesce(sum(EXISTS ( + SELECT 1 + FROM "expected" + WHERE "expected"."type" = "actual"."type" COLLATE BINARY + AND "expected"."name" = "actual"."name" COLLATE BINARY + AND "expected"."table_name" = "actual"."table_name" COLLATE BINARY + AND "expected"."sql" = "actual"."sql" COLLATE BINARY + )), 0) + ELSE 0 + END AS "exact_object_count" +FROM "actual"; +`; + +export interface ProtocolV3CutoverProbe { + readonly gatePresent: boolean; +} + +export function findPendingProdMigrations( + raw: string, + localMigrationNames: readonly string[], +): string[] { + const validateName = (name: unknown): string => { + if (typeof name !== "string" || !/^\d{4}_[a-z0-9_-]+\.sql$/u.test(name)) { + throw new Error("Production D1 migration name is invalid."); + } + return name; + }; + const local = localMigrationNames.map(validateName); + if (new Set(local).size !== local.length) { + throw new Error("Local D1 migration names must be unique."); + } + + const applied = parseD1JsonResults(raw).flatMap((result) => + result.map((row) => validateName(row.name)), + ); + if (new Set(applied).size !== applied.length) { + throw new Error("Production D1 migration ledger contains duplicate names."); + } + const appliedSet = new Set(applied); + if ( + applied.length > local.length || + local.some((name, index) => appliedSet.has(name) !== index < applied.length) + ) { + throw new Error("Production D1 migration ledger is not an exact prefix of local history."); + } + + return local.filter((name) => !appliedSet.has(name)); +} + +export function assertCutoverMigrationJournalAudited(localMigrationNames: readonly string[]): void { + if ( + localMigrationNames.length !== AUDITED_MIGRATION_NAMES.length || + AUDITED_MIGRATION_NAMES.some((name, index) => localMigrationNames[index] !== name) + ) { + throw new Error( + `Production cutover safety is audited only through ${LAST_CUTOVER_AUDITED_MIGRATION}; review every new migration against the gate and drain before updating that boundary.`, + ); + } +} + +export interface ProtocolV3CutoverObjects { + readonly exactObjectCount: number; + readonly objectCount: number; +} + +export interface ProtocolV3LegacyTerminalIntegrity { + readonly ambiguousAssistantRuns: number; + readonly duplicateTerminalRuns: number; + readonly invalidFailedRuns: number; + readonly invalidNonfailedRunErrors: number; + readonly invalidTerminalLinks: number; + readonly legacyMaterializedMessages: number; + readonly legacyStreamRows: number; + readonly legacyTerminalEvents: number; + readonly mismatchedTerminalEvents: number; + readonly missingTerminalEvents: number; + readonly noncanonicalTerminalSources: number; + readonly nonterminalCommands: number; + readonly partialAssistantProjections: number; + readonly partialTerminalProjections: number; + readonly repairableFailedRuns: number; + readonly rewriteCandidateManifestJson: string; + readonly unsettledEffects: number; +} + +export interface ProtocolV3LegacyRewriteAuthorization { + readonly bookmark: string; + readonly candidateCount: number; + readonly candidateManifestJson: string; + readonly deployOwner: string; + readonly expiresAt: number; + readonly releaseTreeOid: string; +} + +export interface ProtocolV3LegacyTerminalSourceKindInventory { + readonly canonical: number; + readonly canonicalTargetCollisions: number; + readonly noncanonical: number; + readonly total: number; +} + +export interface ProtocolV3LegacyTerminalSourceInventory { + readonly cancelled: ProtocolV3LegacyTerminalSourceKindInventory; + readonly completed: ProtocolV3LegacyTerminalSourceKindInventory; + readonly failed: ProtocolV3LegacyTerminalSourceKindInventory; + readonly invalidTerminalLinks: number; + readonly mismatchedTerminalEvents: number; + readonly multipleTerminalRuns: number; +} + +export type ProtocolV3LossyMigrationCategory = + | "attempt_completion_time_fabrication" + | "command_error_omission" + | "command_payload_conflict" + | "control_reason_omission" + | "input_text_omission" + | "input_start_result_omission" + | "mcp_argument_omission" + | "mcp_command_terminal_conflict" + | "mcp_result_conflict" + | "mcp_result_omission" + | "orphan_effect" + | "provider_receipt_loss" + | "permission_payload_rewrite" + | "session_run_error_omission"; + +export interface ProtocolV3LossyMigrationCandidateId { + readonly category: ProtocolV3LossyMigrationCategory; + readonly id: string; +} + +export interface ProtocolV3LossyMigrationInventory { + readonly attemptCompletionTimeFabrications: number; + readonly candidateIds: readonly ProtocolV3LossyMigrationCandidateId[]; + readonly commandErrorOmissions: number; + readonly commandPayloadConflicts: number; + readonly controlReasonOmissions: number; + readonly inputTextOmissions: number; + readonly inputStartResultOmissions: number; + readonly mcpArgumentOmissions: number; + readonly mcpCommandTerminalConflicts: number; + readonly mcpResultConflicts: number; + readonly mcpResultOmissions: number; + readonly orphanEffects: number; + readonly providerReceiptLosses: number; + readonly permissionPayloadRewrites: number; + readonly sessionRunErrorOmissions: number; + readonly totalCandidates: number; +} + +export interface ProtocolV3CutoverDrain { + readonly activeAppDeploymentRuns: number; + readonly activeRuns: number; + readonly liveDrivers: number; + readonly nonterminalApiCommands: number; + readonly nonterminalCommands: number; + readonly unsafeEnvironmentArtifactBackupStaging: number; + readonly unsafeSandboxBackups: number; + readonly unsafeSandboxBackupStaging: number; + readonly unsafeSandboxes: number; + readonly unsafeSandboxSessions: number; + readonly unsafeSessions: number; + readonly unsettledEffects: number; +} + +export interface ProtocolV3CutoverState { + readonly commandFreeze: boolean; + readonly containerApplicationVersion: number | null; + readonly containerImageDigest: string | null; + readonly enabled: boolean; + readonly migrationStarted: boolean; + readonly phase: ProtocolV3CutoverPhase; + readonly releaseTreeOid: string; + readonly workerVersionId: string | null; +} + +export interface ProtocolV3RuntimeAuthorityPreflight { + readonly duplicateSandboxBackups: number; + readonly foreignKeyViolations: number; + readonly invalidDriverGenerations: number; + readonly invalidSandboxBackupPointers: number; + readonly invalidSandboxBackups: number; + readonly invalidSandboxIdentities: number; + readonly invalidSandboxSessionAuthorities: number; + readonly legacyAppDeploymentTraffic: number; + readonly nonstaticSessions: number; +} + +export interface ProtocolV3WorkerDeployment { + readonly versionId: string; +} + +export interface ProtocolV3SmokeStatus { + readonly bootTokenUsedAt: number | null; + readonly connectionId: string | null; + readonly driverPid: number | null; + readonly driverStartedAt: number | null; + readonly driverStatus: string | null; + readonly driverVersion: string | null; + readonly protocolVersion: number | null; + readonly statusEvent: string | null; +} + +export interface ProtocolV3ContainerInstance { + readonly state: string; + readonly version: string | number | null; +} + +export interface ProtocolV3ContainerApplication { + readonly id: string; + readonly imageDigest: string; + readonly imageRepository: string; + readonly name: string; + readonly state: "active" | "degraded" | "provisioning" | "ready"; + readonly version: number; +} + +interface ProtocolV3ContainerPage { + readonly items: readonly T[]; + readonly nextPageToken: string | null; + readonly pageToken: string | null; +} + +export interface ProtocolV3CutoverRecoveryState { + readonly bookmark: string | null; + readonly initialPendingMigrations: readonly string[]; + readonly migrationStarted: boolean; + readonly originalError: unknown; + readonly queuesVerified: boolean; +} + +export interface ProtocolV3CutoverRecoveryEffects { + readonly commitQueueAcceptance: () => void; + readonly pauseAndVerifyQueues: () => Promise; + readonly printBookmark: (bookmark: string) => void; + readonly probe: () => ProtocolV3CutoverProbe; + readonly readBookmark: () => string | null; + readonly readPendingMigrations: () => readonly string[]; + readonly removeMarker: () => void; + readonly resumeAndVerifyQueues: () => Promise; + readonly write: (message: string) => void; +} + +export interface ProtocolV3QueueResumeEffects { + readonly commitAcceptance: () => void; + readonly removeMarker: () => void; + readonly resumeAndVerifyQueues: () => Promise; +} + +type JsonRow = D1JsonRow; +const MAX_PROTOCOL_V3_CONTAINER_PAGES = 100; + +function requireNonNegativeInteger(row: JsonRow, key: string): number { + const value = row[key]; + + if (!Number.isSafeInteger(value) || (value as number) < 0) { + throw new Error(`D1 JSON field ${key} must be a non-negative safe integer.`); + } + + return value as number; +} + +function requireNullableNonNegativeInteger(row: JsonRow, key: string): number | null { + return row[key] === null ? null : requireNonNegativeInteger(row, key); +} + +function requireNullableString(row: JsonRow, key: string): string | null { + const value = row[key]; + if (value !== null && typeof value !== "string") { + throw new Error(`D1 JSON field ${key} must be a string or null.`); + } + return value; +} + +function requireJsonArrayString(row: JsonRow, key: string): string { + const value = row[key]; + if (typeof value !== "string") throw new Error(`D1 JSON field ${key} must be a string.`); + let parsed: unknown; + try { + parsed = JSON.parse(value); + } catch { + throw new Error(`D1 JSON field ${key} must contain valid JSON.`); + } + if (!Array.isArray(parsed)) throw new Error(`D1 JSON field ${key} must contain an array.`); + return value; +} + +function requirePageToken(value: unknown, label: string): string | null { + if (value === null) return null; + if ( + typeof value !== "string" || + value.length === 0 || + value.length > 4_096 || + [...value].some((character) => { + const codePoint = character.codePointAt(0) ?? 0; + return codePoint < 32 || codePoint === 127; + }) + ) { + throw new Error(`${label} must be a valid opaque token or null.`); + } + return value; +} + +function requirePlatformId(value: string, label: string): string { + if (!/^[0-7][0-9A-HJKMNP-TV-Z]{25}$/iu.test(value)) { + throw new Error(`${label} must be a ULID.`); + } + return value; +} + +function requireGitTreeOid(value: string, label: string): string { + if (!GIT_TREE_OID_PATTERN.test(value)) throw new Error(`${label} must be a Git tree OID.`); + return value; +} + +function requireSha256Digest(value: string, label: string): string { + if (!SHA256_DIGEST_PATTERN.test(value)) throw new Error(`${label} must be a SHA-256 digest.`); + return value; +} + +function requireWorkerVersionId(value: string): string { + if (!WORKER_VERSION_ID_PATTERN.test(value)) { + throw new Error("Protocol v3 Worker version ID must be a UUID."); + } + return value; +} + +export function parseCleanGitTreeOid( + treeOutput: string, + statusOutput: string, + indexOutput = "", + ignoredBuildInputOutput = "", + environmentKeys: readonly string[] = [], +): string { + if (statusOutput.trim().length > 0 || /^(?:S|[a-z]) /mu.test(indexOutput)) { + throw new Error("Production deploy requires a clean Git worktree and clean submodules."); + } + if ( + ignoredBuildInputOutput.trim().length > 0 || + environmentKeys.some((key) => key.startsWith("VITE_")) + ) { + throw new Error("Production deploy rejects local or process-level Vite build inputs."); + } + return requireGitTreeOid(treeOutput.trim(), "Production release tree OID"); +} + +export function protocolV3ReleaseTag(releaseTreeOid: string): string { + return `protocol-v3-${requireGitTreeOid(releaseTreeOid, "Protocol v3 release tree OID")}`; +} + +export function assertProtocolV3Release( + state: ProtocolV3CutoverState, + releaseTreeOid: string, +): void { + if (state.releaseTreeOid !== requireGitTreeOid(releaseTreeOid, "Protocol v3 release tree OID")) { + throw new Error( + `Protocol v3 cutover belongs to release tree ${state.releaseTreeOid}, not ${releaseTreeOid}.`, + ); + } +} + +export function parseProtocolV3WorkerDeployment(raw: string): ProtocolV3WorkerDeployment { + const deployment = JSON.parse(raw) as { versions?: unknown }; + if (!Array.isArray(deployment.versions) || deployment.versions.length !== 1) { + throw new Error("Protocol v3 requires exactly one deployed Worker version."); + } + const [version] = deployment.versions as Array>; + if (version?.percentage !== 100 || typeof version.version_id !== "string") { + throw new Error("Protocol v3 Worker release must receive 100% of production traffic."); + } + return { versionId: requireWorkerVersionId(version.version_id) }; +} + +export function assertProtocolV3WorkerVersion( + raw: string, + expectedVersionId: string, + releaseTreeOid: string, +): void { + const version = JSON.parse(raw) as { + annotations?: Record; + id?: unknown; + }; + const versionId = requireWorkerVersionId(expectedVersionId); + if ( + version.id !== versionId || + version.annotations?.["workers/tag"] !== protocolV3ReleaseTag(releaseTreeOid) + ) { + throw new Error("Production Worker version is not the exact protocol v3 release."); + } +} + +export function protocolV3ContainerImageTag( + imageRepository: string, + workerVersionId: string, +): string { + if (imageRepository.trim() !== imageRepository || !imageRepository.includes("/")) { + throw new Error("Protocol v3 Container image repository is invalid."); + } + return `${imageRepository}:${requireWorkerVersionId(workerVersionId).slice(0, 8)}`; +} + +export function parseProtocolV3ContainerManifestDigest(raw: string): string { + const manifest = JSON.parse(raw) as { Descriptor?: { digest?: unknown } }; + const digest = manifest.Descriptor?.digest; + if (typeof digest !== "string" || !digest.startsWith("sha256:")) { + throw new Error("Protocol v3 Container manifest is missing its SHA-256 digest."); + } + return requireSha256Digest(digest.slice("sha256:".length), "Protocol v3 Container manifest"); +} + +function requireSmokeRequestKey(value: string): string { + if ( + !/^protocol-v3-cutover-[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/u.test( + value, + ) + ) { + throw new Error("Protocol v3 smoke request key must contain a UUID v4."); + } + return value; +} + +export function parseProtocolV3CutoverProbe(raw: string): ProtocolV3CutoverProbe { + const row = requireSingleD1Row(raw); + const gatePresent = requireNonNegativeInteger(row, "gate_present"); + if (gatePresent > 1) throw new Error("D1 cutover probe field must be zero or one."); + return { gatePresent: gatePresent === 1 }; +} + +export function parseProtocolV3LegacyTerminalIntegrity( + raw: string, +): ProtocolV3LegacyTerminalIntegrity { + const row = requireSingleD1Row(raw); + const noncanonicalTerminalSources = requireNonNegativeInteger( + row, + "noncanonical_terminal_sources", + ); + const rewriteCandidateManifestJson = requireJsonArrayString( + row, + "rewrite_candidate_manifest_json", + ); + if ( + (JSON.parse(rewriteCandidateManifestJson) as unknown[]).length !== noncanonicalTerminalSources + ) { + throw new Error("Legacy rewrite candidate count and manifest disagree."); + } + + return { + ambiguousAssistantRuns: requireNonNegativeInteger(row, "ambiguous_assistant_runs"), + duplicateTerminalRuns: requireNonNegativeInteger(row, "duplicate_terminal_runs"), + invalidFailedRuns: requireNonNegativeInteger(row, "invalid_failed_runs"), + invalidNonfailedRunErrors: requireNonNegativeInteger(row, "invalid_nonfailed_run_errors"), + invalidTerminalLinks: requireNonNegativeInteger(row, "invalid_terminal_links"), + legacyMaterializedMessages: requireNonNegativeInteger(row, "legacy_materialized_messages"), + legacyStreamRows: requireNonNegativeInteger(row, "legacy_stream_rows"), + legacyTerminalEvents: requireNonNegativeInteger(row, "legacy_terminal_events"), + mismatchedTerminalEvents: requireNonNegativeInteger(row, "mismatched_terminal_events"), + missingTerminalEvents: requireNonNegativeInteger(row, "missing_terminal_events"), + noncanonicalTerminalSources, + nonterminalCommands: requireNonNegativeInteger(row, "nonterminal_commands"), + partialAssistantProjections: requireNonNegativeInteger(row, "partial_assistant_projections"), + partialTerminalProjections: requireNonNegativeInteger(row, "partial_terminal_projections"), + repairableFailedRuns: requireNonNegativeInteger(row, "repairable_failed_runs"), + rewriteCandidateManifestJson, + unsettledEffects: requireNonNegativeInteger(row, "unsettled_effects"), + }; +} + +export function parseProtocolV3LegacyRewriteAuthorization( + raw: string, +): ProtocolV3LegacyRewriteAuthorization { + const row = requireSingleD1Row(raw); + if (row.authorization_table_sql !== PROTOCOL_V3_LEGACY_REWRITE_AUTHORIZATION_TABLE_SQL) { + throw new Error("Protocol v3 legacy rewrite authorization table schema is invalid."); + } + if (row.gate_update_trigger_sql !== PROTOCOL_V3_LEGACY_REWRITE_GATE_UPDATE_TRIGGER_SQL) { + throw new Error("Protocol v3 legacy rewrite gate update guard is invalid."); + } + const candidateCount = requireNonNegativeInteger(row, "candidate_count"); + const candidateManifestJson = requireJsonArrayString(row, "candidate_manifest_json"); + if ((JSON.parse(candidateManifestJson) as unknown[]).length !== candidateCount) { + throw new Error("Legacy rewrite authorization count and manifest disagree."); + } + const bookmark = row.bookmark; + if (typeof bookmark !== "string" || !/^[0-9a-z-]{1,256}$/iu.test(bookmark)) { + throw new Error("Protocol v3 legacy rewrite authorization has an invalid bookmark."); + } + const deployOwner = row.deploy_owner; + if ( + typeof deployOwner !== "string" || + !/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/u.test(deployOwner) + ) { + throw new Error("Protocol v3 legacy rewrite authorization has an invalid owner."); + } + return { + bookmark, + candidateCount, + candidateManifestJson, + deployOwner, + expiresAt: requireNonNegativeInteger(row, "expires_at"), + releaseTreeOid: requireGitTreeOid( + typeof row.release_tree_oid === "string" ? row.release_tree_oid : "", + "Protocol v3 legacy rewrite release tree OID", + ), + }; +} + +export function parseProtocolV3LegacyTerminalSourceInventory( + raw: string, +): ProtocolV3LegacyTerminalSourceInventory { + const row = requireSingleD1Row(raw); + const readKind = (kind: "cancelled" | "completed" | "failed") => { + const inventory = { + canonical: requireNonNegativeInteger(row, `${kind}_canonical`), + canonicalTargetCollisions: requireNonNegativeInteger( + row, + `${kind}_canonical_target_collisions`, + ), + noncanonical: requireNonNegativeInteger(row, `${kind}_noncanonical`), + total: requireNonNegativeInteger(row, `${kind}_total`), + }; + if ( + inventory.canonical + inventory.noncanonical !== inventory.total || + inventory.canonicalTargetCollisions > inventory.total + ) { + throw new Error(`Legacy ${kind} terminal source inventory is inconsistent.`); + } + return inventory; + }; + + return { + cancelled: readKind("cancelled"), + completed: readKind("completed"), + failed: readKind("failed"), + invalidTerminalLinks: requireNonNegativeInteger(row, "invalid_terminal_links"), + mismatchedTerminalEvents: requireNonNegativeInteger(row, "mismatched_terminal_events"), + multipleTerminalRuns: requireNonNegativeInteger(row, "multiple_terminal_runs"), + }; +} + +const PROTOCOL_V3_LOSSY_MIGRATION_CATEGORIES = new Set([ + "attempt_completion_time_fabrication", + "command_error_omission", + "command_payload_conflict", + "control_reason_omission", + "input_text_omission", + "input_start_result_omission", + "mcp_argument_omission", + "mcp_command_terminal_conflict", + "mcp_result_conflict", + "mcp_result_omission", + "orphan_effect", + "provider_receipt_loss", + "permission_payload_rewrite", + "session_run_error_omission", +]); + +export function parseProtocolV3LossyMigrationInventory( + raw: string, +): ProtocolV3LossyMigrationInventory { + const row = requireSingleD1Row(raw); + const inventory = { + attemptCompletionTimeFabrications: requireNonNegativeInteger( + row, + "attempt_completion_time_fabrications", + ), + commandErrorOmissions: requireNonNegativeInteger(row, "command_error_omissions"), + commandPayloadConflicts: requireNonNegativeInteger(row, "command_payload_conflicts"), + controlReasonOmissions: requireNonNegativeInteger(row, "control_reason_omissions"), + inputTextOmissions: requireNonNegativeInteger(row, "input_text_omissions"), + inputStartResultOmissions: requireNonNegativeInteger(row, "input_start_result_omissions"), + mcpArgumentOmissions: requireNonNegativeInteger(row, "mcp_argument_omissions"), + mcpCommandTerminalConflicts: requireNonNegativeInteger(row, "mcp_command_terminal_conflicts"), + mcpResultConflicts: requireNonNegativeInteger(row, "mcp_result_conflicts"), + mcpResultOmissions: requireNonNegativeInteger(row, "mcp_result_omissions"), + orphanEffects: requireNonNegativeInteger(row, "orphan_effects"), + providerReceiptLosses: requireNonNegativeInteger(row, "provider_receipt_losses"), + permissionPayloadRewrites: requireNonNegativeInteger(row, "permission_payload_rewrites"), + sessionRunErrorOmissions: requireNonNegativeInteger(row, "session_run_error_omissions"), + totalCandidates: requireNonNegativeInteger(row, "total_candidates"), + }; + const countedCandidates = + inventory.attemptCompletionTimeFabrications + + inventory.commandErrorOmissions + + inventory.commandPayloadConflicts + + inventory.controlReasonOmissions + + inventory.inputTextOmissions + + inventory.inputStartResultOmissions + + inventory.mcpArgumentOmissions + + inventory.mcpCommandTerminalConflicts + + inventory.mcpResultConflicts + + inventory.mcpResultOmissions + + inventory.orphanEffects + + inventory.providerReceiptLosses + + inventory.permissionPayloadRewrites + + inventory.sessionRunErrorOmissions; + if (countedCandidates !== inventory.totalCandidates) { + throw new Error("Lossy migration inventory category counts are inconsistent."); + } + + const parsedIds = JSON.parse(requireJsonArrayString(row, "candidate_ids_json")) as unknown[]; + if (parsedIds.length !== Math.min(inventory.totalCandidates, 50)) { + throw new Error("Lossy migration inventory candidate ID count is inconsistent."); + } + let previousKey: string | null = null; + const candidateIds = parsedIds.map((entry): ProtocolV3LossyMigrationCandidateId => { + if (!Array.isArray(entry) || entry.length !== 2) { + throw new Error("Lossy migration inventory candidate IDs are invalid."); + } + const [category, id] = entry; + if ( + typeof category !== "string" || + !PROTOCOL_V3_LOSSY_MIGRATION_CATEGORIES.has(category as ProtocolV3LossyMigrationCategory) || + typeof id !== "string" + ) { + throw new Error("Lossy migration inventory candidate IDs are invalid."); + } + const key = `${category}\0${requirePlatformId(id, "Lossy migration candidate ID")}`; + if (previousKey !== null && previousKey >= key) { + throw new Error("Lossy migration inventory candidate IDs are not stable and unique."); + } + previousKey = key; + return { category: category as ProtocolV3LossyMigrationCategory, id }; + }); + + return { ...inventory, candidateIds }; +} + +export function assertProtocolV3LossyMigrationInventory( + inventory: ProtocolV3LossyMigrationInventory, +): void { + if (inventory.totalCandidates === 0) return; + throw new Error( + `Migration 0013 would discard or overwrite production history: ${[ + ["attemptCompletionTimeFabrications", inventory.attemptCompletionTimeFabrications], + ["mcpArgumentOmissions", inventory.mcpArgumentOmissions], + ["commandPayloadConflicts", inventory.commandPayloadConflicts], + ["inputTextOmissions", inventory.inputTextOmissions], + ["inputStartResultOmissions", inventory.inputStartResultOmissions], + ["controlReasonOmissions", inventory.controlReasonOmissions], + ["mcpResultOmissions", inventory.mcpResultOmissions], + ["orphanEffects", inventory.orphanEffects], + ["mcpResultConflicts", inventory.mcpResultConflicts], + ["providerReceiptLosses", inventory.providerReceiptLosses], + ["permissionPayloadRewrites", inventory.permissionPayloadRewrites], + ["mcpCommandTerminalConflicts", inventory.mcpCommandTerminalConflicts], + ["commandErrorOmissions", inventory.commandErrorOmissions], + ["sessionRunErrorOmissions", inventory.sessionRunErrorOmissions], + ] + .filter(([, count]) => count !== 0) + .map(([category, count]) => `${category}=${count}`) + .join(" ")}. No lossy migration candidates are authorized.`, + ); +} + +export function assertProtocolV3LegacyTerminalSourceInventory( + inventory: ProtocolV3LegacyTerminalSourceInventory, +): void { + if ( + inventory.invalidTerminalLinks > 0 || + inventory.mismatchedTerminalEvents > 0 || + inventory.multipleTerminalRuns > 0 || + [inventory.cancelled, inventory.completed, inventory.failed].some( + (entry) => entry.canonicalTargetCollisions > 0, + ) + ) { + throw new Error( + "Legacy terminal source inventory is not ready for the protocol v3 production cutover.", + ); + } +} + +export function assertProtocolV3LegacyTerminalIntegrity( + integrity: ProtocolV3LegacyTerminalIntegrity, +): void { + const blockers = { + ambiguousAssistantRuns: integrity.ambiguousAssistantRuns, + duplicateTerminalRuns: integrity.duplicateTerminalRuns, + invalidFailedRuns: integrity.invalidFailedRuns, + invalidNonfailedRunErrors: integrity.invalidNonfailedRunErrors, + invalidTerminalLinks: integrity.invalidTerminalLinks, + mismatchedTerminalEvents: integrity.mismatchedTerminalEvents, + missingTerminalEvents: integrity.missingTerminalEvents, + nonterminalCommands: integrity.nonterminalCommands, + partialAssistantProjections: integrity.partialAssistantProjections, + partialTerminalProjections: integrity.partialTerminalProjections, + unsettledEffects: integrity.unsettledEffects, + }; + + if (Object.values(blockers).some((count) => count > 0)) { + throw new Error( + `Legacy terminal integrity is ambiguous and cannot be migrated: ${Object.entries(blockers) + .map(([name, count]) => `${name}=${count}`) + .join(" ")}.`, + ); + } +} + +function parseProtocolV3ContainerApplicationPage( + value: unknown, +): ProtocolV3ContainerPage { + if (typeof value !== "object" || value === null || Array.isArray(value)) { + throw new Error("Container applications API output must be an object."); + } + + const applications = "result" in value ? value.result : null; + const resultInfo = "result_info" in value ? value.result_info : null; + if ( + !("success" in value) || + value.success !== true || + !Array.isArray(applications) || + typeof resultInfo !== "object" || + resultInfo === null + ) { + throw new Error("Container applications API output is missing pagination fields."); + } + + return { + items: applications.map((application) => { + if ( + typeof application !== "object" || + application === null || + !("id" in application) || + typeof application.id !== "string" || + !/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/iu.test(application.id) || + !("name" in application) || + typeof application.name !== "string" || + application.name.length === 0 || + !("version" in application) || + !("health" in application) || + typeof application.health !== "object" || + application.health === null || + !("instances" in application.health) || + typeof application.health.instances !== "object" || + application.health.instances === null + ) { + throw new Error("Container application API output is invalid."); + } + + const configuration = "configuration" in application ? application.configuration : null; + const image = + typeof configuration === "object" && + configuration !== null && + "image" in configuration && + typeof configuration.image === "string" + ? configuration.image + : ""; + const imageMatch = /^(.+)@sha256:([0-9a-f]{64})$/u.exec(image); + if (imageMatch === null || imageMatch[1]?.trim() !== imageMatch[1]) { + throw new Error("Container application image is not content-addressed."); + } + + const health = application.health.instances as JsonRow; + const active = requireNonNegativeInteger(health, "active"); + const failed = requireNonNegativeInteger(health, "failed"); + requireNonNegativeInteger(health, "healthy"); + const scheduling = requireNonNegativeInteger(health, "scheduling"); + const starting = requireNonNegativeInteger(health, "starting"); + const version = requireNonNegativeInteger(application as JsonRow, "version"); + const state = + failed > 0 + ? "degraded" + : starting > 0 || scheduling > 0 + ? "provisioning" + : active > 0 + ? "active" + : "ready"; + + return { + id: application.id, + imageDigest: requireSha256Digest(imageMatch[2] ?? "", "Container application image"), + imageRepository: imageMatch[1] ?? "", + name: application.name, + state, + version, + }; + }), + nextPageToken: requirePageToken( + "next_page_token" in resultInfo ? resultInfo.next_page_token : undefined, + "Container application next page token", + ), + pageToken: requirePageToken( + "page_token" in resultInfo ? resultInfo.page_token : undefined, + "Container application page token", + ), + }; +} + +function parseProtocolV3ContainerInstancePage( + raw: string, +): ProtocolV3ContainerPage { + const value = JSON.parse(raw) as unknown; + if (typeof value !== "object" || value === null || Array.isArray(value)) { + throw new Error("Container instances JSON output must be a paginated object."); + } + + const instances = "instances" in value ? value.instances : null; + const resultInfo = "result_info" in value ? value.result_info : null; + if (!Array.isArray(instances) || typeof resultInfo !== "object" || resultInfo === null) { + throw new Error("Container instances JSON output is missing pagination fields."); + } + + return { + items: instances.map((instance) => { + if ( + typeof instance !== "object" || + instance === null || + !("state" in instance) || + typeof instance.state !== "string" || + !("version" in instance) || + (instance.version !== null && + typeof instance.version !== "string" && + typeof instance.version !== "number") + ) { + throw new Error("Container instance JSON output is invalid."); + } + return { state: instance.state, version: instance.version }; + }), + nextPageToken: requirePageToken( + "next_page_token" in resultInfo ? resultInfo.next_page_token : undefined, + "Container next page token", + ), + pageToken: requirePageToken( + "page_token" in resultInfo ? resultInfo.page_token : undefined, + "Container page token", + ), + }; +} + +async function collectPages( + readPage: (pageToken: string | null) => Promise | Raw, + parsePage: (raw: Raw) => ProtocolV3ContainerPage, + label: string, +): Promise { + const items: T[] = []; + const seenPageTokens = new Set(); + let pageToken: string | null = null; + let pageCount = 0; + + while (true) { + pageCount += 1; + if (pageCount > MAX_PROTOCOL_V3_CONTAINER_PAGES) { + throw new Error(`${label} pagination exceeded its safety limit.`); + } + if (pageToken !== null && seenPageTokens.has(pageToken)) { + throw new Error(`${label} pagination returned a repeated page token.`); + } + if (pageToken !== null) seenPageTokens.add(pageToken); + + const page = parsePage(await readPage(pageToken)); + if (page.pageToken !== pageToken) { + throw new Error(`${label} pagination returned the wrong page token.`); + } + items.push(...page.items); + if (page.nextPageToken === null) return items; + pageToken = page.nextPageToken; + } +} + +export function collectProtocolV3ContainerInstances( + readPage: (pageToken: string | null) => Promise | string, +): Promise { + return collectPages(readPage, parseProtocolV3ContainerInstancePage, "Container instance"); +} + +export function collectProtocolV3ContainerApplications( + readPage: (pageToken: string | null) => Promise | unknown, +): Promise { + return collectPages(readPage, parseProtocolV3ContainerApplicationPage, "Container application"); +} + +function errorMessage(error: unknown): string { + return error instanceof Error ? error.message : String(error); +} + +export async function recoverProtocolV3CutoverFailure( + state: ProtocolV3CutoverRecoveryState, + effects: ProtocolV3CutoverRecoveryEffects, +): Promise { + if (state.queuesVerified) { + try { + await effects.resumeAndVerifyQueues(); + if (!effects.probe().gatePresent) { + effects.write(" protocol v3 queue and marker recovery was already complete"); + return; + } + effects.commitQueueAcceptance(); + effects.removeMarker(); + effects.write(" protocol v3 queue and marker recovery completed after acceptance"); + return; + } catch (recoveryError) { + effects.write( + `✗ Protocol v3 was accepted, but final queue/marker confirmation failed: ${errorMessage(recoveryError)}`, + ); + effects.write( + " Queue delivery was already verified open and will not be re-paused after this commit point.", + ); + effects.write( + " Rerun this exact v3 release to repeat queue verification and marker cleanup.", + ); + throw new AggregateError( + [state.originalError, recoveryError], + "Protocol v3 post-acceptance cleanup remains incomplete.", + { cause: recoveryError }, + ); + } + } + + let migrationMayHaveCommitted = + state.initialPendingMigrations.length === 0 || state.migrationStarted; + if (!migrationMayHaveCommitted) { + try { + const stillPending = new Set(effects.readPendingMigrations()); + migrationMayHaveCommitted = state.initialPendingMigrations.some( + (name) => !stillPending.has(name), + ); + } catch (migrationProbeError) { + migrationMayHaveCommitted = true; + effects.write( + `✗ Could not prove every initially pending migration remained unapplied; keeping production closed: ${errorMessage(migrationProbeError)}`, + ); + } + } + + const reportFailures = (label: string, failures: readonly string[]) => { + if (failures.length === 0) return; + effects.write(`✗ ${label}:`); + for (const failure of failures) effects.write(` ${failure}`); + }; + + if (!migrationMayHaveCommitted) { + const failures: string[] = []; + try { + await effects.resumeAndVerifyQueues(); + effects.removeMarker(); + } catch (recoveryError) { + failures.push(`queue or gate recovery: ${errorMessage(recoveryError)}`); + try { + await effects.pauseAndVerifyQueues(); + } catch (pauseError) { + failures.push(`queue re-pause verification: ${errorMessage(pauseError)}`); + } + try { + if (!effects.probe().gatePresent) { + try { + await effects.resumeAndVerifyQueues(); + failures.length = 0; + } catch (resumeError) { + failures.push(`old service queue resume verification: ${errorMessage(resumeError)}`); + try { + await effects.pauseAndVerifyQueues(); + } catch (pauseError) { + failures.push(`final queue re-pause verification: ${errorMessage(pauseError)}`); + } + } + } + } catch (probeError) { + failures.push(`gate cleanup readback: ${errorMessage(probeError)}`); + } + } + reportFailures("Old service recovery was incomplete", failures); + if (failures.length === 0) { + effects.write(" no migration committed; the old service admission and queues were restored"); + } + } else { + try { + await effects.pauseAndVerifyQueues(); + } catch (pauseError) { + reportFailures("Failed to re-pause and verify every queue", [errorMessage(pauseError)]); + } + let bookmark = state.bookmark; + if (bookmark === null) { + try { + bookmark = effects.readBookmark(); + } catch { + // The bookmark is an emergency destructive backup, not a roll-forward prerequisite. + } + } + effects.write( + "✗ A migration request may have committed; queue delivery remains paused. Do not roll back only the Worker.", + ); + effects.write(" Repair only by rolling forward this exact v3 release."); + if (bookmark !== null) effects.printBookmark(bookmark); + } + + throw state.originalError; +} + +export async function completeProtocolV3QueueResume( + state: ProtocolV3CutoverState, + effects: ProtocolV3QueueResumeEffects, +): Promise { + if (state.phase !== "queues_resuming" || !state.commandFreeze) { + throw new Error("Protocol v3 queue resume requires its durable frozen phase."); + } + await effects.resumeAndVerifyQueues(); + if (state.enabled) effects.commitAcceptance(); + effects.removeMarker(); +} + +export function isProtocolV3ContainerRolloutConverged( + application: { readonly state: string; readonly version: string | number }, + instances: readonly ProtocolV3ContainerInstance[], +): boolean { + if (application.state !== "active" && application.state !== "ready") return false; + + return instances.every( + (instance) => + instance.state === "inactive" || + (instance.state === "running" && String(instance.version) === String(application.version)), + ); +} + +export function parseProtocolV3CutoverDrain(raw: string): ProtocolV3CutoverDrain { + const row = requireSingleD1Row(raw); + + return { + activeAppDeploymentRuns: requireNonNegativeInteger(row, "active_app_deployment_runs"), + activeRuns: requireNonNegativeInteger(row, "active_runs"), + liveDrivers: requireNonNegativeInteger(row, "live_drivers"), + nonterminalApiCommands: requireNonNegativeInteger(row, "nonterminal_api_commands"), + nonterminalCommands: requireNonNegativeInteger(row, "nonterminal_commands"), + unsafeEnvironmentArtifactBackupStaging: requireNonNegativeInteger( + row, + "unsafe_environment_artifact_backup_staging", + ), + unsafeSandboxBackups: requireNonNegativeInteger(row, "unsafe_sandbox_backups"), + unsafeSandboxBackupStaging: requireNonNegativeInteger(row, "unsafe_sandbox_backup_staging"), + unsafeSandboxes: requireNonNegativeInteger(row, "unsafe_sandboxes"), + unsafeSandboxSessions: requireNonNegativeInteger(row, "unsafe_sandbox_sessions"), + unsafeSessions: requireNonNegativeInteger(row, "unsafe_sessions"), + unsettledEffects: requireNonNegativeInteger(row, "unsettled_effects"), + }; +} + +export function parseProtocolV3RuntimeAuthorityPreflight( + raw: string, +): ProtocolV3RuntimeAuthorityPreflight { + const row = requireSingleD1Row(raw); + return { + duplicateSandboxBackups: requireNonNegativeInteger(row, "duplicate_sandbox_backups"), + foreignKeyViolations: requireNonNegativeInteger(row, "foreign_key_violations"), + invalidDriverGenerations: requireNonNegativeInteger(row, "invalid_driver_generations"), + invalidSandboxBackupPointers: requireNonNegativeInteger(row, "invalid_sandbox_backup_pointers"), + invalidSandboxBackups: requireNonNegativeInteger(row, "invalid_sandbox_backups"), + invalidSandboxIdentities: requireNonNegativeInteger(row, "invalid_sandbox_identities"), + invalidSandboxSessionAuthorities: requireNonNegativeInteger( + row, + "invalid_sandbox_session_authorities", + ), + legacyAppDeploymentTraffic: requireNonNegativeInteger(row, "legacy_app_deployment_traffic"), + nonstaticSessions: requireNonNegativeInteger(row, "nonstatic_sessions"), + }; +} + +export function assertProtocolV3RuntimeAuthorityPreflight( + state: ProtocolV3RuntimeAuthorityPreflight, +): void { + const violations = Object.entries(state).filter(([, count]) => count !== 0); + if (violations.length === 0) return; + throw new Error( + `Runtime authority migration preflight failed: ${violations + .map(([name, count]) => `${name}=${count}`) + .join( + ", ", + )}. Migration 0019 remains the atomic authority and will not apply until these rows are repaired through supported lifecycle paths.`, + ); +} + +export function isProtocolV3CutoverDrained(state: ProtocolV3CutoverDrain): boolean { + return isProtocolV3RuntimeDrained(state) && state.nonterminalCommands === 0; +} + +export function isProtocolV3RuntimeDrained(state: ProtocolV3CutoverDrain): boolean { + return ( + state.activeAppDeploymentRuns === 0 && + state.activeRuns === 0 && + state.liveDrivers === 0 && + state.nonterminalApiCommands === 0 && + state.unsafeEnvironmentArtifactBackupStaging === 0 && + state.unsafeSandboxBackups === 0 && + state.unsafeSandboxBackupStaging === 0 && + state.unsafeSandboxes === 0 && + state.unsafeSandboxSessions === 0 && + state.unsafeSessions === 0 && + state.unsettledEffects === 0 + ); +} + +export function parseTimeTravelBookmark(raw: string): string { + const parsed = JSON.parse(raw) as { bookmark?: unknown } | null; + const bookmark = parsed?.bookmark; + + if (typeof bookmark !== "string" || !/^[0-9a-z-]{1,256}$/iu.test(bookmark)) { + throw new Error("D1 Time Travel returned an invalid bookmark."); + } + + return bookmark; +} + +export function parseStoredProtocolV3CutoverBookmark(raw: string): string | null { + const bookmark = requireSingleD1Row(raw).bookmark; + + if (bookmark === null) { + return null; + } + if (typeof bookmark !== "string" || !/^[0-9a-z-]{1,256}$/iu.test(bookmark)) { + throw new Error("The stored D1 Time Travel bookmark is invalid."); + } + + return bookmark; +} + +export function parseProtocolV3CutoverObjects(raw: string): ProtocolV3CutoverObjects { + const row = requireSingleD1Row(raw); + const objectCount = requireNonNegativeInteger(row, "object_count"); + const exactObjectCount = requireNonNegativeInteger(row, "exact_object_count"); + if (exactObjectCount > objectCount) { + throw new Error("D1 cutover exact object count exceeds its reserved object count."); + } + return { exactObjectCount, objectCount }; +} + +export function parseProtocolV3CutoverState(raw: string): ProtocolV3CutoverState { + const row = requireSingleD1Row(raw); + const enabled = requireNonNegativeInteger(row, "enabled"); + const freeze = requireNonNegativeInteger(row, "command_freeze"); + const containerApplicationVersion = requireNullableNonNegativeInteger( + row, + "target_container_application_version", + ); + const containerImage = requireNullableString(row, "target_container_image_digest"); + const workerVersion = requireNullableString(row, "target_worker_version_id"); + const migrationStarted = requireNonNegativeInteger(row, "migration_started"); + const migrationIntentTriggerCount = requireNonNegativeInteger( + row, + "migration_intent_trigger_count", + ); + const phase = row.phase; + if (enabled > 1) throw new Error("D1 cutover gate enabled field must be zero or one."); + if (freeze > 1) throw new Error("D1 command freeze field must be zero or one."); + if (migrationStarted > 1) { + throw new Error("D1 migration intent field must be zero or one."); + } + if ( + row.migration_intent_table_sql !== PROTOCOL_V3_MIGRATION_INTENT_TABLE_SQL || + migrationIntentTriggerCount !== 0 + ) { + throw new Error("D1 migration intent authority is invalid."); + } + if (phase !== "draining" && phase !== "queues_resuming") { + throw new Error("D1 cutover phase is invalid."); + } + if ( + (phase === "draining" && enabled !== 1) || + (phase === "queues_resuming" && freeze !== 1) || + new Set([containerApplicationVersion === null, containerImage === null, workerVersion === null]) + .size !== 1 + ) { + throw new Error("D1 cutover phase state is inconsistent."); + } + + return { + commandFreeze: freeze === 1, + containerApplicationVersion, + containerImageDigest: + containerImage === null + ? null + : requireSha256Digest(containerImage, "Stored protocol v3 Container image"), + enabled: enabled === 1, + migrationStarted: migrationStarted === 1, + phase, + releaseTreeOid: requireGitTreeOid( + typeof row.release_tree_oid === "string" ? row.release_tree_oid : "", + "Stored protocol v3 release tree OID", + ), + workerVersionId: workerVersion === null ? null : requireWorkerVersionId(workerVersion), + }; +} + +export function parseProtocolV3CommandFreeze(raw: string): boolean { + return parseProtocolV3CutoverState(raw).commandFreeze; +} + +export function parseProtocolV3SmokeStatus(raw: string): ProtocolV3SmokeStatus { + const row = requireSingleD1Row(raw); + + return { + bootTokenUsedAt: requireNullableNonNegativeInteger(row, "boot_token_used_at"), + connectionId: requireNullableString(row, "connection_id"), + driverPid: requireNullableNonNegativeInteger(row, "driver_pid"), + driverStartedAt: requireNullableNonNegativeInteger(row, "driver_started_at"), + driverStatus: requireNullableString(row, "driver_status"), + driverVersion: requireNullableString(row, "driver_version"), + protocolVersion: requireNullableNonNegativeInteger(row, "protocol_version"), + statusEvent: requireNullableString(row, "status_event"), + }; +} + +export function parseStoredProtocolV3SmokeSession(raw: string): string | null { + const sessionId = requireSingleD1Row(raw).smoke_session_id; + if (sessionId === null) return null; + if (typeof sessionId !== "string") { + throw new Error("The stored protocol v3 smoke Session ID is invalid."); + } + return requirePlatformId(sessionId, "Stored protocol v3 smoke Session ID"); +} + +export function parseStoredProtocolV3SmokeRequestKey(raw: string): string | null { + const requestKey = requireSingleD1Row(raw).smoke_request_key; + if (requestKey === null) return null; + if (typeof requestKey !== "string") { + throw new Error("The stored protocol v3 smoke request key is invalid."); + } + return requireSmokeRequestKey(requestKey); +} + +export function protocolV3SmokeAgentSql(agentId: string): string { + const validated = requirePlatformId(agentId, "Protocol v3 smoke Agent ID"); + return `SELECT + (SELECT "kind" FROM "agent" WHERE "id" = '${validated}') AS "kind", + (SELECT "status" FROM "agent" WHERE "id" = '${validated}') AS "status";`; +} + +export function assertProtocolV3SmokeAgent(raw: string): void { + const row = requireSingleD1Row(raw); + if (row.kind !== "cattle" || row.status !== "published") { + throw new Error( + "Protocol v3 smoke configuration must identify an existing published cattle Agent.", + ); + } +} + +export function isProtocolV3SmokeReady(status: ProtocolV3SmokeStatus): boolean { + return ( + status.driverStatus === "ready" && + status.statusEvent === "driver.ready" && + status.protocolVersion === 3 && + status.bootTokenUsedAt !== null && + status.connectionId !== null && + status.connectionId.length > 0 && + status.driverPid !== null && + status.driverPid > 0 && + status.driverStartedAt !== null && + status.driverVersion !== null && + status.driverVersion.length > 0 + ); +} + +export function openProtocolV3SmokeWindowSql(accountId: string): string { + const validated = requirePlatformId(accountId, "Protocol v3 smoke account ID"); + return `UPDATE "${PROTOCOL_V3_CUTOVER_TABLE}" SET "command_freeze" = 1, "smoke_account_id" = '${validated}' WHERE "id" = 1 AND "enabled" = 1 AND "phase" = 'draining';`; +} + +export function storeProtocolV3SmokeRequestKeySql(requestKey: string): string { + const validated = requireSmokeRequestKey(requestKey); + return `UPDATE "${PROTOCOL_V3_CUTOVER_TABLE}" SET "smoke_request_key" = '${validated}' WHERE "id" = 1 AND "enabled" = 1 AND "phase" = 'draining';`; +} + +export function storeProtocolV3SmokeSessionSql(sessionId: string): string { + const validated = requirePlatformId(sessionId, "Protocol v3 smoke Session ID"); + return `UPDATE "${PROTOCOL_V3_CUTOVER_TABLE}" SET "smoke_session_id" = '${validated}' WHERE "id" = 1 AND "enabled" = 1 AND "phase" = 'draining';`; +} + +export function protocolV3SmokeStatusSql(sessionId: string): string { + const validated = requirePlatformId(sessionId, "Protocol v3 smoke Session ID"); + return ` +WITH "latest" AS ( + SELECT * + FROM "driver_instance" + WHERE "sandbox_session_id" = '${validated}' + ORDER BY "created_at" DESC + LIMIT 1 +) +SELECT + "latest"."status" AS "driver_status", + "latest"."status_event" AS "status_event", + "latest"."protocol_version" AS "protocol_version", + "latest"."driver_pid" AS "driver_pid", + "latest"."driver_started_at" AS "driver_started_at", + "latest"."driver_version" AS "driver_version", + "latest"."connection_id" AS "connection_id", + "latest"."boot_token_used_at" AS "boot_token_used_at" +FROM (SELECT 1) AS "singleton" +LEFT JOIN "latest" ON 1 = 1; +`; +} + +export function storeProtocolV3CutoverBookmarkSql(bookmark: string): string { + if (!/^[0-9a-z-]{1,256}$/iu.test(bookmark)) { + throw new Error("Cannot persist an invalid D1 Time Travel bookmark."); + } + + return `UPDATE "${PROTOCOL_V3_CUTOVER_TABLE}" SET "pre_migration_bookmark" = '${bookmark}' WHERE "id" = 1 AND "enabled" = 1 AND "phase" = 'draining' AND "pre_migration_bookmark" IS NULL;`; +} diff --git a/apps/api/package.json b/apps/api/package.json index 80fa6125..c2f55db8 100644 --- a/apps/api/package.json +++ b/apps/api/package.json @@ -10,10 +10,10 @@ "db:migrate:local": "vp exec wrangler d1 migrations apply DB --local", "lint": "vp lint .", "tc": "vp run cf:types && vp exec tsc --noEmit", - "test": "vp exec bun test tests" + "test": "vp exec bun test tests --isolate" }, "dependencies": { - "@cloudflare/sandbox": "0.12.6", + "@cloudflare/sandbox": "0.12.9", "@mosoo/ag-ui-session": "workspace:*", "@mosoo/agent-driver": "workspace:*", "@mosoo/agent-package": "workspace:*", @@ -30,20 +30,20 @@ "@orpc/server": "^1.15.0", "arktype": "^2.2.0", "better-auth": "1.6.28", - "cloudflare": "7.0.0", + "cloudflare": "7.1.0", "drizzle-orm": "^0.45.2", "fflate": "^0.8.3", "graphql": "^17.0.2", "graphql-yoga": "^5.21.3", "hono": "^4.12.32", + "ignore": "7.0.7", "jsonc-parser": "3.3.1", - "smol-toml": "1.8.0", - "xstate": "^5.32.0" + "smol-toml": "1.8.0" }, "devDependencies": { "@types/node": "^25.8.0", "typescript": "^6.0.3", - "vite-plus": "^0.1.23", - "wrangler": "^4.123.0" + "vite-plus": "^0.3.0", + "wrangler": "^4.127.1" } } diff --git a/apps/api/src/adapters/durable-objects/sandbox.do.ts b/apps/api/src/adapters/durable-objects/sandbox.do.ts index 7f40dbb0..d475dbcf 100644 --- a/apps/api/src/adapters/durable-objects/sandbox.do.ts +++ b/apps/api/src/adapters/durable-objects/sandbox.do.ts @@ -1,5 +1,7 @@ +import { isAsyncTimeoutError, promiseWithTimeout } from "@mosoo/effects"; import { DurableObject } from "cloudflare:workers"; +import { hashSandboxNetworkConstraints } from "../../modules/runtime/domain/sandbox-network-constraints"; import type { ApiBindings } from "../../platform/cloudflare/worker-types"; import { configureSandboxNetworkConstraints, @@ -12,7 +14,31 @@ import type { SandboxRpcForwardMethod } from "./sandbox-rpc-methods"; interface SandboxDelegate extends SandboxNetworkDelegate { alarm(alarmProps?: { isRetry: boolean; retryCount: number }): Promise; + createBackup(options: { + dir: string; + excludes?: string[]; + localBucket?: boolean; + name?: string; + ttl?: number; + }): Promise<{ dir: string; id: string }>; + createSession(options: { id: string }): Promise<{ + readFile(path: string, options?: { encoding?: "utf8" }): Promise<{ content: string }>; + terminal(request: Request, options?: unknown): Promise; + }>; + deleteSession(sessionId: string): Promise; + destroy(): Promise; + exists(path: string): Promise<{ + exists: boolean; + path: string; + success: boolean; + timestamp: string; + }>; fetch(request: Request): Promise; + getContainerPlacementId(): Promise; + mkdir(path: string, options?: { recursive?: boolean }): Promise; + readFile(path: string, options?: { encoding?: "utf8" }): Promise<{ content: string }>; + setKeepAlive(keepAlive: boolean): Promise; + writeFile(path: string, content: string): Promise; } type SandboxContainerState = DurableObjectState<{}> & { @@ -22,8 +48,35 @@ type SandboxContainerState = DurableObjectState<{}> & { }; const FORWARD_SANDBOX_METHOD = Symbol("forwardSandboxMethod"); +const RUNTIME_SUBJECT_INCARNATION_STORAGE_KEY = "mosooRuntimeSubjectIncarnation"; +const RUNTIME_SUBJECT_NETWORK_CONSTRAINTS_HASH_STORAGE_KEY = + "mosooRuntimeSubjectNetworkConstraintsHash"; +const RUNTIME_SUBJECT_READY_PLACEMENT_STORAGE_KEY = "mosooRuntimeSubjectReadyPlacement"; +const RUNTIME_SUBJECT_RETIRED_STORAGE_KEY = "mosooRuntimeSubjectRetiredAt"; +const RUNTIME_SUBJECT_SENTINEL_PATH = "/tmp/.mosoo-runtime-subject-incarnation"; +const RUNTIME_SUBJECT_RETIRED_RETRY_DELAY_MS = 5_000; +const RUNTIME_SUBJECT_RETIRED_DESTROY_TIMEOUT_MS = 15_000; + +function assertRuntimeSubjectIncarnation(value: number): void { + if (!Number.isSafeInteger(value) || value < 0) { + throw new TypeError("Runtime subject incarnation must be a non-negative safe integer."); + } +} + +function assertRuntimeSubjectNetworkConstraintsHash(value: string): void { + if (!/^[0-9a-f]{64}$/u.test(value)) { + throw new TypeError("Runtime subject network constraints hash must be a SHA-256 digest."); + } +} + +function isRuntimeSubjectSentinelMissing(error: unknown): boolean { + return ( + typeof error === "object" && error !== null && Reflect.get(error, "code") === "FILE_NOT_FOUND" + ); +} export class Sandbox extends DurableObject { + readonly #backupCreations = new Map>(); readonly #delegatePromise: Promise; readonly #httpsInterceptionDisabled: boolean; readonly #networkRestorePromise: Promise; @@ -46,38 +99,590 @@ export class Sandbox extends DurableObject { } async configureNetworkConstraints(constraints: unknown): Promise { - await this.#networkRestorePromise; - const delegate = await this.#delegatePromise; + await this.#runUnlessRuntimeSubjectRetired(async () => { + const networkConstraintsHash = await hashSandboxNetworkConstraints(constraints); + const admittedHash = await this.ctx.storage.get( + RUNTIME_SUBJECT_NETWORK_CONSTRAINTS_HASH_STORAGE_KEY, + ); + if (admittedHash !== networkConstraintsHash) { + throw new Error("Sandbox network constraints do not match the admitted incarnation."); + } + await this.#networkRestorePromise; + const delegate = await this.#delegatePromise; - await configureSandboxNetworkConstraints(this.ctx.storage, delegate, constraints, { - containerRunning: (this.ctx as SandboxContainerState).container?.running === true, - httpsInterceptionDisabled: this.#httpsInterceptionDisabled, + await configureSandboxNetworkConstraints(this.ctx.storage, delegate, constraints, { + containerRunning: (this.ctx as SandboxContainerState).container?.running === true, + httpsInterceptionDisabled: this.#httpsInterceptionDisabled, + }); }); } override async fetch(request: Request): Promise { - await this.#networkRestorePromise; - return (await this.#delegatePromise).fetch(request); + return this.#runUnlessRuntimeSubjectRetired(async () => { + await this.#networkRestorePromise; + return (await this.#delegatePromise).fetch(request); + }); } override async alarm(alarmProps?: { isRetry: boolean; retryCount: number }): Promise { - await this.#networkRestorePromise; - await (await this.#delegatePromise).alarm(alarmProps); + try { + if (await this.#isRuntimeSubjectRetired()) { + return; + } + + await this.#networkRestorePromise; + await (await this.#delegatePromise).alarm(alarmProps); + } finally { + await this.#destroyRuntimeSubjectContainerIfRetired(); + } + } + + async activateRuntimeSubjectIncarnation( + incarnation: number, + networkConstraintsHash: string, + ): Promise { + assertRuntimeSubjectIncarnation(incarnation); + assertRuntimeSubjectNetworkConstraintsHash(networkConstraintsHash); + + await this.ctx.blockConcurrencyWhile(async () => { + this.#assertRuntimeSubjectNotRetired( + await this.ctx.storage.get(RUNTIME_SUBJECT_RETIRED_STORAGE_KEY), + ); + const [current, currentNetworkConstraintsHash] = await Promise.all([ + this.ctx.storage.get(RUNTIME_SUBJECT_INCARNATION_STORAGE_KEY), + this.ctx.storage.get(RUNTIME_SUBJECT_NETWORK_CONSTRAINTS_HASH_STORAGE_KEY), + ]); + + if (current === incarnation && currentNetworkConstraintsHash === networkConstraintsHash) { + return; + } + if (current === undefined && currentNetworkConstraintsHash === undefined) { + await this.ctx.storage.put({ + [RUNTIME_SUBJECT_INCARNATION_STORAGE_KEY]: incarnation, + [RUNTIME_SUBJECT_NETWORK_CONSTRAINTS_HASH_STORAGE_KEY]: networkConstraintsHash, + }); + return; + } + throw new Error("Runtime subject incarnation identity does not match this Durable Object."); + }); + } + + async createRuntimeSubjectBackup( + incarnation: number, + options: { + dir: string; + excludes?: string[]; + forbiddenPaths?: string[]; + localBucket?: boolean; + name: string; + ttl?: number; + }, + ): Promise<{ dir: string; id: string }> { + assertRuntimeSubjectIncarnation(incarnation); + if (options.name.length === 0) { + throw new TypeError("Runtime subject backup name must not be empty."); + } + + const key = `${incarnation}:${options.name}`; + let creation = this.#backupCreations.get(key); + if (creation === undefined) { + creation = this.#runUnlessRuntimeSubjectRetired(async () => { + const current = await this.ctx.storage.get(RUNTIME_SUBJECT_INCARNATION_STORAGE_KEY); + if (current !== incarnation) { + throw new Error("Runtime subject backup targeted a stale incarnation."); + } + await this.#networkRestorePromise; + const delegate = await this.#delegatePromise; + for (const path of options.forbiddenPaths ?? []) { + const result = await delegate.exists(path); + if (!result.success) { + throw new Error("Runtime subject backup secret preflight failed."); + } + if (result.exists) { + throw new Error("Runtime subject backup contains legacy persistent credentials."); + } + } + await delegate.mkdir(options.dir, { recursive: true }); + const { forbiddenPaths: _, ...backupOptions } = options; + return delegate.createBackup(backupOptions); + }); + this.#backupCreations.set(key, creation); + } + try { + const backup = await creation; + if (backup.dir !== options.dir) { + throw new Error("Runtime subject backup name was reused for a different directory."); + } + return backup; + } finally { + if (this.#backupCreations.get(key) === creation) { + this.#backupCreations.delete(key); + } + } + } + + async inspectRuntimeSubjectIncarnation( + incarnation: number, + networkConstraintsHash: string, + ): Promise<{ kind: "healthy" | "missing" | "retired" | "stale" | "unknown" }> { + assertRuntimeSubjectIncarnation(incarnation); + assertRuntimeSubjectNetworkConstraintsHash(networkConstraintsHash); + const before = await this.#readRuntimeSubjectReadyState(); + if (before.retired) { + return { kind: "retired" }; + } + if ( + before.incarnation !== incarnation || + before.networkConstraintsHash !== networkConstraintsHash + ) { + return { kind: "stale" }; + } + if (before.placement === undefined) { + return { kind: "unknown" }; + } + + let health: "healthy" | "missing" | "unknown"; + try { + health = await this.#readyRuntimeSubjectHealth( + incarnation, + networkConstraintsHash, + before.placement, + ); + } finally { + await this.#destroyRuntimeSubjectContainerIfRetired(); + } + const after = await this.#readRuntimeSubjectReadyState(); + if (after.retired) { + return { kind: "retired" }; + } + if ( + after.incarnation !== incarnation || + after.networkConstraintsHash !== networkConstraintsHash + ) { + return { kind: "stale" }; + } + return { kind: after.placement === before.placement ? health : "unknown" }; + } + + async markRuntimeSubjectIncarnationReady( + incarnation: number, + networkConstraintsHash: string, + ): Promise { + assertRuntimeSubjectIncarnation(incarnation); + assertRuntimeSubjectNetworkConstraintsHash(networkConstraintsHash); + + const before = await this.#readRuntimeSubjectReadyState(); + if (before.retired) { + throw new Error("Runtime subject incarnation is retired."); + } + if ( + before.incarnation !== incarnation || + before.networkConstraintsHash !== networkConstraintsHash + ) { + throw new Error("Runtime subject readiness targeted a stale incarnation."); + } + if ((this.ctx as SandboxContainerState).container?.running !== true) { + throw new Error("Runtime subject container stopped before readiness was recorded."); + } + + try { + await this.#networkRestorePromise; + const delegate = await this.#delegatePromise; + await delegate.writeFile( + RUNTIME_SUBJECT_SENTINEL_PATH, + `${incarnation}:${networkConstraintsHash}`, + ); + const placement = await delegate.getContainerPlacementId(); + if (placement === undefined) { + throw new Error("Runtime subject container placement is not available."); + } + + await this.ctx.blockConcurrencyWhile(async () => { + this.#assertRuntimeSubjectNotRetired( + await this.ctx.storage.get(RUNTIME_SUBJECT_RETIRED_STORAGE_KEY), + ); + const [current, currentNetworkConstraintsHash] = await Promise.all([ + this.ctx.storage.get(RUNTIME_SUBJECT_INCARNATION_STORAGE_KEY), + this.ctx.storage.get(RUNTIME_SUBJECT_NETWORK_CONSTRAINTS_HASH_STORAGE_KEY), + ]); + if (current !== incarnation || currentNetworkConstraintsHash !== networkConstraintsHash) { + throw new Error("Runtime subject readiness targeted a stale incarnation."); + } + if ((this.ctx as SandboxContainerState).container?.running !== true) { + throw new Error("Runtime subject container stopped before readiness was recorded."); + } + await this.ctx.storage.put(RUNTIME_SUBJECT_READY_PLACEMENT_STORAGE_KEY, placement); + }); + } finally { + await this.#destroyRuntimeSubjectContainerIfRetired(); + } + } + + async destroyRuntimeSubjectIncarnation( + incarnation: number, + ): Promise<{ kind: "destroyed" | "stale" }> { + assertRuntimeSubjectIncarnation(incarnation); + + const stale = await this.ctx.blockConcurrencyWhile(async () => { + const current = await this.ctx.storage.get(RUNTIME_SUBJECT_INCARNATION_STORAGE_KEY); + + if (current !== undefined && current !== incarnation) { + return true; + } + if (current === undefined) { + await this.ctx.storage.put(RUNTIME_SUBJECT_INCARNATION_STORAGE_KEY, incarnation); + } + + await this.ctx.storage.put(RUNTIME_SUBJECT_RETIRED_STORAGE_KEY, Date.now()); + await this.ctx.storage.setAlarm(Date.now() + RUNTIME_SUBJECT_RETIRED_RETRY_DELAY_MS); + return false; + }); + if (stale) { + return { kind: "stale" }; + } + + await this.#destroyRetiredRuntimeSubjectContainer(); + return { kind: "destroyed" }; + } + + async #isRuntimeSubjectRetired(): Promise { + return (await this.ctx.storage.get(RUNTIME_SUBJECT_RETIRED_STORAGE_KEY)) !== undefined; + } + + async #readyRuntimeSubjectHealth( + incarnation: number, + networkConstraintsHash: string, + readyPlacement: string | null, + ): Promise<"healthy" | "missing" | "unknown"> { + if ((this.ctx as SandboxContainerState).container?.running !== true) { + return "missing"; + } + + const delegate = await this.#delegatePromise; + try { + await this.#networkRestorePromise; + // The SDK placement id is cached in DO storage and survives onStop. + // A sentinel read forces a real container handshake before we trust it. + const sentinel = await delegate.readFile(RUNTIME_SUBJECT_SENTINEL_PATH, { + encoding: "utf8", + }); + const placement = await delegate.getContainerPlacementId(); + if ((this.ctx as SandboxContainerState).container?.running !== true) { + return "missing"; + } + if (sentinel.content !== `${incarnation}:${networkConstraintsHash}`) { + return "missing"; + } + if (typeof placement === "string" && typeof readyPlacement === "string") { + return placement === readyPlacement ? "healthy" : "missing"; + } + if (placement !== null || readyPlacement !== null) { + return "unknown"; + } + return "healthy"; + } catch (error) { + if ((this.ctx as SandboxContainerState).container?.running !== true) { + return "missing"; + } + try { + const placement = await delegate.getContainerPlacementId(); + if ( + typeof placement === "string" && + typeof readyPlacement === "string" && + placement !== readyPlacement + ) { + return "missing"; + } + } catch { + // Preserve the original, more informative health failure below. + } + return isRuntimeSubjectSentinelMissing(error) ? "missing" : "unknown"; + } + } + + #assertRuntimeSubjectNotRetired(retiredAt: number | undefined): void { + if (retiredAt !== undefined) { + throw new Error("Runtime subject incarnation is retired."); + } + } + + async #destroyRetiredRuntimeSubjectContainer(): Promise { + try { + const delegate = await this.#delegatePromise; + await promiseWithTimeout( + (async () => { + await delegate.setKeepAlive(false); + await delegate.destroy(); + })(), + { + label: "Runtime subject retired container destroy", + timeoutMs: RUNTIME_SUBJECT_RETIRED_DESTROY_TIMEOUT_MS, + }, + ); + } catch (error) { + await this.ctx.storage.setAlarm(Date.now() + RUNTIME_SUBJECT_RETIRED_RETRY_DELAY_MS); + if (isAsyncTimeoutError(error)) { + this.ctx.abort("Runtime subject retired container destroy timed out.", { + retryAlarm: true, + }); + } + throw error; + } + } + + async #destroyRuntimeSubjectContainerIfRetired(): Promise { + if (await this.#isRuntimeSubjectRetired()) { + await this.#destroyRetiredRuntimeSubjectContainer(); + } + } + + async #assertReadyRuntimeSubjectContainerHealthy(): Promise<{ + readonly incarnation: number | undefined; + readonly networkConstraintsHash: string | undefined; + readonly placement: string | null | undefined; + readonly retired: boolean; + }> { + const before = await this.#readRuntimeSubjectReadyState(); + if (before.retired) { + throw new Error("Runtime subject incarnation is retired."); + } + if (before.incarnation === undefined || before.placement === undefined) { + return before; + } + let health: "healthy" | "missing" | "unknown"; + try { + if (before.networkConstraintsHash === undefined) { + throw new Error("Runtime subject ready container has no network identity."); + } + health = await this.#readyRuntimeSubjectHealth( + before.incarnation, + before.networkConstraintsHash, + before.placement, + ); + } finally { + await this.#destroyRuntimeSubjectContainerIfRetired(); + } + const after = await this.#readRuntimeSubjectReadyState(); + if ( + after.retired || + after.incarnation !== before.incarnation || + after.networkConstraintsHash !== before.networkConstraintsHash || + after.placement !== before.placement + ) { + throw new Error("Runtime subject incarnation changed during its health probe."); + } + if (health === "healthy") { + return after; + } + if (health === "unknown") { + throw new Error("Runtime subject ready container health is unknown."); + } + + await this.ctx.blockConcurrencyWhile(async () => { + const current = await this.#readRuntimeSubjectReadyStateWithoutGate(); + if ( + current.retired || + current.incarnation !== before.incarnation || + current.networkConstraintsHash !== before.networkConstraintsHash || + current.placement !== before.placement + ) { + throw new Error("Runtime subject incarnation changed before retirement."); + } + await this.ctx.storage.put(RUNTIME_SUBJECT_RETIRED_STORAGE_KEY, Date.now()); + await this.ctx.storage.setAlarm(Date.now() + RUNTIME_SUBJECT_RETIRED_RETRY_DELAY_MS); + }); + throw new Error("Runtime subject ready container was replaced or stopped."); + } + + async #readRuntimeSubjectReadyState(): Promise<{ + readonly incarnation: number | undefined; + readonly networkConstraintsHash: string | undefined; + readonly placement: string | null | undefined; + readonly retired: boolean; + }> { + return this.ctx.blockConcurrencyWhile(() => this.#readRuntimeSubjectReadyStateWithoutGate()); + } + + async #readRuntimeSubjectReadyStateWithoutGate(): Promise<{ + readonly incarnation: number | undefined; + readonly networkConstraintsHash: string | undefined; + readonly placement: string | null | undefined; + readonly retired: boolean; + }> { + const [incarnation, networkConstraintsHash, placement, retiredAt] = await Promise.all([ + this.ctx.storage.get(RUNTIME_SUBJECT_INCARNATION_STORAGE_KEY), + this.ctx.storage.get(RUNTIME_SUBJECT_NETWORK_CONSTRAINTS_HASH_STORAGE_KEY), + this.ctx.storage.get(RUNTIME_SUBJECT_READY_PLACEMENT_STORAGE_KEY), + this.ctx.storage.get(RUNTIME_SUBJECT_RETIRED_STORAGE_KEY), + ]); + return { incarnation, networkConstraintsHash, placement, retired: retiredAt !== undefined }; + } + + #guardRuntimeSubjectProcess(process: unknown): unknown { + if (typeof process !== "object" || process === null) { + throw new TypeError("Cloudflare Sandbox process handle is not an object."); + } + return this.#guardRuntimeSubjectRpcTarget(process); + } + + #guardRuntimeSubjectForwardResult(method: string, result: unknown): unknown { + if (method === "createSession" || method === "getSession") { + return this.#guardRuntimeSubjectExecutionSession(result); + } + if (method === "startProcess") { + return this.#guardRuntimeSubjectProcess(result); + } + if (method === "getProcess") { + return result === null ? null : this.#guardRuntimeSubjectProcess(result); + } + if (method === "listProcesses") { + if (!Array.isArray(result)) { + throw new TypeError("Cloudflare Sandbox process list is not an array."); + } + return result.map((process) => this.#guardRuntimeSubjectProcess(process)); + } + return result; + } + + #guardRuntimeSubjectExecutionSession(session: unknown): unknown { + if (typeof session !== "object" || session === null) { + throw new TypeError("Cloudflare Sandbox execution session is not an object."); + } + return this.#guardRuntimeSubjectRpcTarget(session); + } + + #guardRuntimeSubjectRpcTarget(target: object): object { + const facade: Record = {}; + for (const key of Reflect.ownKeys(target)) { + const value = Reflect.get(target, key); + Reflect.set( + facade, + key, + typeof value === "function" + ? (...args: unknown[]) => + this.#runUnlessRuntimeSubjectRetired(async () => { + const result = await (Reflect.apply( + value, + target, + this.#guardRuntimeSubjectInvocationArgs(String(key), args), + ) as Promise); + return this.#guardRuntimeSubjectForwardResult(String(key), result); + }) + : value, + ); + } + return facade; + } + + #guardRuntimeSubjectInvocationArgs(method: string, args: readonly unknown[]): readonly unknown[] { + if (method !== "startProcess") { + return args; + } + const options = args[1]; + if (typeof options !== "object" || options === null) { + return args; + } + const onStart = Reflect.get(options, "onStart"); + if (typeof onStart !== "function") { + return args; + } + const guardedOptions = { + ...options, + onStart: (process: unknown) => + Reflect.apply(onStart, options, [this.#guardRuntimeSubjectProcess(process)]), + }; + return [args[0], guardedOptions, ...args.slice(2)]; + } + + async #runUnlessRuntimeSubjectRetired(task: () => Promise): Promise { + const failures: unknown[] = []; + let retired = false; + let result: T | undefined; + try { + const before = await this.#assertReadyRuntimeSubjectContainerHealthy(); + + try { + result = await task(); + } catch (error) { + failures.push(error); + } + + try { + retired = await this.#isRuntimeSubjectRetired(); + if (!retired) { + const after = await this.#assertReadyRuntimeSubjectContainerHealthy(); + if ( + before.incarnation !== after.incarnation || + before.networkConstraintsHash !== after.networkConstraintsHash || + before.placement !== after.placement + ) { + await this.ctx.blockConcurrencyWhile(async () => { + await this.ctx.storage.put(RUNTIME_SUBJECT_RETIRED_STORAGE_KEY, Date.now()); + await this.ctx.storage.setAlarm(Date.now() + RUNTIME_SUBJECT_RETIRED_RETRY_DELAY_MS); + }); + throw new Error("Runtime subject incarnation changed during the operation."); + } + } + } catch (error) { + failures.push(error); + } + } catch (error) { + failures.push(error); + } + + if (!retired) { + try { + retired = await this.#isRuntimeSubjectRetired(); + } catch (error) { + failures.push(error); + } + } + if (retired) { + if (failures.length === 0) { + failures.push(new Error("Runtime subject incarnation was retired during the operation.")); + } + try { + await this.#destroyRetiredRuntimeSubjectContainer(); + } catch (error) { + failures.push(error); + } + } + + if (failures.length === 1) { + throw failures[0]; + } + if (failures.length > 1) { + throw new AggregateError(failures, "Runtime subject operation failed in multiple phases."); + } + return result as T; } async [FORWARD_SANDBOX_METHOD]( method: SandboxRpcForwardMethod, args: readonly unknown[], ): Promise { - await waitForSandboxNetworkRestore(this.#networkRestorePromise, method, args); - const delegate = await this.#delegatePromise; - const action = Reflect.get(delegate, method); + return this.#runUnlessRuntimeSubjectRetired(async () => { + await waitForSandboxNetworkRestore(this.#networkRestorePromise, method, args); + const delegate = await this.#delegatePromise; + const action = Reflect.get(delegate, method); - if (typeof action !== "function") { - throw new TypeError(`Cloudflare Sandbox delegate is missing ${method}.`); - } + if (typeof action !== "function") { + throw new TypeError(`Cloudflare Sandbox delegate is missing ${method}.`); + } - return await (Reflect.apply(action, delegate, args) as Promise); + const result = await (Reflect.apply( + action, + delegate, + this.#guardRuntimeSubjectInvocationArgs(method, args), + ) as Promise); + const runtimeSubjectIncarnation = await this.ctx.storage.get( + RUNTIME_SUBJECT_INCARNATION_STORAGE_KEY, + ); + if (runtimeSubjectIncarnation !== undefined) { + return this.#guardRuntimeSubjectForwardResult(method, result); + } + return result; + }); } } diff --git a/apps/api/src/adapters/durable-objects/session.do.ts b/apps/api/src/adapters/durable-objects/session.do.ts index c92f6eb3..d56dce8c 100644 --- a/apps/api/src/adapters/durable-objects/session.do.ts +++ b/apps/api/src/adapters/durable-objects/session.do.ts @@ -8,7 +8,20 @@ interface SessionDelegate { closeViewers(sessionId: string, reason: string): Promise; destroy(sessionId: string, reason: string): Promise; fetch(request: Request): Promise; - publishEvents(sessionId: string, events: AgUiSessionEvent[]): Promise; + publishEventBatches( + sessionId: string, + batches: Array<{ + events: AgUiSessionEvent[]; + previousRuntimeEventSeqCursor: number | null; + runtimeEventSeqCursor: number | null; + }>, + ): Promise; + publishEvents( + sessionId: string, + events: AgUiSessionEvent[], + runtimeEventSeqCursor?: number | null, + previousRuntimeEventSeqCursor?: number | null, + ): Promise; syncViewers(sessionId: string): Promise; webSocketClose(ws: WebSocket, code: number, reason: string): Promise; webSocketError(ws: WebSocket, error: unknown): Promise | void; @@ -34,8 +47,26 @@ export class Session extends DurableObject { await (await this.#delegatePromise).alarm(); } - async publishEvents(sessionId: string, events: AgUiSessionEvent[]): Promise { - await (await this.#delegatePromise).publishEvents(sessionId, events); + async publishEvents( + sessionId: string, + events: AgUiSessionEvent[], + runtimeEventSeqCursor: number | null = null, + previousRuntimeEventSeqCursor: number | null = null, + ): Promise { + await ( + await this.#delegatePromise + ).publishEvents(sessionId, events, runtimeEventSeqCursor, previousRuntimeEventSeqCursor); + } + + async publishEventBatches( + sessionId: string, + batches: Array<{ + events: AgUiSessionEvent[]; + previousRuntimeEventSeqCursor: number | null; + runtimeEventSeqCursor: number | null; + }>, + ): Promise { + await (await this.#delegatePromise).publishEventBatches(sessionId, batches); } async syncViewers(sessionId: string): Promise { diff --git a/apps/api/src/adapters/graphql/schema.generated.graphql b/apps/api/src/adapters/graphql/schema.generated.graphql index 2f6d1c6a..6938fc2b 100644 --- a/apps/api/src/adapters/graphql/schema.generated.graphql +++ b/apps/api/src/adapters/graphql/schema.generated.graphql @@ -509,7 +509,7 @@ enum AppDeploymentRunStatus { } enum AppDeploymentTargetKind { - cloudflare_pages + cloudflare_static_assets cloudflare_worker } diff --git a/apps/api/src/adapters/graphql/schema/app-schema.ts b/apps/api/src/adapters/graphql/schema/app-schema.ts index 76625b02..666bfa55 100644 --- a/apps/api/src/adapters/graphql/schema/app-schema.ts +++ b/apps/api/src/adapters/graphql/schema/app-schema.ts @@ -19,7 +19,7 @@ export const appSchema = /* GraphQL */ ` } enum AppDeploymentTargetKind { - cloudflare_pages + cloudflare_static_assets cloudflare_worker } diff --git a/apps/api/src/index.ts b/apps/api/src/index.ts index b9941fa0..c3faa401 100644 --- a/apps/api/src/index.ts +++ b/apps/api/src/index.ts @@ -4,6 +4,7 @@ export { ContainerProxy } from "./adapters/durable-objects/sandbox-container-pro export { DriverConnection } from "./adapters/durable-objects/driver-connection.do"; export { Sandbox } from "./adapters/durable-objects/sandbox.do"; export { Session } from "./adapters/durable-objects/session.do"; +export { AppDeploymentWorkflow } from "./platform/cloudflare/app-deployment-workflow"; void arktypeWorkerConfigInitialized; diff --git a/apps/api/src/modules/api-command/application/api-command-enqueue.ts b/apps/api/src/modules/api-command/application/api-command-enqueue.ts index 50ae37a0..e1d05048 100644 --- a/apps/api/src/modules/api-command/application/api-command-enqueue.ts +++ b/apps/api/src/modules/api-command/application/api-command-enqueue.ts @@ -1,11 +1,13 @@ import type { AppDeploymentRunId } from "@mosoo/id"; import type { ApiBindings } from "../../../platform/cloudflare/worker-types"; -import { admitApiCommand, enqueueApiCommand } from "./api-command-ledger"; -import type { ApiCommandAdmission, EnqueueApiCommandInput } from "./api-command-ledger"; +import { enqueueApiCommand } from "./api-command-ledger"; +import type { EnqueueApiCommandInput } from "./api-command-ledger"; import type { AppDeploymentRunDispatchCommandPayload, + AppDeploymentScriptReconciliationCommandPayload, CostLedgerReconciliationCommandPayload, + SandboxBackupReconciliationCommandPayload, ScheduledMaintenanceCommandPayload, SessionRunDispatchCommandPayload, } from "./api-command-payload"; @@ -17,7 +19,7 @@ export function createAppDeploymentRunDispatchDedupeKey(runId: AppDeploymentRunI } export async function enqueueAppDeploymentRunDispatchCommand( - bindings: Pick, + bindings: Pick, payload: AppDeploymentRunDispatchCommandPayload, ): Promise { await enqueueApiCommand(bindings, { @@ -27,6 +29,17 @@ export async function enqueueAppDeploymentRunDispatchCommand( }); } +export async function enqueueAppDeploymentScriptReconciliationCommand( + bindings: Pick, + payload: AppDeploymentScriptReconciliationCommandPayload, +): Promise { + await enqueueApiCommand(bindings, { + dedupeKey: `app-deployment-script-reconciliation:${payload.scheduledTime}:${payload.cursor ?? "root"}`, + kind: "app_deployment_script_reconciliation", + payload, + }); +} + export async function enqueueCostLedgerReconciliationCommand( bindings: Pick, payload: CostLedgerReconciliationCommandPayload, @@ -54,11 +67,20 @@ export async function enqueueScheduledMaintenanceCommand( }); } -export async function admitSessionRunDispatchCommand( +export async function enqueueSandboxBackupReconciliationCommand( bindings: Pick, - payload: SessionRunDispatchCommandPayload, -): Promise { - return admitApiCommand(bindings, createSessionRunDispatchApiCommandInput(payload)); + payload: SandboxBackupReconciliationCommandPayload, +): Promise { + await enqueueApiCommand(bindings, { + dedupeKey: [ + "sandbox_backup_reconciliation", + payload.scheduledTime, + payload.databasePage, + payload.cursor ?? "start", + ].join(":"), + kind: "sandbox_backup_reconciliation", + payload, + }); } export function createSessionRunDispatchApiCommandInput( diff --git a/apps/api/src/modules/api-command/application/api-command-ledger.ts b/apps/api/src/modules/api-command/application/api-command-ledger.ts index 4cb37e4f..b6fc6f3b 100644 --- a/apps/api/src/modules/api-command/application/api-command-ledger.ts +++ b/apps/api/src/modules/api-command/application/api-command-ledger.ts @@ -1,7 +1,7 @@ import { apiCommandsTable } from "@mosoo/db"; import type { ApiCommandId, ApiCommandKind, ApiCommandRow } from "@mosoo/db"; import { createPlatformId } from "@mosoo/id"; -import { and, asc, eq, inArray, lt, or, sql } from "drizzle-orm"; +import { and, asc, eq, gt, inArray, isNull, lte, or, sql } from "drizzle-orm"; import { createErrorLogContext, logError } from "../../../platform/cloudflare/logger"; import type { ApiBindings } from "../../../platform/cloudflare/worker-types"; @@ -23,6 +23,8 @@ const API_COMMAND_QUEUE_SEND_FAILED_MESSAGE = "API command queue send failed."; const API_COMMAND_QUEUE_REDRIVE_LIMIT = 100; +const APP_DEPLOYMENT_WORKFLOW_RECONCILE_LIMIT = 100; + export interface EnqueueApiCommandInput { dedupeKey: string; kind: ApiCommandKind; @@ -37,12 +39,26 @@ export interface PreparedApiCommand { export interface ApiCommandClaim { attemptCount: number; + claimOwner: string; commandId: ApiCommandId; dedupeKey: string; + deliveryGeneration: number; kind: ApiCommandKind; + lastErrorCode: string | null; + lastErrorMessage: string | null; payloadJson: string; } +export type ApiCommandClaimAuthority = Pick< + ApiCommandClaim, + "attemptCount" | "claimOwner" | "commandId" | "deliveryGeneration" +>; + +export type ApiCommandClaimResult = + | { readonly claim: ApiCommandClaim; readonly kind: "claimed" } + | { readonly claimExpiresAt: number | null; readonly kind: "busy" } + | { readonly kind: "missing" | "stale" | "terminal" }; + export interface ApiCommandAdmission { readonly commandId: ApiCommandId; readonly kind: ApiCommandKind; @@ -50,7 +66,7 @@ export interface ApiCommandAdmission { } type ApiCommandDeliveryBindings = Pick & - Partial>; + Partial>; function normalizeDedupeKey(value: string): string { const dedupeKey = value.trim(); @@ -62,18 +78,42 @@ function normalizeDedupeKey(value: string): string { return dedupeKey; } -function toQueueMessage(commandId: ApiCommandId): ApiCommandMessage { - return { commandId }; +function toQueueMessage(commandId: ApiCommandId, deliveryGeneration: number): ApiCommandMessage { + return { commandId, deliveryGeneration }; +} + +export function getAppDeploymentWorkflowInstanceId( + commandId: ApiCommandId, + deliveryGeneration: number, +): string { + return `app-${commandId.toLowerCase()}-g${deliveryGeneration.toString(36)}`; +} + +async function createAppDeploymentWorkflowInstance( + workflow: Workflow, + message: ApiCommandMessage, +): Promise { + const instanceId = getAppDeploymentWorkflowInstanceId( + message.commandId, + message.deliveryGeneration, + ); + + await workflow.createBatch([{ id: instanceId, params: message }]); } export async function findApiCommandByDedupeKey( database: D1Database, dedupeKey: string, -): Promise | null> { +): Promise | null> { return ( (await getAppDatabase(database) .select({ id: apiCommandsTable.id, + deliveryGeneration: apiCommandsTable.deliveryGeneration, + kind: apiCommandsTable.kind, lastErrorCode: apiCommandsTable.lastErrorCode, lastErrorMessage: apiCommandsTable.lastErrorMessage, status: apiCommandsTable.status, @@ -88,6 +128,7 @@ export async function findApiCommandByDedupeKey( async function markApiCommandQueueSendFailed(input: { commandId: ApiCommandId; database: D1Database; + deliveryGeneration: number; }): Promise { await getAppDatabase(input.database) .update(apiCommandsTable) @@ -96,13 +137,20 @@ async function markApiCommandQueueSendFailed(input: { lastErrorMessage: API_COMMAND_QUEUE_SEND_FAILED_MESSAGE, updatedAt: currentTimestampMs(), }) - .where(and(eq(apiCommandsTable.id, input.commandId), eq(apiCommandsTable.status, "queued"))) + .where( + and( + eq(apiCommandsTable.id, input.commandId), + eq(apiCommandsTable.deliveryGeneration, input.deliveryGeneration), + eq(apiCommandsTable.status, "queued"), + ), + ) .run(); } async function clearApiCommandQueueSendFailure(input: { commandId: ApiCommandId; database: D1Database; + deliveryGeneration: number; }): Promise { await getAppDatabase(input.database) .update(apiCommandsTable) @@ -114,6 +162,7 @@ async function clearApiCommandQueueSendFailure(input: { .where( and( eq(apiCommandsTable.id, input.commandId), + eq(apiCommandsTable.deliveryGeneration, input.deliveryGeneration), eq(apiCommandsTable.status, "queued"), inArray(apiCommandsTable.lastErrorCode, [ API_COMMAND_QUEUE_DELIVERY_PENDING_CODE, @@ -127,22 +176,35 @@ async function clearApiCommandQueueSendFailure(input: { async function sendApiCommandMessage( bindings: ApiCommandDeliveryBindings, commandId: ApiCommandId, + deliveryGeneration: number, kind: ApiCommandKind, -): Promise { - const queue = - kind === "environment_package_artifact_build" - ? bindings.ENVIRONMENT_ARTIFACT_BUILD_QUEUE - : bindings.API_COMMAND_QUEUE; - if (!queue) { - throw new Error("Environment artifact build queue binding is required."); - } +): Promise { try { - await queue.send(toQueueMessage(commandId)); + const message = toQueueMessage(commandId, deliveryGeneration); + if (kind === "app_deployment_run_dispatch") { + if (!bindings.APP_DEPLOYMENT_WORKFLOW) { + throw new Error("App deployment Workflow binding is required."); + } + await createAppDeploymentWorkflowInstance(bindings.APP_DEPLOYMENT_WORKFLOW, message); + } else { + const queue = + kind === "environment_package_artifact_build" + ? bindings.ENVIRONMENT_ARTIFACT_BUILD_QUEUE + : bindings.API_COMMAND_QUEUE; + if (!queue) { + throw new Error("Environment artifact build queue binding is required."); + } + await queue.send(message); + } } catch (error) { // A rejected producer response does not prove that Queue discarded the message. // The durable outbox record remains eligible for scheduled redrive either way. try { - await markApiCommandQueueSendFailed({ commandId, database: bindings.DB }); + await markApiCommandQueueSendFailed({ + commandId, + database: bindings.DB, + deliveryGeneration, + }); } catch (markError) { logError("api-command.enqueue_failure_mark_failed", { ...createErrorLogContext(markError), @@ -154,11 +216,15 @@ async function sendApiCommandMessage( ...createErrorLogContext(error), commandId, }); - return; + return false; } try { - await clearApiCommandQueueSendFailure({ commandId, database: bindings.DB }); + await clearApiCommandQueueSendFailure({ + commandId, + database: bindings.DB, + deliveryGeneration, + }); } catch (error) { // Queue accepted the command. Leaving its delivery marker intact is safe: // a later redrive may send a duplicate, and consumer claiming is idempotent. @@ -167,13 +233,56 @@ async function sendApiCommandMessage( commandId, }); } + return true; +} + +export type ApiCommandQueueHandoffDisposition = "continue" | "finished" | "retry"; + +export async function handoffAppDeploymentQueueMessage( + bindings: ApiCommandDeliveryBindings, + message: ApiCommandMessage, +): Promise { + const command = await getAppDatabase(bindings.DB) + .select({ + deliveryGeneration: apiCommandsTable.deliveryGeneration, + kind: apiCommandsTable.kind, + status: apiCommandsTable.status, + }) + .from(apiCommandsTable) + .where(eq(apiCommandsTable.id, message.commandId)) + .limit(1) + .get(); + + if ( + command === undefined || + command.deliveryGeneration !== message.deliveryGeneration || + (command.status !== "queued" && command.status !== "running") + ) { + return "finished"; + } + if (command.kind !== "app_deployment_run_dispatch") { + return "continue"; + } + + return (await sendApiCommandMessage( + bindings, + message.commandId, + message.deliveryGeneration, + command.kind, + )) + ? "finished" + : "retry"; } export async function redriveFailedApiCommandEnqueues( bindings: ApiCommandDeliveryBindings, ): Promise { const commands = await getAppDatabase(bindings.DB) - .select({ id: apiCommandsTable.id, kind: apiCommandsTable.kind }) + .select({ + deliveryGeneration: apiCommandsTable.deliveryGeneration, + id: apiCommandsTable.id, + kind: apiCommandsTable.kind, + }) .from(apiCommandsTable) .where( and( @@ -189,7 +298,156 @@ export async function redriveFailedApiCommandEnqueues( .all(); for (const command of commands) { - await sendApiCommandMessage(bindings, command.id, command.kind); + await sendApiCommandMessage(bindings, command.id, command.deliveryGeneration, command.kind); + } +} + +function recoverableApiCommandPredicate(nowMs: number) { + return or( + eq(apiCommandsTable.status, "queued"), + and( + eq(apiCommandsTable.status, "running"), + or(isNull(apiCommandsTable.claimExpiresAt), lte(apiCommandsTable.claimExpiresAt, nowMs)), + ), + ); +} + +async function markAppDeploymentWorkflowReconciled(input: { + commandId: ApiCommandId; + database: D1Database; + deliveryGeneration: number; + nowMs: number; + payloadJson: string; +}): Promise { + await getAppDatabase(input.database) + .update(apiCommandsTable) + .set({ + updatedAt: sql`CASE WHEN ${apiCommandsTable.updatedAt} = ${Number.MAX_SAFE_INTEGER} THEN ${apiCommandsTable.updatedAt} ELSE max(${apiCommandsTable.updatedAt} + 1, ${input.nowMs}) END`, + }) + .where( + and( + eq(apiCommandsTable.id, input.commandId), + eq(apiCommandsTable.deliveryGeneration, input.deliveryGeneration), + eq(apiCommandsTable.kind, "app_deployment_run_dispatch"), + eq(apiCommandsTable.payloadJson, input.payloadJson), + recoverableApiCommandPredicate(input.nowMs), + ), + ) + .run(); +} + +export async function reconcileAppDeploymentWorkflowDeliveries( + bindings: ApiCommandDeliveryBindings & Pick, + nowMs = currentTimestampMs(), +): Promise { + const commands = await getAppDatabase(bindings.DB) + .select({ + deliveryGeneration: apiCommandsTable.deliveryGeneration, + id: apiCommandsTable.id, + payloadJson: apiCommandsTable.payloadJson, + }) + .from(apiCommandsTable) + .where( + and( + eq(apiCommandsTable.kind, "app_deployment_run_dispatch"), + recoverableApiCommandPredicate(nowMs), + ), + ) + .orderBy(asc(apiCommandsTable.updatedAt), asc(apiCommandsTable.id)) + .limit(APP_DEPLOYMENT_WORKFLOW_RECONCILE_LIMIT) + .all(); + + for (const command of commands) { + try { + const instanceId = getAppDeploymentWorkflowInstanceId(command.id, command.deliveryGeneration); + let instance: WorkflowInstance; + try { + instance = await bindings.APP_DEPLOYMENT_WORKFLOW.get(instanceId); + } catch { + await sendApiCommandMessage( + bindings, + command.id, + command.deliveryGeneration, + "app_deployment_run_dispatch", + ); + continue; + } + + let status: InstanceStatus["status"]; + try { + status = (await instance.status()).status; + } catch (error) { + logError("api-command.app_deployment_workflow_status_failed", { + ...createErrorLogContext(error), + commandId: command.id, + deliveryGeneration: command.deliveryGeneration, + }); + await sendApiCommandMessage( + bindings, + command.id, + command.deliveryGeneration, + "app_deployment_run_dispatch", + ); + continue; + } + + try { + switch (status) { + case "paused": + case "waitingForPause": { + await instance.resume(); + break; + } + case "complete": + case "errored": + case "terminated": { + await instance.restart(); + break; + } + case "queued": + case "running": + case "waiting": { + break; + } + case "unknown": { + logError("api-command.app_deployment_workflow_status_unknown", { + commandId: command.id, + deliveryGeneration: command.deliveryGeneration, + }); + await sendApiCommandMessage( + bindings, + command.id, + command.deliveryGeneration, + "app_deployment_run_dispatch", + ); + break; + } + } + } catch (error) { + logError("api-command.app_deployment_workflow_recovery_failed", { + ...createErrorLogContext(error), + commandId: command.id, + deliveryGeneration: command.deliveryGeneration, + status, + }); + } + } finally { + try { + await markAppDeploymentWorkflowReconciled({ + commandId: command.id, + database: bindings.DB, + deliveryGeneration: command.deliveryGeneration, + nowMs, + payloadJson: command.payloadJson, + }); + } catch (error) { + logError("api-command.app_deployment_workflow_cursor_update_failed", { + ...createErrorLogContext(error), + commandId: command.id, + deliveryGeneration: command.deliveryGeneration, + }); + } + } } } @@ -209,6 +467,7 @@ export function prepareApiCommand( completedAt: null, createdAt: timestampMs, dedupeKey: normalizeDedupeKey(input.dedupeKey), + deliveryGeneration: 1, id: commandId, kind: input.kind, lastErrorCode: API_COMMAND_QUEUE_DELIVERY_PENDING_CODE, @@ -242,15 +501,23 @@ export async function admitApiCommand( if (current === null) { throw new Error("API command enqueue could not confirm the ledger row."); } + if (current.kind !== input.kind) { + throw new Error("API command dedupe key is already used by a different command kind."); + } if (input.retryTerminal === true && current.status !== "queued" && current.status !== "running") { - await database + if (current.deliveryGeneration >= Number.MAX_SAFE_INTEGER) { + throw new Error("API command delivery generation is exhausted."); + } + + const retried = await database .update(apiCommandsTable) .set({ attemptCount: 0, claimExpiresAt: null, claimOwner: null, completedAt: null, + deliveryGeneration: sql`${apiCommandsTable.deliveryGeneration} + 1`, lastErrorCode: API_COMMAND_QUEUE_DELIVERY_PENDING_CODE, lastErrorMessage: API_COMMAND_QUEUE_DELIVERY_PENDING_MESSAGE, payloadJson: prepared.record.payloadJson, @@ -260,11 +527,13 @@ export async function admitApiCommand( .where( and( eq(apiCommandsTable.id, current.id), + eq(apiCommandsTable.deliveryGeneration, current.deliveryGeneration), inArray(apiCommandsTable.status, ["dead_lettered", "failed", "succeeded"]), ), ) - .run(); - return { commandId: current.id, kind: input.kind, shouldDeliver: true }; + .returning({ id: apiCommandsTable.id }) + .get(); + return { commandId: current.id, kind: input.kind, shouldDeliver: retried !== undefined }; } if ( @@ -286,7 +555,24 @@ export async function deliverApiCommand( return; } - await sendApiCommandMessage(bindings, admission.commandId, admission.kind); + const command = await getAppDatabase(bindings.DB) + .select({ + deliveryGeneration: apiCommandsTable.deliveryGeneration, + kind: apiCommandsTable.kind, + }) + .from(apiCommandsTable) + .where(eq(apiCommandsTable.id, admission.commandId)) + .limit(1) + .get(); + if (command === undefined) { + throw new Error("API command delivery could not find its durable ledger row."); + } + await sendApiCommandMessage( + bindings, + admission.commandId, + command.deliveryGeneration, + command.kind, + ); } export async function enqueueApiCommand( @@ -301,9 +587,13 @@ export async function enqueueApiCommand( export async function claimApiCommand(input: { commandId: ApiCommandId; database: D1Database; + deliveryGeneration: number; nowMs?: number; - ownerId: string; -}): Promise { + claimOwner: string; +}): Promise { + if (input.claimOwner.trim().length === 0) { + throw new Error("API command claim owner is required."); + } const nowMs = input.nowMs ?? currentTimestampMs(); const row = (await getAppDatabase(input.database) @@ -311,16 +601,24 @@ export async function claimApiCommand(input: { .set({ attemptCount: sql`${apiCommandsTable.attemptCount} + 1`, claimExpiresAt: nowMs + API_COMMAND_LEASE_MS, - claimOwner: input.ownerId, + claimOwner: input.claimOwner, status: "running", updatedAt: nowMs, }) .where( and( eq(apiCommandsTable.id, input.commandId), + eq(apiCommandsTable.deliveryGeneration, input.deliveryGeneration), + sql`typeof(${apiCommandsTable.attemptCount}) = 'integer' AND ${apiCommandsTable.attemptCount} BETWEEN 0 AND ${Number.MAX_SAFE_INTEGER - 1}`, or( eq(apiCommandsTable.status, "queued"), - and(eq(apiCommandsTable.status, "running"), lt(apiCommandsTable.claimExpiresAt, nowMs)), + and( + eq(apiCommandsTable.status, "running"), + or( + isNull(apiCommandsTable.claimExpiresAt), + lte(apiCommandsTable.claimExpiresAt, nowMs), + ), + ), ), ), ) @@ -328,19 +626,62 @@ export async function claimApiCommand(input: { attemptCount: apiCommandsTable.attemptCount, commandId: apiCommandsTable.id, dedupeKey: apiCommandsTable.dedupeKey, + deliveryGeneration: apiCommandsTable.deliveryGeneration, kind: apiCommandsTable.kind, + lastErrorCode: apiCommandsTable.lastErrorCode, + lastErrorMessage: apiCommandsTable.lastErrorMessage, payloadJson: apiCommandsTable.payloadJson, }) .get()) ?? null; - return row; + if (row !== null) { + return { claim: { ...row, claimOwner: input.claimOwner }, kind: "claimed" }; + } + + const state = await getAppDatabase(input.database) + .select({ + attemptCount: apiCommandsTable.attemptCount, + claimExpiresAt: apiCommandsTable.claimExpiresAt, + deliveryGeneration: apiCommandsTable.deliveryGeneration, + status: apiCommandsTable.status, + }) + .from(apiCommandsTable) + .where(eq(apiCommandsTable.id, input.commandId)) + .limit(1) + .get(); + if (state === undefined) { + return { kind: "missing" }; + } + if (state.deliveryGeneration !== input.deliveryGeneration) { + return { kind: "stale" }; + } + if (state.status !== "queued" && state.status !== "running") { + return { kind: "terminal" }; + } + if (!Number.isSafeInteger(state.attemptCount) || state.attemptCount < 0) { + throw new Error("API command attempt count is corrupt."); + } + if (state.attemptCount === Number.MAX_SAFE_INTEGER) { + throw new Error("API command attempt count is exhausted."); + } + return { claimExpiresAt: state.claimExpiresAt, kind: "busy" }; +} + +export function exactApiCommandClaimPredicate(claim: ApiCommandClaimAuthority, nowMs: number) { + return and( + eq(apiCommandsTable.id, claim.commandId), + eq(apiCommandsTable.deliveryGeneration, claim.deliveryGeneration), + eq(apiCommandsTable.attemptCount, claim.attemptCount), + eq(apiCommandsTable.status, "running"), + eq(apiCommandsTable.claimOwner, claim.claimOwner), + gt(apiCommandsTable.claimExpiresAt, nowMs), + ); } export async function renewApiCommandClaim(input: { - commandId: ApiCommandId; + claim: ApiCommandClaim; database: D1Database; nowMs?: number; - ownerId: string; }): Promise { const nowMs = input.nowMs ?? currentTimestampMs(); const result = await getAppDatabase(input.database) @@ -349,27 +690,20 @@ export async function renewApiCommandClaim(input: { claimExpiresAt: nowMs + API_COMMAND_LEASE_MS, updatedAt: nowMs, }) - .where( - and( - eq(apiCommandsTable.id, input.commandId), - eq(apiCommandsTable.status, "running"), - eq(apiCommandsTable.claimOwner, input.ownerId), - ), - ) + .where(exactApiCommandClaimPredicate(input.claim, nowMs)) .run(); return getD1ChangeCount(result) > 0; } export async function completeApiCommand(input: { - commandId: ApiCommandId; + claim: ApiCommandClaim; database: D1Database; nowMs?: number; - ownerId: string; -}): Promise { +}): Promise { const nowMs = input.nowMs ?? currentTimestampMs(); - await getAppDatabase(input.database) + const result = await getAppDatabase(input.database) .update(apiCommandsTable) .set({ claimExpiresAt: null, @@ -380,27 +714,21 @@ export async function completeApiCommand(input: { status: "succeeded", updatedAt: nowMs, }) - .where( - and( - eq(apiCommandsTable.id, input.commandId), - eq(apiCommandsTable.status, "running"), - eq(apiCommandsTable.claimOwner, input.ownerId), - ), - ) + .where(exactApiCommandClaimPredicate(input.claim, nowMs)) .run(); + return getD1ChangeCount(result) > 0; } export async function releaseApiCommandForRetry(input: { - commandId: ApiCommandId; + claim: ApiCommandClaim; database: D1Database; errorCode: string; errorMessage: string; nowMs?: number; - ownerId: string; -}): Promise { +}): Promise { const nowMs = input.nowMs ?? currentTimestampMs(); - await getAppDatabase(input.database) + const result = await getAppDatabase(input.database) .update(apiCommandsTable) .set({ claimExpiresAt: null, @@ -410,27 +738,21 @@ export async function releaseApiCommandForRetry(input: { status: "queued", updatedAt: nowMs, }) - .where( - and( - eq(apiCommandsTable.id, input.commandId), - eq(apiCommandsTable.status, "running"), - eq(apiCommandsTable.claimOwner, input.ownerId), - ), - ) + .where(exactApiCommandClaimPredicate(input.claim, nowMs)) .run(); + return getD1ChangeCount(result) > 0; } export async function markApiCommandFailed(input: { - commandId: ApiCommandId; + claim: ApiCommandClaim; database: D1Database; errorCode: string; errorMessage: string; nowMs?: number; - ownerId: string; -}): Promise { +}): Promise { const nowMs = input.nowMs ?? currentTimestampMs(); - await getAppDatabase(input.database) + const result = await getAppDatabase(input.database) .update(apiCommandsTable) .set({ claimExpiresAt: null, @@ -441,26 +763,21 @@ export async function markApiCommandFailed(input: { status: "failed", updatedAt: nowMs, }) - .where( - and( - eq(apiCommandsTable.id, input.commandId), - eq(apiCommandsTable.status, "running"), - eq(apiCommandsTable.claimOwner, input.ownerId), - ), - ) + .where(exactApiCommandClaimPredicate(input.claim, nowMs)) .run(); + return getD1ChangeCount(result) > 0; } export async function markApiCommandDeadLettered(input: { - commandId: ApiCommandId; + claim: ApiCommandClaim; database: D1Database; errorCode: string; errorMessage: string; nowMs?: number; -}): Promise { +}): Promise { const nowMs = input.nowMs ?? currentTimestampMs(); - await getAppDatabase(input.database) + const result = await getAppDatabase(input.database) .update(apiCommandsTable) .set({ claimExpiresAt: null, @@ -471,6 +788,7 @@ export async function markApiCommandDeadLettered(input: { status: "dead_lettered", updatedAt: nowMs, }) - .where(eq(apiCommandsTable.id, input.commandId)) + .where(exactApiCommandClaimPredicate(input.claim, nowMs)) .run(); + return getD1ChangeCount(result) > 0; } diff --git a/apps/api/src/modules/api-command/application/api-command-message.ts b/apps/api/src/modules/api-command/application/api-command-message.ts index f6345241..03bf7efc 100644 --- a/apps/api/src/modules/api-command/application/api-command-message.ts +++ b/apps/api/src/modules/api-command/application/api-command-message.ts @@ -3,6 +3,7 @@ import { parsePlatformId } from "@mosoo/id"; export interface ApiCommandMessage { commandId: ApiCommandId; + deliveryGeneration: number; } export function parseApiCommandMessage(value: unknown): ApiCommandMessage { @@ -11,8 +12,20 @@ export function parseApiCommandMessage(value: unknown): ApiCommandMessage { } const commandId = (value as Record)["commandId"]; + const deliveryGeneration = (value as Record)["deliveryGeneration"]; + + if ( + typeof deliveryGeneration !== "number" || + !Number.isSafeInteger(deliveryGeneration) || + deliveryGeneration <= 0 + ) { + throw new Error( + "API command queue message deliveryGeneration must be a positive safe integer.", + ); + } return { commandId: parsePlatformId(commandId, "API command queue message commandId"), + deliveryGeneration, }; } diff --git a/apps/api/src/modules/api-command/application/api-command-payload.ts b/apps/api/src/modules/api-command/application/api-command-payload.ts index 80fe461c..e4aee160 100644 --- a/apps/api/src/modules/api-command/application/api-command-payload.ts +++ b/apps/api/src/modules/api-command/application/api-command-payload.ts @@ -17,8 +17,10 @@ import type { type ApiCommandPayload = | AppDeploymentRunDispatchCommandPayload + | AppDeploymentScriptReconciliationCommandPayload | CostLedgerReconciliationCommandPayload | EnvironmentPackageArtifactBuildCommandPayload + | SandboxBackupReconciliationCommandPayload | ScheduledMaintenanceCommandPayload | SessionRunDispatchCommandPayload; @@ -34,6 +36,17 @@ export interface CostLedgerReconciliationCommandPayload { scheduledTime: number; } +export interface SandboxBackupReconciliationCommandPayload { + cursor: string | null; + databasePage: number; + scheduledTime: number; +} + +export interface AppDeploymentScriptReconciliationCommandPayload { + cursor: string | null; + scheduledTime: number; +} + export interface AppDeploymentRunDispatchCommandPayload { appDeploymentRunId: AppDeploymentRunId; } @@ -228,6 +241,42 @@ function parseCostLedgerReconciliationPayload( }; } +function parseSandboxBackupReconciliationPayload( + value: unknown, +): SandboxBackupReconciliationCommandPayload { + const label = "sandbox_backup_reconciliation payload"; + const record = requireRecord(value, label); + const cursor = readOptionalString(record, "cursor", label); + if (cursor !== null && cursor.length === 0) { + throw new ApiCommandPayloadError(`${label}.cursor must not be empty.`); + } + const databasePage = readInteger(record, "databasePage", label); + if (databasePage < 0) { + throw new ApiCommandPayloadError(`${label}.databasePage must not be negative.`); + } + const scheduledTime = readInteger(record, "scheduledTime", label); + if (scheduledTime < 0 || !Number.isFinite(new Date(scheduledTime).getTime())) { + throw new ApiCommandPayloadError(`${label}.scheduledTime must be a valid timestamp.`); + } + return { cursor, databasePage, scheduledTime }; +} + +function parseAppDeploymentScriptReconciliationPayload( + value: unknown, +): AppDeploymentScriptReconciliationCommandPayload { + const label = "app_deployment_script_reconciliation payload"; + const record = requireRecord(value, label); + const cursor = readOptionalString(record, "cursor", label); + if (cursor !== null && cursor.length === 0) { + throw new ApiCommandPayloadError(`${label}.cursor must not be empty.`); + } + const scheduledTime = readInteger(record, "scheduledTime", label); + if (scheduledTime < 0 || !Number.isFinite(new Date(scheduledTime).getTime())) { + throw new ApiCommandPayloadError(`${label}.scheduledTime must be a valid timestamp.`); + } + return { cursor, scheduledTime }; +} + function parseAppDeploymentRunDispatchPayload( value: unknown, ): AppDeploymentRunDispatchCommandPayload { @@ -291,12 +340,18 @@ export function parseApiCommandPayload( case "app_deployment_run_dispatch": { return parseAppDeploymentRunDispatchPayload(parsed); } + case "app_deployment_script_reconciliation": { + return parseAppDeploymentScriptReconciliationPayload(parsed); + } case "cost_ledger_reconciliation": { return parseCostLedgerReconciliationPayload(parsed); } case "environment_package_artifact_build": { return parseEnvironmentPackageArtifactBuildPayload(parsed); } + case "sandbox_backup_reconciliation": { + return parseSandboxBackupReconciliationPayload(parsed); + } case "scheduled_maintenance": { return parseScheduledMaintenancePayload(parsed); } diff --git a/apps/api/src/modules/api-command/application/api-command-processor.ts b/apps/api/src/modules/api-command/application/api-command-processor.ts index 1e6ab2fc..50aa448e 100644 --- a/apps/api/src/modules/api-command/application/api-command-processor.ts +++ b/apps/api/src/modules/api-command/application/api-command-processor.ts @@ -1,14 +1,25 @@ -import { apiCommandsTable, appDeploymentRunsTable } from "@mosoo/db"; -import type { ApiCommandId } from "@mosoo/db"; +import { apiCommandsTable, appDeploymentRunsTable, appDeploymentsTable } from "@mosoo/db"; import type { AppDeploymentRunId } from "@mosoo/id"; import { parsePlatformId } from "@mosoo/id"; -import { and, eq, inArray } from "drizzle-orm"; +import { and, eq, exists, inArray, isNull, notExists, or, sql } from "drizzle-orm"; import { createErrorLogContext, logError, logInfo } from "../../../platform/cloudflare/logger"; import type { ApiBindings } from "../../../platform/cloudflare/worker-types"; -import { getAppDatabase } from "../../../platform/db/drizzle"; +import { + getAppDatabase, + getD1ChangeCount, + runAppDatabaseBatch, +} from "../../../platform/db/drizzle"; +import type { AppDatabase } from "../../../platform/db/drizzle"; import { currentTimestampMs } from "../../../time"; +import { createCloudflareDeploymentClient } from "../../apps/application/app-deployment-cloudflare-client"; import { dispatchAppDeploymentRun } from "../../apps/application/app-deployment-executor.service"; +import type { AppDeploymentDispatchOutcome } from "../../apps/application/app-deployment-executor.service"; +import { reconcileAppDeploymentScriptPage } from "../../apps/application/app-deployment-script-reconciliation.service"; +import type { + AppDeploymentScriptCleanupAction, + AppDeploymentScriptReconciliationAuthority, +} from "../../apps/application/app-deployment-script-reconciliation.service"; import { ACTIVE_APP_DEPLOYMENT_RUN_STATUSES } from "../../apps/domain/app-deployment-lifecycle"; import { parseCostLedgerReconciliationActivationMode, @@ -17,15 +28,23 @@ import { import { runUsageDailyRollup } from "../../cost/application/cost-rollup.service"; import { buildEnvironmentPackageArtifact } from "../../environments/application/environment-package-artifact-build.service"; import { dispatchQueuedSessionRun } from "../../runtime/application/session-runs/dispatch-queued-run.service"; +import { createLeaseOwnershipRenewal } from "../../runtime/infrastructure/runtime-subject-lifecycle/lease-ownership-renewal"; import { runSandboxMaintenance } from "../../runtime/infrastructure/runtime-subject-lifecycle/runtime-subject-maintenance.service"; +import { destroyUnversionedSandboxContainer } from "../../runtime/infrastructure/runtime-subject-lifecycle/runtime-subject-platform"; +import { reconcileSandboxBackupPage } from "../../runtime/infrastructure/sandbox-backup-reconciliation.service"; import { APP_DEPLOYMENT_RUN_DISPATCH_DEDUPE_PREFIX, + enqueueAppDeploymentScriptReconciliationCommand, enqueueCostLedgerReconciliationCommand, + enqueueSandboxBackupReconciliationCommand, } from "./api-command-enqueue"; import { + API_COMMAND_LEASE_MS, API_COMMAND_LEASE_RENEWAL_INTERVAL_MS, claimApiCommand, completeApiCommand, + exactApiCommandClaimPredicate, + handoffAppDeploymentQueueMessage, markApiCommandDeadLettered, markApiCommandFailed, releaseApiCommandForRetry, @@ -37,8 +56,10 @@ import type { ApiCommandMessage } from "./api-command-message"; import { ApiCommandPayloadError, parseApiCommandPayload } from "./api-command-payload"; import type { AppDeploymentRunDispatchCommandPayload, + AppDeploymentScriptReconciliationCommandPayload, CostLedgerReconciliationCommandPayload, EnvironmentPackageArtifactBuildCommandPayload, + SandboxBackupReconciliationCommandPayload, ScheduledMaintenanceCommandPayload, SessionRunDispatchCommandPayload, } from "./api-command-payload"; @@ -50,9 +71,25 @@ import { const API_COMMAND_RETRY_DELAY_SECONDS = 30; -function createClaimOwnerId(message: Message): string { - const normalized = message.id.replaceAll(":", "_").trim(); - return normalized || "api-command-worker"; +const APP_DEPLOYMENT_SCRIPT_CLEANUP_TIMEOUT_MS = 2 * 60 * 1_000; + +export type ApiCommandDeliveryDisposition = + | { readonly kind: "finished" } + | { readonly delaySeconds: number; readonly kind: "retry" }; + +export interface ProcessApiCommandDeliveryOptions { + readonly appDeploymentDeadlineMs?: number; + readonly dispatchAppDeploymentRun?: typeof dispatchAppDeploymentRun; +} + +const FINISHED_DISPOSITION = { kind: "finished" } as const; + +function retryDisposition(delaySeconds = API_COMMAND_RETRY_DELAY_SECONDS) { + return { delaySeconds, kind: "retry" } as const; +} + +function createClaimOwnerId(): string { + return crypto.randomUUID(); } function getErrorMessage(error: unknown): string { @@ -79,6 +116,134 @@ function shouldStartCostLedgerReconciliation(now: Date): boolean { return now.getUTCHours() === 1 && now.getUTCMinutes() === 0; } +function shouldStartSandboxBackupReconciliation(now: Date): boolean { + return now.getUTCHours() === 3 && now.getUTCMinutes() === 0; +} + +function shouldStartAppDeploymentScriptReconciliation(now: Date): boolean { + return now.getUTCHours() === 4 && now.getUTCMinutes() === 0; +} + +async function withAppDeploymentCleanupTimeout( + operation: Promise, + label: string, +): Promise { + let timeout: ReturnType | undefined; + const timeoutPromise = new Promise((_resolve, reject) => { + timeout = setTimeout( + () => reject(new Error(`${label} exceeded its cleanup timeout.`)), + APP_DEPLOYMENT_SCRIPT_CLEANUP_TIMEOUT_MS, + ); + }); + try { + return await Promise.race([operation, timeoutPromise]); + } finally { + clearTimeout(timeout); + } +} + +export async function deleteAppDeploymentArtifactPrefix( + bucket: R2Bucket, + prefix: string, +): Promise { + const keys: string[] = []; + let page = await bucket.list({ limit: 1_000, prefix }); + while (page.truncated) { + keys.push(...page.objects.map(({ key }) => key)); + if (!page.cursor) { + throw new Error("App deployment artifact cleanup returned no continuation cursor."); + } + page = await bucket.list({ cursor: page.cursor, limit: 1_000, prefix }); + } + keys.push(...page.objects.map(({ key }) => key)); + for (let offset = 0; offset < keys.length; offset += 1_000) { + await bucket.delete(keys.slice(offset, offset + 1_000)); + } +} + +function assertAppDeploymentCleanupSucceeded( + results: readonly PromiseSettledResult[], + scriptName: string, +): void { + const errors = results.flatMap((result) => (result.status === "rejected" ? [result.reason] : [])); + if (errors.length > 0) { + throw new AggregateError(errors, `App deployment script cleanup failed for ${scriptName}.`); + } +} + +export async function cleanupAppDeploymentScript( + bindings: ApiBindings, + action: AppDeploymentScriptCleanupAction, +): Promise { + const producerResults = await Promise.allSettled([ + destroyUnversionedSandboxContainer( + bindings, + action.buildSandboxId, + APP_DEPLOYMENT_SCRIPT_CLEANUP_TIMEOUT_MS, + ), + destroyUnversionedSandboxContainer( + bindings, + action.deploySandboxId, + APP_DEPLOYMENT_SCRIPT_CLEANUP_TIMEOUT_MS, + ), + ]); + assertAppDeploymentCleanupSucceeded(producerResults, action.scriptName); + + const outputResults = await Promise.allSettled([ + withAppDeploymentCleanupTimeout( + deleteAppDeploymentArtifactPrefix(bindings.FILE_BUCKET, action.artifactPrefix), + "App deployment artifact cleanup", + ), + action.uploadStartedAt === null + ? Promise.resolve() + : (async () => + createCloudflareDeploymentClient(bindings).deleteWorkerScript({ + scriptName: action.scriptName, + timeoutMs: APP_DEPLOYMENT_SCRIPT_CLEANUP_TIMEOUT_MS, + }))(), + ]); + assertAppDeploymentCleanupSucceeded(outputResults, action.scriptName); +} + +async function processAppDeploymentScriptReconciliationCommand( + bindings: ApiBindings, + payload: AppDeploymentScriptReconciliationCommandPayload, + processedAtMs: number, + authority: AppDeploymentScriptReconciliationAuthority, +): Promise { + const result = await reconcileAppDeploymentScriptPage( + bindings.DB, + { authority, cursor: payload.cursor }, + (action) => cleanupAppDeploymentScript(bindings, action), + ); + for (const failure of result.failures) { + logError("app-deployment.script_reconciliation_cleanup_failed", { + ...createErrorLogContext(failure.error), + scriptName: failure.scriptName, + }); + } + logInfo("app-deployment.script_reconciliation_page_completed", { + armed: result.armed, + cleaned: result.cleaned, + deferred: result.deferred, + failureCount: result.failures.length, + processed: result.processed, + processedAtMs, + scheduledTime: payload.scheduledTime, + }); + if (!result.hasMore) { + return; + } + if (result.nextCursor === null) { + throw new Error("App deployment script reconciliation returned no continuation cursor."); + } + await authority.requireOwnership(); + await enqueueAppDeploymentScriptReconciliationCommand(bindings, { + cursor: result.nextCursor, + scheduledTime: payload.scheduledTime, + }); +} + async function processScheduledMaintenanceCommand( bindings: ApiBindings, payload: ScheduledMaintenanceCommandPayload, @@ -106,9 +271,54 @@ async function processScheduledMaintenanceCommand( } } + if (shouldStartSandboxBackupReconciliation(scheduledAt)) { + tasks.push( + enqueueSandboxBackupReconciliationCommand(bindings, { + cursor: null, + databasePage: 0, + scheduledTime: payload.scheduledTime, + }), + ); + } + + if (shouldStartAppDeploymentScriptReconciliation(scheduledAt)) { + tasks.push( + enqueueAppDeploymentScriptReconciliationCommand(bindings, { + cursor: null, + scheduledTime: payload.scheduledTime, + }), + ); + } + await Promise.all(tasks); } +async function processSandboxBackupReconciliationCommand( + bindings: ApiBindings, + payload: SandboxBackupReconciliationCommandPayload, + processedAtMs: number, +): Promise { + const result = await reconcileSandboxBackupPage(bindings, { + cursor: payload.cursor, + }); + logInfo("runtime.sandbox_backup.reconciliation_page_completed", { + ...result, + processedAtMs, + scheduledTime: payload.scheduledTime, + }); + if (!result.hasMore) { + return; + } + if (payload.databasePage === Number.MAX_SAFE_INTEGER) { + throw new Error("Sandbox backup reconciliation exhausted its database page identity."); + } + await enqueueSandboxBackupReconciliationCommand(bindings, { + cursor: result.nextCursor, + databasePage: payload.databasePage + 1, + scheduledTime: payload.scheduledTime, + }); +} + async function processCostLedgerReconciliationCommand( bindings: ApiBindings, payload: CostLedgerReconciliationCommandPayload, @@ -162,59 +372,6 @@ async function processSessionRunDispatchCommand( }); } -async function failActiveAppDeploymentRun( - bindings: ApiBindings, - runId: AppDeploymentRunId, - input: { errorCode: string; errorMessage: string; nowMs: number }, -): Promise { - await getAppDatabase(bindings.DB) - .update(appDeploymentRunsTable) - .set({ - errorCode: input.errorCode, - errorMessage: input.errorMessage, - status: "failed", - updatedAt: input.nowMs, - }) - .where( - and( - eq(appDeploymentRunsTable.id, runId), - inArray(appDeploymentRunsTable.status, ACTIVE_APP_DEPLOYMENT_RUN_STATUSES), - ), - ) - .run(); -} - -async function failAppDeploymentRunFromPayloadJson( - bindings: ApiBindings, - input: { - errorCode: string; - errorMessage: string; - fallbackDedupeKey?: string; - nowMs: number; - payloadJson: string; - }, - logEventName?: string, -): Promise { - try { - const runId = readAppDeploymentRunIdFromPayload(input); - - if (runId === null) { - return; - } - - await failActiveAppDeploymentRun(bindings, runId, input); - } catch (error) { - if (logEventName === undefined) { - throw error; - } - - logError(logEventName, { - ...createErrorLogContext(error), - errorCode: getErrorCode(error), - }); - } -} - function readAppDeploymentRunIdFromPayload(input: { fallbackDedupeKey?: string; payloadJson: string; @@ -255,17 +412,341 @@ function readAppDeploymentRunIdFromPayload(input: { } } +function claimedApiCommandExists(database: AppDatabase, claim: ApiCommandClaim, nowMs: number) { + return exists( + database + .select({ id: apiCommandsTable.id }) + .from(apiCommandsTable) + .where(exactClaimedApiCommand(claim, nowMs)), + ); +} + +function exactClaimedApiCommand(claim: ApiCommandClaim, nowMs: number) { + return and( + exactApiCommandClaimPredicate(claim, nowMs), + eq(apiCommandsTable.kind, claim.kind), + eq(apiCommandsTable.payloadJson, claim.payloadJson), + ); +} + +async function completeAppDeploymentCommand( + bindings: ApiBindings, + claim: ApiCommandClaim, + outcome: Extract, + nowMs: number, +): Promise { + const results = await runAppDatabaseBatch(bindings.DB, (database) => { + const deploymentIsCurrent = exists( + database + .select({ id: appDeploymentsTable.id }) + .from(appDeploymentsTable) + .where( + and( + eq(appDeploymentsTable.id, outcome.deploymentId), + eq(appDeploymentsTable.latestRunId, outcome.runId), + isNull(appDeploymentsTable.deletedAt), + ), + ), + ); + const runIsActivating = exists( + database + .select({ id: appDeploymentRunsTable.id }) + .from(appDeploymentRunsTable) + .where( + and( + eq(appDeploymentRunsTable.id, outcome.runId), + eq(appDeploymentRunsTable.deploymentId, outcome.deploymentId), + eq(appDeploymentRunsTable.status, "activating"), + eq(appDeploymentRunsTable.targetScriptName, outcome.activeScriptName), + ), + ), + ); + const deploymentMatchesOutcome = exists( + database + .select({ id: appDeploymentsTable.id }) + .from(appDeploymentsTable) + .where( + and( + eq(appDeploymentsTable.id, outcome.deploymentId), + eq(appDeploymentsTable.activeScriptName, outcome.activeScriptName), + eq(appDeploymentsTable.latestRunId, outcome.runId), + eq(appDeploymentsTable.lastSuccessfulUrl, outcome.url), + isNull(appDeploymentsTable.deletedAt), + ), + ), + ); + const runMatchesOutcome = exists( + database + .select({ id: appDeploymentRunsTable.id }) + .from(appDeploymentRunsTable) + .where( + and( + eq(appDeploymentRunsTable.id, outcome.runId), + eq(appDeploymentRunsTable.deploymentId, outcome.deploymentId), + eq(appDeploymentRunsTable.status, "success"), + eq(appDeploymentRunsTable.targetScriptName, outcome.activeScriptName), + eq(appDeploymentRunsTable.url, outcome.url), + outcome.externalDeploymentId === null + ? isNull(appDeploymentRunsTable.externalDeploymentId) + : eq(appDeploymentRunsTable.externalDeploymentId, outcome.externalDeploymentId), + outcome.externalProjectId === null + ? isNull(appDeploymentRunsTable.externalProjectId) + : eq(appDeploymentRunsTable.externalProjectId, outcome.externalProjectId), + outcome.externalVersionId === null + ? isNull(appDeploymentRunsTable.externalVersionId) + : eq(appDeploymentRunsTable.externalVersionId, outcome.externalVersionId), + ), + ), + ); + + return [ + database + .update(appDeploymentsTable) + .set({ + activeScriptName: outcome.activeScriptName, + lastSuccessfulUrl: outcome.url, + updatedAt: nowMs, + }) + .where( + and( + eq(appDeploymentsTable.id, outcome.deploymentId), + eq(appDeploymentsTable.latestRunId, outcome.runId), + isNull(appDeploymentsTable.deletedAt), + runIsActivating, + claimedApiCommandExists(database, claim, nowMs), + ), + ), + database + .update(appDeploymentRunsTable) + .set({ + errorCode: null, + errorMessage: null, + externalDeploymentId: outcome.externalDeploymentId, + externalProjectId: outcome.externalProjectId, + externalVersionId: outcome.externalVersionId, + status: "success", + updatedAt: nowMs, + url: outcome.url, + }) + .where( + and( + eq(appDeploymentRunsTable.id, outcome.runId), + eq(appDeploymentRunsTable.deploymentId, outcome.deploymentId), + eq(appDeploymentRunsTable.status, "activating"), + eq(appDeploymentRunsTable.targetScriptName, outcome.activeScriptName), + deploymentIsCurrent, + claimedApiCommandExists(database, claim, nowMs), + ), + ), + database + .update(apiCommandsTable) + .set({ + claimExpiresAt: null, + claimOwner: null, + completedAt: nowMs, + lastErrorCode: null, + lastErrorMessage: null, + status: "succeeded", + updatedAt: nowMs, + }) + .where( + and(exactClaimedApiCommand(claim, nowMs), deploymentMatchesOutcome, runMatchesOutcome), + ), + ]; + }); + + return getD1ChangeCount(results[2]) > 0; +} + +async function failAppDeploymentCommand( + bindings: ApiBindings, + claim: ApiCommandClaim, + input: { + errorCode: string; + errorMessage: string; + nowMs: number; + runId: AppDeploymentRunId; + status: "dead_lettered" | "failed"; + }, +): Promise { + const results = await runAppDatabaseBatch(bindings.DB, (database) => { + const runExists = database + .select({ id: appDeploymentRunsTable.id }) + .from(appDeploymentRunsTable) + .where(eq(appDeploymentRunsTable.id, input.runId)); + const runIsFailed = database + .select({ id: appDeploymentRunsTable.id }) + .from(appDeploymentRunsTable) + .where( + and( + eq(appDeploymentRunsTable.id, input.runId), + eq(appDeploymentRunsTable.status, "failed"), + ), + ); + + return [ + database + .update(appDeploymentRunsTable) + .set({ + errorCode: input.errorCode, + errorMessage: input.errorMessage, + status: "failed", + updatedAt: input.nowMs, + }) + .where( + and( + eq(appDeploymentRunsTable.id, input.runId), + inArray(appDeploymentRunsTable.status, ACTIVE_APP_DEPLOYMENT_RUN_STATUSES), + claimedApiCommandExists(database, claim, input.nowMs), + ), + ), + database + .update(apiCommandsTable) + .set({ + claimExpiresAt: null, + claimOwner: null, + completedAt: input.nowMs, + lastErrorCode: sql`coalesce( + (select ${appDeploymentRunsTable.errorCode} from ${appDeploymentRunsTable} + where ${appDeploymentRunsTable.id} = ${input.runId}), + ${input.errorCode} + )`, + lastErrorMessage: sql`coalesce( + (select ${appDeploymentRunsTable.errorMessage} from ${appDeploymentRunsTable} + where ${appDeploymentRunsTable.id} = ${input.runId}), + ${input.errorMessage} + )`, + status: input.status, + updatedAt: input.nowMs, + }) + .where( + and( + exactClaimedApiCommand(claim, input.nowMs), + or(exists(runIsFailed), notExists(runExists)), + ), + ), + ]; + }); + + return getD1ChangeCount(results[1]) > 0; +} + +async function reconcileSkippedAppDeploymentCommand( + bindings: ApiBindings, + claim: ApiCommandClaim, + nowMs: number, +): Promise { + const runId = readAppDeploymentRunIdFromPayload({ + fallbackDedupeKey: claim.dedupeKey, + payloadJson: claim.payloadJson, + }); + if (runId === null) { + return markApiCommandFailed({ + claim, + database: bindings.DB, + errorCode: "app_deployment_run_missing", + errorMessage: "App deployment command has no valid deployment run.", + nowMs, + }); + } + + const run = await getAppDatabase(bindings.DB) + .select({ + deploymentId: appDeploymentRunsTable.deploymentId, + errorCode: appDeploymentRunsTable.errorCode, + errorMessage: appDeploymentRunsTable.errorMessage, + status: appDeploymentRunsTable.status, + }) + .from(appDeploymentRunsTable) + .where(eq(appDeploymentRunsTable.id, runId)) + .limit(1) + .get(); + if (run === undefined) { + return markApiCommandFailed({ + claim, + database: bindings.DB, + errorCode: "app_deployment_run_missing", + errorMessage: "App deployment run no longer exists.", + nowMs, + }); + } + if (run.status === "success") { + return completeApiCommand({ claim, database: bindings.DB, nowMs }); + } + if (run.status === "failed") { + return markApiCommandFailed({ + claim, + database: bindings.DB, + errorCode: run.errorCode ?? "app_deployment_run_failed", + errorMessage: run.errorMessage ?? "App deployment run failed.", + nowMs, + }); + } + + const currentDeployment = await getAppDatabase(bindings.DB) + .select({ id: appDeploymentsTable.id }) + .from(appDeploymentsTable) + .where( + and( + eq(appDeploymentsTable.id, run.deploymentId), + eq(appDeploymentsTable.latestRunId, runId), + isNull(appDeploymentsTable.deletedAt), + ), + ) + .limit(1) + .get(); + if (currentDeployment !== undefined) { + return false; + } + + return failAppDeploymentCommand(bindings, claim, { + errorCode: "app_deployment_context_lost", + errorMessage: "App deployment is no longer current or active.", + nowMs, + runId, + status: "failed", + }); +} + async function processClaimedApiCommand( bindings: ApiBindings, claim: ApiCommandClaim, processedAtMs: number, -): Promise { + requireOwnership: () => Promise, + dispatchDeployment: typeof dispatchAppDeploymentRun, + appDeploymentDeadlineMs?: number, +): Promise< + { kind: "completed" } | { kind: "app_deployment"; outcome: AppDeploymentDispatchOutcome } +> { const payload = parseApiCommandPayload(claim.kind, claim.payloadJson); + const authority = { + attemptCount: claim.attemptCount, + claimOwner: claim.claimOwner, + commandId: claim.commandId, + deliveryGeneration: claim.deliveryGeneration, + requireOwnership, + }; + + await requireOwnership(); switch (claim.kind) { case "app_deployment_run_dispatch": { - await dispatchAppDeploymentRun(bindings, payload as AppDeploymentRunDispatchCommandPayload); - return; + const outcome = await dispatchDeployment( + bindings, + payload as AppDeploymentRunDispatchCommandPayload, + authority, + appDeploymentDeadlineMs === undefined ? {} : { deadlineMs: appDeploymentDeadlineMs }, + ); + return { kind: "app_deployment", outcome }; + } + case "app_deployment_script_reconciliation": { + await processAppDeploymentScriptReconciliationCommand( + bindings, + payload as AppDeploymentScriptReconciliationCommandPayload, + processedAtMs, + authority, + ); + break; } case "cost_ledger_reconciliation": { await processCostLedgerReconciliationCommand( @@ -273,148 +754,229 @@ async function processClaimedApiCommand( payload as CostLedgerReconciliationCommandPayload, processedAtMs, ); - return; + break; } case "environment_package_artifact_build": { await buildEnvironmentPackageArtifact( bindings, payload as EnvironmentPackageArtifactBuildCommandPayload, + authority, ); - return; + break; + } + case "sandbox_backup_reconciliation": { + await processSandboxBackupReconciliationCommand( + bindings, + payload as SandboxBackupReconciliationCommandPayload, + processedAtMs, + ); + break; } case "scheduled_maintenance": { await processScheduledMaintenanceCommand( bindings, payload as ScheduledMaintenanceCommandPayload, ); - return; + break; } case "session_run_dispatch": { await processSessionRunDispatchCommand(bindings, payload as SessionRunDispatchCommandPayload); - return; + break; } } + + await requireOwnership(); + return { kind: "completed" }; } async function processClaimedApiCommandWithLeaseRenewal( bindings: ApiBindings, claim: ApiCommandClaim, - ownerId: string, - processedAtMs: number, -): Promise { + nowMs: () => number, + dispatchDeployment: typeof dispatchAppDeploymentRun, + appDeploymentDeadlineMs?: number, +): Promise>> { let stopped = false; - let renewal = Promise.resolve(); + let lossEvent: "api-command.claim_lost" | "api-command.claim_renew_failed" | null = null; + let lossLogged = false; + let pendingHeartbeat = Promise.resolve(); + const requireOwnership = createLeaseOwnershipRenewal(async () => { + try { + const renewed = await renewApiCommandClaim({ + claim, + database: bindings.DB, + nowMs: nowMs(), + }); + if (!renewed) { + lossEvent ??= "api-command.claim_lost"; + } + return renewed; + } catch (error) { + lossEvent ??= "api-command.claim_renew_failed"; + throw error; + } + }, "API command lease ownership was lost."); + const logOwnershipLoss = (error: unknown): void => { + if (lossLogged || lossEvent === null) { + return; + } + lossLogged = true; + logError(lossEvent, { + ...createErrorLogContext(error), + attemptCount: claim.attemptCount, + commandId: claim.commandId, + deliveryGeneration: claim.deliveryGeneration, + kind: claim.kind, + }); + }; const timer = setInterval(() => { if (stopped) { return; } - renewal = renewal - .then(() => - renewApiCommandClaim({ - commandId: claim.commandId, - database: bindings.DB, - ownerId, - }), - ) - .then((renewed) => { - if (renewed) { - return; - } - - stopped = true; - logError("api-command.claim_lost", { - commandId: claim.commandId, - kind: claim.kind, - }); - }) - .catch((error: unknown) => { - logError("api-command.claim_renew_failed", { - ...createErrorLogContext(error), - commandId: claim.commandId, - kind: claim.kind, - }); - }); + pendingHeartbeat = requireOwnership().catch(logOwnershipLoss); }, API_COMMAND_LEASE_RENEWAL_INTERVAL_MS); try { - await processClaimedApiCommand(bindings, claim, processedAtMs); - stopped = true; - await renewal; + await requireOwnership(); + const result = await processClaimedApiCommand( + bindings, + claim, + nowMs(), + requireOwnership, + dispatchDeployment, + appDeploymentDeadlineMs, + ); + await requireOwnership(); + return result; + } catch (error) { + logOwnershipLoss(error); + throw error; } finally { stopped = true; clearInterval(timer); + await pendingHeartbeat; } } -export async function processApiCommandMessage( - bindings: ApiBindings, - message: Message, - nowMs: () => number = currentTimestampMs, -): Promise { - let commandId: ApiCommandId; +function busyRetryDelaySeconds(claimExpiresAt: number | null, nowMs: number): number { + if (claimExpiresAt === null) { + return API_COMMAND_RETRY_DELAY_SECONDS; + } + return Math.min( + API_COMMAND_LEASE_MS / 1_000, + Math.max(1, Math.ceil((claimExpiresAt - nowMs) / 1_000)), + ); +} +function retryMessage(message: Message, delaySeconds: number): void { + message.retry({ delaySeconds }); +} + +async function handoffLegacyAppDeploymentQueueMessage( + bindings: ApiBindings, + message: ApiCommandMessage, +): Promise<"continue" | ApiCommandDeliveryDisposition> { try { - commandId = parseApiCommandMessage(message.body).commandId; + const handoff = await handoffAppDeploymentQueueMessage(bindings, message); + if (handoff === "continue") { + return "continue"; + } + return handoff === "finished" ? FINISHED_DISPOSITION : retryDisposition(); } catch (error) { - logError("api-command.message_invalid", { + logError("api-command.app_deployment_workflow_handoff_failed", { ...createErrorLogContext(error), - errorCode: getErrorCode(error), + commandId: message.commandId, + deliveryGeneration: message.deliveryGeneration, }); - message.ack(); - return; + return retryDisposition(); } +} - const ownerId = createClaimOwnerId(message); - const startMs = nowMs(); - const claim = await claimApiCommand({ - commandId, - database: bindings.DB, - nowMs: startMs, - ownerId, +async function finalizeProcessedCommand( + bindings: ApiBindings, + claim: ApiCommandClaim, + result: Awaited>, + nowMs: number, +): Promise { + if (result.kind === "completed") { + return completeApiCommand({ claim, database: bindings.DB, nowMs }); + } + if (result.outcome.kind === "skipped") { + const reconciled = await reconcileSkippedAppDeploymentCommand(bindings, claim, nowMs); + if (!reconciled) { + await releaseApiCommandForRetry({ + claim, + database: bindings.DB, + errorCode: "app_deployment_dispatch_incomplete", + errorMessage: "App deployment dispatch did not reach a terminal state.", + nowMs, + }); + } + return reconciled; + } + if (result.outcome.kind === "succeeded") { + return completeAppDeploymentCommand(bindings, claim, result.outcome, nowMs); + } + return failAppDeploymentCommand(bindings, claim, { + errorCode: result.outcome.errorCode, + errorMessage: result.outcome.errorMessage, + nowMs, + runId: result.outcome.runId, + status: "failed", }); +} - if (!claim) { - message.ack(); - return; +async function terminalizeFailedCommand( + bindings: ApiBindings, + claim: ApiCommandClaim, + input: { errorCode: string; errorMessage: string; nowMs: number }, +): Promise { + if (claim.kind !== "app_deployment_run_dispatch") { + return markApiCommandFailed({ claim, database: bindings.DB, ...input }); } - try { - await processClaimedApiCommandWithLeaseRenewal(bindings, claim, ownerId, nowMs()); - await completeApiCommand({ - commandId, - database: bindings.DB, - nowMs: nowMs(), - ownerId, - }); - message.ack(); - } catch (error) { - const errorCode = getErrorCode(error); - const errorMessage = getErrorMessage(error); + const runId = readAppDeploymentRunIdFromPayload({ + fallbackDedupeKey: claim.dedupeKey, + payloadJson: claim.payloadJson, + }); + if (runId === null) { + return markApiCommandFailed({ claim, database: bindings.DB, ...input }); + } + return failAppDeploymentCommand(bindings, claim, { + ...input, + runId, + status: "failed", + }); +} - logError("api-command.failed", { - ...createErrorLogContext(error), - attemptCount: claim.attemptCount, - commandId, - errorCode, - kind: claim.kind, - }); +async function handleClaimedCommandFailure( + bindings: ApiBindings, + claim: ApiCommandClaim, + error: unknown, + nowMs: () => number, +): Promise { + const errorCode = getErrorCode(error); + const errorMessage = getErrorMessage(error); + + logError("api-command.failed", { + ...createErrorLogContext(error), + attemptCount: claim.attemptCount, + commandId: claim.commandId, + deliveryGeneration: claim.deliveryGeneration, + errorCode, + kind: claim.kind, + }); - const appDeploymentRunHasTerminalError = - claim.kind === "app_deployment_run_dispatch" && - (error instanceof ApiCommandPayloadError || - (error instanceof Error && - (error.name === "AppDeploymentDetectionError" || - error.name === "AppDeploymentNonRetryableError"))); - const appDeploymentRunRetryExhausted = - claim.kind === "app_deployment_run_dispatch" && - !appDeploymentRunHasTerminalError && - claim.attemptCount >= APP_DEPLOYMENT_RUN_DISPATCH_MAX_ATTEMPTS; - const shouldFailAppDeploymentRun = - appDeploymentRunHasTerminalError || appDeploymentRunRetryExhausted; - - if (shouldFailAppDeploymentRun) { - const failedAtMs = nowMs(); + const appDeploymentPayloadInvalid = + claim.kind === "app_deployment_run_dispatch" && error instanceof ApiCommandPayloadError; + const appDeploymentRunRetryExhausted = + claim.kind === "app_deployment_run_dispatch" && + !appDeploymentPayloadInvalid && + claim.attemptCount >= APP_DEPLOYMENT_RUN_DISPATCH_MAX_ATTEMPTS; + + try { + if (appDeploymentPayloadInvalid || appDeploymentRunRetryExhausted) { const failureCode = appDeploymentRunRetryExhausted ? APP_DEPLOYMENT_RUN_DISPATCH_RETRY_EXHAUSTED_CODE : errorCode; @@ -424,113 +986,242 @@ export async function processApiCommandMessage( lastErrorMessage: errorMessage, }) : errorMessage; - - await failAppDeploymentRunFromPayloadJson( - bindings, - { - errorCode: failureCode, - errorMessage: failureMessage, - fallbackDedupeKey: claim.dedupeKey, - nowMs: failedAtMs, - payloadJson: claim.payloadJson, - }, - "api-command.app_deployment_run_fail_failed", - ); - - await markApiCommandFailed({ - commandId, - database: bindings.DB, + const terminalized = await terminalizeFailedCommand(bindings, claim, { errorCode: failureCode, errorMessage: failureMessage, - nowMs: failedAtMs, - ownerId, + nowMs: nowMs(), }); - message.ack(); - return; + return terminalized ? FINISHED_DISPOSITION : retryDisposition(); } if (error instanceof ApiCommandPayloadError) { - await markApiCommandFailed({ - commandId, + const terminalized = await markApiCommandFailed({ + claim, database: bindings.DB, errorCode, errorMessage, nowMs: nowMs(), - ownerId, }); - message.ack(); - return; + return terminalized ? FINISHED_DISPOSITION : retryDisposition(); } await releaseApiCommandForRetry({ - commandId, + claim, database: bindings.DB, errorCode, errorMessage, nowMs: nowMs(), - ownerId, }); - message.retry({ delaySeconds: API_COMMAND_RETRY_DELAY_SECONDS }); + } catch (persistenceError) { + logError("api-command.failure_persist_failed", { + ...createErrorLogContext(persistenceError), + commandId: claim.commandId, + deliveryGeneration: claim.deliveryGeneration, + }); + } + + return retryDisposition(); +} + +export async function processApiCommandDelivery( + bindings: ApiBindings, + body: unknown, + nowMs: () => number = currentTimestampMs, + options: ProcessApiCommandDeliveryOptions = {}, +): Promise { + let queueMessage: ApiCommandMessage; + + try { + queueMessage = parseApiCommandMessage(body); + } catch (error) { + logError("api-command.message_invalid", { + ...createErrorLogContext(error), + errorCode: getErrorCode(error), + }); + return FINISHED_DISPOSITION; + } + + const claimStartedAtMs = nowMs(); + let claimResult: Awaited>; + try { + claimResult = await claimApiCommand({ + claimOwner: createClaimOwnerId(), + commandId: queueMessage.commandId, + database: bindings.DB, + deliveryGeneration: queueMessage.deliveryGeneration, + nowMs: claimStartedAtMs, + }); + } catch (error) { + logError("api-command.claim_failed", { + ...createErrorLogContext(error), + commandId: queueMessage.commandId, + deliveryGeneration: queueMessage.deliveryGeneration, + }); + return retryDisposition(); + } + + if (claimResult.kind === "busy") { + return retryDisposition(busyRetryDelaySeconds(claimResult.claimExpiresAt, claimStartedAtMs)); + } + if (claimResult.kind !== "claimed") { + return FINISHED_DISPOSITION; + } + + const claim = claimResult.claim; + if ( + claim.kind === "app_deployment_run_dispatch" && + claim.attemptCount > APP_DEPLOYMENT_RUN_DISPATCH_MAX_ATTEMPTS + ) { + try { + const terminalized = await terminalizeFailedCommand(bindings, claim, { + errorCode: APP_DEPLOYMENT_RUN_DISPATCH_RETRY_EXHAUSTED_CODE, + errorMessage: createAppDeploymentDispatchRetryExhaustedMessage({ + attemptCount: APP_DEPLOYMENT_RUN_DISPATCH_MAX_ATTEMPTS, + lastErrorMessage: claim.lastErrorMessage, + }), + nowMs: nowMs(), + }); + return terminalized ? FINISHED_DISPOSITION : retryDisposition(); + } catch (error) { + logError("api-command.retry_exhaustion_persist_failed", { + ...createErrorLogContext(error), + attemptCount: claim.attemptCount, + commandId: claim.commandId, + deliveryGeneration: claim.deliveryGeneration, + }); + return retryDisposition(); + } + } + + let result: Awaited>; + try { + result = await processClaimedApiCommandWithLeaseRenewal( + bindings, + claim, + nowMs, + options.dispatchAppDeploymentRun ?? dispatchAppDeploymentRun, + options.appDeploymentDeadlineMs, + ); + } catch (error) { + return handleClaimedCommandFailure(bindings, claim, error, nowMs); + } + + try { + const finalized = await finalizeProcessedCommand(bindings, claim, result, nowMs()); + return finalized ? FINISHED_DISPOSITION : retryDisposition(); + } catch (error) { + logError("api-command.completion_failed", { + ...createErrorLogContext(error), + commandId: claim.commandId, + deliveryGeneration: claim.deliveryGeneration, + }); + return retryDisposition(); } } +export async function processApiCommandMessage( + bindings: ApiBindings, + message: Message, + nowMs: () => number = currentTimestampMs, + options: ProcessApiCommandDeliveryOptions = {}, +): Promise { + let queueMessage: ApiCommandMessage; + try { + queueMessage = parseApiCommandMessage(message.body); + } catch (error) { + logError("api-command.message_invalid", { + ...createErrorLogContext(error), + errorCode: getErrorCode(error), + }); + message.ack(); + return; + } + const handoff = await handoffLegacyAppDeploymentQueueMessage(bindings, queueMessage); + const disposition = + handoff === "continue" + ? await processApiCommandDelivery(bindings, queueMessage, nowMs, options) + : handoff; + if (disposition.kind === "finished") { + message.ack(); + return; + } + retryMessage(message, disposition.delaySeconds); +} + export async function processApiCommandDeadLetterMessage( bindings: ApiBindings, message: Message, nowMs: () => number = currentTimestampMs, ): Promise { + let queueMessage: ApiCommandMessage; try { - const { commandId } = parseApiCommandMessage(message.body); - const deadLetteredAtMs = nowMs(); - const command = - (await getAppDatabase(bindings.DB) - .select({ - dedupeKey: apiCommandsTable.dedupeKey, - kind: apiCommandsTable.kind, - lastErrorCode: apiCommandsTable.lastErrorCode, - lastErrorMessage: apiCommandsTable.lastErrorMessage, - payloadJson: apiCommandsTable.payloadJson, - }) - .from(apiCommandsTable) - .where(eq(apiCommandsTable.id, commandId)) - .limit(1) - .get()) ?? null; + queueMessage = parseApiCommandMessage(message.body); + } catch (error) { + logError("api-command.dead_letter_invalid", { + ...createErrorLogContext(error), + errorCode: getErrorCode(error), + }); + message.ack(); + return; + } - if (command?.kind === "app_deployment_run_dispatch") { - await failAppDeploymentRunFromPayloadJson( - bindings, - { - errorCode: "queue_dead_lettered", - errorMessage: "Deployment dispatch reached the queue dead-letter consumer.", - fallbackDedupeKey: command.dedupeKey, - nowMs: deadLetteredAtMs, - payloadJson: command.payloadJson, - }, - "api-command.app_deployment_run_dead_letter_fail_failed", - ); + const handoff = await handoffLegacyAppDeploymentQueueMessage(bindings, queueMessage); + if (handoff !== "continue") { + if (handoff.kind === "finished") { + message.ack(); + } else { + retryMessage(message, handoff.delaySeconds); } + return; + } - const preserveArtifactFailure = command?.kind === "environment_package_artifact_build"; + const claimStartedAtMs = nowMs(); + try { + const claimResult = await claimApiCommand({ + claimOwner: createClaimOwnerId(), + commandId: queueMessage.commandId, + database: bindings.DB, + deliveryGeneration: queueMessage.deliveryGeneration, + nowMs: claimStartedAtMs, + }); + if (claimResult.kind === "busy") { + retryMessage(message, busyRetryDelaySeconds(claimResult.claimExpiresAt, claimStartedAtMs)); + return; + } + if (claimResult.kind !== "claimed") { + message.ack(); + return; + } - await markApiCommandDeadLettered({ - commandId, + const claim = claimResult.claim; + const preserveArtifactFailure = claim.kind === "environment_package_artifact_build"; + const errorCode = + preserveArtifactFailure && claimResult.claim.lastErrorCode + ? claimResult.claim.lastErrorCode + : "queue_dead_lettered"; + const errorMessage = + preserveArtifactFailure && claimResult.claim.lastErrorMessage + ? claimResult.claim.lastErrorMessage + : "API command reached the queue dead-letter consumer."; + const deadLettered = await markApiCommandDeadLettered({ + claim, database: bindings.DB, - errorCode: - preserveArtifactFailure && command.lastErrorCode - ? command.lastErrorCode - : "queue_dead_lettered", - errorMessage: - preserveArtifactFailure && command.lastErrorMessage - ? command.lastErrorMessage - : "API command reached the queue dead-letter consumer.", - nowMs: deadLetteredAtMs, + errorCode, + errorMessage, + nowMs: nowMs(), }); + + if (deadLettered) { + message.ack(); + } else { + retryMessage(message, API_COMMAND_RETRY_DELAY_SECONDS); + } } catch (error) { - logError("api-command.dead_letter_invalid", { + logError("api-command.dead_letter_failed", { ...createErrorLogContext(error), - errorCode: getErrorCode(error), + commandId: queueMessage.commandId, + deliveryGeneration: queueMessage.deliveryGeneration, }); + retryMessage(message, API_COMMAND_RETRY_DELAY_SECONDS); } - - message.ack(); } diff --git a/apps/api/src/modules/api-command/application/api-command-workflow.ts b/apps/api/src/modules/api-command/application/api-command-workflow.ts new file mode 100644 index 00000000..3ba52f15 --- /dev/null +++ b/apps/api/src/modules/api-command/application/api-command-workflow.ts @@ -0,0 +1,43 @@ +import type { WorkflowStep } from "cloudflare:workers"; + +import type { ApiBindings } from "../../../platform/cloudflare/worker-types"; +import { currentTimestampMs } from "../../../time"; +import type { ApiCommandMessage } from "./api-command-message"; +import { processApiCommandDelivery } from "./api-command-processor"; +import type { ProcessApiCommandDeliveryOptions } from "./api-command-processor"; + +export const APP_DEPLOYMENT_WORKFLOW_EXECUTION_BUDGET_MS = 29 * 60 * 1_000; + +export class ApiCommandWorkflowRetryError extends Error { + constructor(delaySeconds: number) { + super(`API command delivery requested retry after ${delaySeconds} seconds.`); + this.name = "ApiCommandWorkflowRetryError"; + } +} + +export async function runAppDeploymentWorkflow( + bindings: ApiBindings, + message: ApiCommandMessage, + step: WorkflowStep, + nowMs: () => number = currentTimestampMs, + options: ProcessApiCommandDeliveryOptions = {}, +): Promise { + await step.do( + "dispatch", + { + retries: { backoff: "constant", delay: "30 seconds", limit: 1_000 }, + timeout: "30 minutes", + }, + async () => { + const deadlineMs = nowMs() + APP_DEPLOYMENT_WORKFLOW_EXECUTION_BUDGET_MS; + const disposition = await processApiCommandDelivery(bindings, message, nowMs, { + ...options, + appDeploymentDeadlineMs: deadlineMs, + }); + if (disposition.kind === "retry") { + throw new ApiCommandWorkflowRetryError(disposition.delaySeconds); + } + return null; + }, + ); +} diff --git a/apps/api/src/modules/apps/application/app-deployment-cloudflare-client.ts b/apps/api/src/modules/apps/application/app-deployment-cloudflare-client.ts index 87d6670b..d0392fb0 100644 --- a/apps/api/src/modules/apps/application/app-deployment-cloudflare-client.ts +++ b/apps/api/src/modules/apps/application/app-deployment-cloudflare-client.ts @@ -1,28 +1,15 @@ import Cloudflare from "cloudflare"; -import { createErrorLogContext, logError } from "../../../platform/cloudflare/logger"; import type { ApiBindings } from "../../../platform/cloudflare/worker-types"; -type CloudflareWorkerVersion = Awaited< - ReturnType ->; - -export interface CloudflarePagesProjectInput { - branch: string; - projectName: string; -} - -export interface CloudflarePagesDomainInput { - hostname: string; - projectName: string; -} - export interface CloudflareWorkerModuleInput { compatibilityDate: string; mainModuleName: string; scriptContent: string; scriptName: string; - /** Plain-text env vars injected into the Worker (e.g. agent thread URLs). */ + tags: string[]; + timeoutMs?: number; + /** Bearer capability URLs injected as Worker secrets. */ vars: Record; } @@ -31,376 +18,378 @@ export interface CloudflareWorkerDeploymentResult { versionId: string | null; } -export interface CloudflarePagesDomainResult { - status: string | null; +export interface CloudflareStaticAssetManifestEntry { + hash: string; + size: number; } -export type CloudflareDeploymentResourceTargetKind = - | "cloudflare_pages" - | "cloudflare_pages_domain" - | "cloudflare_worker" - | "cloudflare_worker_domain" - | "cloudflare_worker_route"; - -export interface CloudflareDeploymentResourceDeleteFailure { - error: unknown; - resourceName: string; - targetKind: CloudflareDeploymentResourceTargetKind; +export type CloudflareStaticAssetManifest = Record; + +export interface CloudflareStaticAssetsUploadSession { + buckets: string[][]; + uploadToken: string; } export interface CloudflareDeploymentClient { - deletePagesDomain(input: CloudflarePagesDomainInput): Promise; - deletePagesProject(input: { projectName: string }): Promise; - deleteWorkerDomain(input: { hostname: string }): Promise; - deleteWorkerRoute(input: { hostname: string }): Promise; - deleteWorkerScript(input: { scriptName: string }): Promise; + createStaticAssetsUploadSession(input: { + manifest: CloudflareStaticAssetManifest; + scriptName: string; + timeoutMs?: number; + }): Promise; + deleteWorkerScript(input: { scriptName: string; timeoutMs?: number }): Promise; + deployStaticAssets(input: { + compatibilityDate: string; + completionToken: string; + headers: string | null; + redirects: string | null; + scriptName: string; + tags: string[]; + timeoutMs?: number; + }): Promise; deployWorkerModule(input: CloudflareWorkerModuleInput): Promise; - ensurePagesDomain(input: CloudflarePagesDomainInput): Promise; - ensurePagesProject(input: CloudflarePagesProjectInput): Promise<{ projectId: string | null }>; - ensureWorkerDomain(input: { hostname: string; scriptName: string }): Promise; - ensureWorkerRoute(input: { hostname: string; scriptName: string }): Promise; - getLatestPagesDeployment(input: { - projectName: string; - }): Promise<{ deploymentId: string | null; url: string | null }>; } export type CloudflareClientBindings = Pick< ApiBindings, - "CLOUDFLARE_ACCOUNT_ID" | "CLOUDFLARE_API_TOKEN" | "CLOUDFLARE_ZONE_ID" + "CLOUDFLARE_ACCOUNT_ID" | "CLOUDFLARE_API_TOKEN" >; -function toStatus(error: unknown, status: number): boolean { - return ( - typeof error === "object" && - error !== null && - "status" in error && - Reflect.get(error, "status") === status - ); +interface WorkerModuleUpload { + files: File[]; + metadata: { + bindings: Array<{ name: string; text: string; type: "secret_text" }>; + compatibility_date: string; + main_module: string; + tags: string[]; + }; } -export function logCloudflareDeploymentResourceDeleteFailures( - eventName: string, - failures: readonly CloudflareDeploymentResourceDeleteFailure[], -): void { - for (const failure of failures) { - logError(eventName, { - ...createErrorLogContext(failure.error), - resourceName: failure.resourceName, - targetKind: failure.targetKind, - }); - } +interface CloudflareScriptUploadResponse { + etag?: string; + id?: string; + startup_time_ms: number; + tag?: string; } -export function createCloudflareDeploymentClient( - bindings: CloudflareClientBindings, -): CloudflareDeploymentClient { - const client = new Cloudflare({ apiToken: bindings.CLOUDFLARE_API_TOKEN }); - const accountId = bindings.CLOUDFLARE_ACCOUNT_ID; - const zoneId = bindings.CLOUDFLARE_ZONE_ID; +interface CloudflareApiEnvelope { + result?: T; + success?: boolean; +} - return { - async deletePagesDomain(input) { - try { - await client.pages.projects.domains.delete(input.hostname, { - account_id: accountId, - project_name: input.projectName, - }); - } catch (error) { - if (!toStatus(error, 404)) { - throw error; - } - } - }, - async deletePagesProject(input) { - try { - await client.pages.projects.delete(input.projectName, { account_id: accountId }); - } catch (error) { - if (!toStatus(error, 404)) { - throw error; - } - } - }, - async deleteWorkerDomain(input) { - const domain = await findWorkerDomain(client, accountId, input.hostname); +const CLOUDFLARE_API_BASE_URL = "https://api.cloudflare.com/client/v4"; +const CLOUDFLARE_APP_DEPLOYMENT_REQUEST_TIMEOUT_MS = 10 * 60 * 1000; +const STATIC_ASSET_HASH_PATTERN = /^[a-f0-9]{32}$/u; - if (domain?.id === undefined) { - return; - } +function requiredStringBinding(bindings: CloudflareClientBindings, name: string): string { + const value = Reflect.get(bindings, name); - await client.workers.domains.delete(domain.id, { account_id: accountId }); - }, - async deleteWorkerRoute(input) { - const pattern = workerRoutePattern(input.hostname); - const route = await findWorkerRoute(client, zoneId, pattern); + if (typeof value !== "string" || value.trim().length === 0) { + throw new Error(`${name} is required for App deployment.`); + } - if (route?.id === undefined) { - return; - } + return value; +} - await client.workers.routes.delete(route.id, { zone_id: zoneId }); - }, - async deleteWorkerScript(input) { - try { - await client.workers.scripts.delete(input.scriptName, { account_id: accountId }); - } catch (error) { - if (!toStatus(error, 404)) { - throw error; - } - } - }, - async deployWorkerModule(input) { - const scriptPath = `/accounts/${accountId}/workers/scripts/${encodeURIComponent(input.scriptName)}`; - const createVersion = async (): Promise => - ( - await client.post<{ result: CloudflareWorkerVersion }>(`${scriptPath}/versions`, { - body: createWorkerModuleUpload(input), - }) - ).result; - let version; +function requestTimeout(timeoutMs: number | undefined): number { + if (timeoutMs === undefined) { + return CLOUDFLARE_APP_DEPLOYMENT_REQUEST_TIMEOUT_MS; + } - try { - version = await createVersion(); - } catch (error) { - if (!toCloudflareCode(error, 10007)) { - throw error; - } + if (!Number.isSafeInteger(timeoutMs) || timeoutMs <= 0) { + throw new Error("Cloudflare App deployment request timeout must be a positive integer."); + } - await client.put(scriptPath, { - body: createWorkerModuleUpload(input), - }); - version = await createVersion(); - } - const versionId = version.id ?? null; + return Math.min(timeoutMs, CLOUDFLARE_APP_DEPLOYMENT_REQUEST_TIMEOUT_MS); +} - if (versionId === null) { - throw new Error("Cloudflare Worker version response did not include an id."); - } +function remainingRequestTimeout(deadlineMs: number): number { + const remaining = Math.floor(deadlineMs - Date.now()); - const deployment = await client.workers.scripts.deployments.create(input.scriptName, { - account_id: accountId, - strategy: "percentage", - versions: [{ percentage: 100, version_id: versionId }], - }); + if (remaining <= 0) { + throw new Error("Cloudflare App deployment request exceeded its execution budget."); + } - return { - deploymentId: deployment.id ?? null, - versionId, - }; - }, - async ensurePagesDomain(input) { - try { - const domain = await client.pages.projects.domains.create(input.projectName, { - account_id: accountId, - name: input.hostname, - }); + return remaining; +} - return { status: domain.status ?? null }; - } catch (error) { - if (!toStatus(error, 409)) { - throw error; - } +function toStatus(error: unknown, status: number): boolean { + return ( + typeof error === "object" && + error !== null && + "status" in error && + Reflect.get(error, "status") === status + ); +} - const domain = await client.pages.projects.domains.get(input.hostname, { - account_id: accountId, - project_name: input.projectName, - }); +export function validateStaticAssetsManifest(manifest: CloudflareStaticAssetManifest): Set { + const hashes = new Set(); - return { status: domain.status ?? null }; - } - }, - async ensurePagesProject(input) { - try { - const project = await client.pages.projects.create({ - account_id: accountId, - name: input.projectName, - production_branch: input.branch, - }); + for (const [path, entry] of Object.entries(manifest)) { + const parts = path.slice(1).split("/"); - return { projectId: project.id ?? null }; - } catch (error) { - if (!toStatus(error, 409)) { - throw error; - } + if ( + !path.startsWith("/") || + path.includes("\0") || + path.includes("\\") || + parts.some((part) => part.length === 0 || part === "." || part === "..") + ) { + throw new Error("Cloudflare Static Assets manifest contains an invalid path."); + } - const project = await client.pages.projects.get(input.projectName, { - account_id: accountId, - }); + if ( + !STATIC_ASSET_HASH_PATTERN.test(entry.hash) || + !Number.isSafeInteger(entry.size) || + entry.size < 0 + ) { + throw new Error(`Cloudflare Static Assets manifest entry is invalid for ${path}.`); + } - return { projectId: project.id ?? null }; - } - }, - async ensureWorkerDomain(input) { - const existingDomain = await findWorkerDomain(client, accountId, input.hostname); + hashes.add(entry.hash); + } - if (existingDomain !== null && existingDomain.service === input.scriptName) { - return; - } + return hashes; +} - await client.workers.domains.update({ - account_id: accountId, - hostname: input.hostname, - service: input.scriptName, - zone_id: zoneId, - }); - }, - async ensureWorkerRoute(input) { - const pattern = workerRoutePattern(input.hostname); - const existingRoute = await findWorkerRoute(client, zoneId, pattern); - - if (existingRoute !== null) { - if (existingRoute.script !== input.scriptName && existingRoute.id !== undefined) { - await client.workers.routes.update(existingRoute.id, { - pattern, - script: input.scriptName, - zone_id: zoneId, - }); - } +function normalizeAssetUploadSession( + response: { buckets?: string[][]; jwt?: string }, + manifestHashes: ReadonlySet, +): CloudflareStaticAssetsUploadSession { + if (typeof response.jwt !== "string" || response.jwt.length === 0) { + throw new Error("Cloudflare did not return a Static Assets upload token."); + } - return; - } + const buckets = response.buckets ?? []; + const requested = new Set(); - await client.workers.routes.create({ - pattern, - script: input.scriptName, - zone_id: zoneId, - }); - }, - async getLatestPagesDeployment(input) { - const deployments = client.pages.projects.deployments.list(input.projectName, { - account_id: accountId, - per_page: 1, - }); + for (const bucket of buckets) { + if (!Array.isArray(bucket) || bucket.length === 0) { + throw new Error("Cloudflare returned an invalid Static Assets upload bucket."); + } - for await (const deployment of deployments) { - return { deploymentId: deployment.id ?? null, url: deployment.url ?? null }; + for (const hash of bucket) { + if (typeof hash !== "string" || !manifestHashes.has(hash) || requested.has(hash)) { + throw new Error("Cloudflare requested an invalid Static Assets upload hash."); } - return { deploymentId: null, url: null }; - }, - }; -} - -export async function deleteCloudflareDeploymentResources( - cloudflareClient: CloudflareDeploymentClient, - input: { hostname: string; resourceName: string }, -): Promise { - const failures = await Promise.all([ - deleteCloudflareDeploymentResource("cloudflare_pages_domain", input.resourceName, () => - cloudflareClient.deletePagesDomain({ - hostname: input.hostname, - projectName: input.resourceName, - }), - ), - deleteCloudflareDeploymentResource("cloudflare_pages", input.resourceName, () => - cloudflareClient.deletePagesProject({ projectName: input.resourceName }), - ), - deleteCloudflareDeploymentResource("cloudflare_worker_domain", input.hostname, () => - cloudflareClient.deleteWorkerDomain({ hostname: input.hostname }), - ), - deleteCloudflareDeploymentResource("cloudflare_worker_route", input.resourceName, () => - cloudflareClient.deleteWorkerRoute({ hostname: input.hostname }), - ), - deleteCloudflareDeploymentResource("cloudflare_worker", input.resourceName, () => - cloudflareClient.deleteWorkerScript({ scriptName: input.resourceName }), - ), - ]); - - return failures.filter( - (failure): failure is CloudflareDeploymentResourceDeleteFailure => failure !== null, - ); -} - -async function deleteCloudflareDeploymentResource( - targetKind: CloudflareDeploymentResourceTargetKind, - resourceName: string, - runDelete: () => Promise, -): Promise { - try { - await runDelete(); - return null; - } catch (error) { - return { error, resourceName, targetKind }; + requested.add(hash); + } } -} -function workerRoutePattern(hostname: string): string { - return `${hostname}/*`; + return { buckets, uploadToken: response.jwt }; } -export function createWorkerModuleUpload(input: CloudflareWorkerModuleInput): FormData { - const upload = new FormData(); - const file = new File([input.scriptContent], input.mainModuleName, { - type: "application/javascript+module", - }); - const metadata = { - bindings: Object.entries(input.vars).map(([name, text]) => ({ - name, - text, - type: "plain_text" as const, - })), - compatibility_date: input.compatibilityDate, - main_module: input.mainModuleName, - }; - - upload.append("metadata", JSON.stringify(metadata)); - upload.append(input.mainModuleName, file); - - return upload; +function staticAssetsScriptUrl(input: { + accountId: string; + dispatchNamespace: string; + scriptName: string; +}): string { + return `${CLOUDFLARE_API_BASE_URL}/accounts/${encodeURIComponent( + input.accountId, + )}/workers/dispatch/namespaces/${encodeURIComponent( + input.dispatchNamespace, + )}/scripts/${encodeURIComponent(input.scriptName)}`; } -function toCloudflareCode(error: unknown, code: number): boolean { - if (typeof error !== "object" || error === null) { - return false; +async function readCloudflareApiResult(response: Response, operation: string): Promise { + let envelope: CloudflareApiEnvelope; + + try { + envelope = (await response.json()) as CloudflareApiEnvelope; + } catch { + throw new Error(`Cloudflare ${operation} returned a non-JSON response (${response.status}).`); } - if ("code" in error && Reflect.get(error, "code") === code) { - return true; + if (!response.ok || envelope.success !== true || envelope.result === undefined) { + throw new Error(`Cloudflare ${operation} failed (${response.status}).`); } - const cause = Reflect.get(error, "error"); + return envelope.result; +} - if (typeof cause === "object" && cause !== null && Reflect.get(cause, "code") === code) { - return true; +export function createStaticAssetsUploadForm(input: { + compatibilityDate: string; + completionToken: string; + headers: string | null; + redirects: string | null; + tags: string[]; +}): FormData { + const config: { _headers?: string; _redirects?: string } = {}; + + if (input.headers !== null) { + config["_headers"] = input.headers; } - const errors = Reflect.get(error, "errors"); + if (input.redirects !== null) { + config["_redirects"] = input.redirects; + } - return ( - Array.isArray(errors) && - errors.some( - (entry) => typeof entry === "object" && entry !== null && Reflect.get(entry, "code") === code, - ) + const form = new FormData(); + form.set( + "metadata", + JSON.stringify({ + assets: { + config, + jwt: input.completionToken, + }, + compatibility_date: input.compatibilityDate, + tags: input.tags, + }), ); + return form; } -async function findWorkerDomain( - client: Cloudflare, - accountId: string, - hostname: string, -): Promise<{ id?: string; service?: string } | null> { - const domains = client.workers.domains.list({ account_id: accountId }); +export function createCloudflareDeploymentClient( + bindings: CloudflareClientBindings, +): CloudflareDeploymentClient { + const apiToken = requiredStringBinding(bindings, "CLOUDFLARE_API_TOKEN"); + const client = new Cloudflare({ + apiToken, + timeout: CLOUDFLARE_APP_DEPLOYMENT_REQUEST_TIMEOUT_MS, + }); + const accountId = requiredStringBinding(bindings, "CLOUDFLARE_ACCOUNT_ID"); + const dispatchNamespace = requiredStringBinding(bindings, "MOSOO_APP_DISPATCH_NAMESPACE"); + + async function verifyWorkerScriptTags( + scriptName: string, + expectedTags: readonly string[], + deadlineMs: number, + ): Promise { + const actualTags: string[] = []; + + for await (const tag of client.workersForPlatforms.dispatch.namespaces.scripts.tags.list( + scriptName, + { + account_id: accountId, + dispatch_namespace: dispatchNamespace, + }, + { timeout: remainingRequestTimeout(deadlineMs) }, + )) { + actualTags.push(tag); + } - for await (const domain of domains) { - if (domain.hostname === hostname) { - return domain; + if (!sameStringSet(actualTags, expectedTags)) { + throw new Error(`Cloudflare did not retain the expected tags for ${scriptName}.`); } } - return null; + return { + async createStaticAssetsUploadSession(input) { + const manifestHashes = validateStaticAssetsManifest(input.manifest); + const response = + await client.workersForPlatforms.dispatch.namespaces.scripts.assetUpload.create( + input.scriptName, + { + account_id: accountId, + dispatch_namespace: dispatchNamespace, + manifest: input.manifest, + }, + { timeout: requestTimeout(input.timeoutMs) }, + ); + + return normalizeAssetUploadSession(response, manifestHashes); + }, + async deleteWorkerScript(input) { + try { + await client.workersForPlatforms.dispatch.namespaces.scripts.delete( + input.scriptName, + { + account_id: accountId, + dispatch_namespace: dispatchNamespace, + }, + { timeout: requestTimeout(input.timeoutMs) }, + ); + } catch (error) { + if (!toStatus(error, 404)) { + throw error; + } + } + }, + async deployStaticAssets(input) { + const timeoutMs = requestTimeout(input.timeoutMs); + const deadlineMs = Date.now() + timeoutMs; + const response = await fetch( + staticAssetsScriptUrl({ accountId, dispatchNamespace, scriptName: input.scriptName }), + { + body: createStaticAssetsUploadForm(input), + headers: { Authorization: `Bearer ${apiToken}` }, + method: "PUT", + signal: AbortSignal.timeout(remainingRequestTimeout(deadlineMs)), + }, + ); + const worker = await readCloudflareApiResult( + response, + "Static Assets deployment", + ); + + if (worker.id !== undefined && worker.id !== input.scriptName) { + throw new Error( + `Cloudflare uploaded App deployment as ${worker.id}, expected ${input.scriptName}.`, + ); + } + + await verifyWorkerScriptTags(input.scriptName, input.tags, deadlineMs); + + return { + deploymentId: worker.tag ?? null, + versionId: worker.etag ?? null, + }; + }, + async deployWorkerModule(input) { + const timeoutMs = requestTimeout(input.timeoutMs); + const deadlineMs = Date.now() + timeoutMs; + const upload = createWorkerModuleUpload(input); + const worker = await client.workersForPlatforms.dispatch.namespaces.scripts.update( + input.scriptName, + { + account_id: accountId, + dispatch_namespace: dispatchNamespace, + ...upload, + }, + { timeout: remainingRequestTimeout(deadlineMs) }, + ); + + if (worker.id !== undefined && worker.id !== input.scriptName) { + throw new Error( + `Cloudflare uploaded App deployment as ${worker.id}, expected ${input.scriptName}.`, + ); + } + + await verifyWorkerScriptTags(input.scriptName, input.tags, deadlineMs); + + return { + deploymentId: worker.tag ?? null, + versionId: worker.etag ?? null, + }; + }, + }; } -async function findWorkerRoute( - client: Cloudflare, - zoneId: string, - pattern: string, -): Promise<{ id?: string; script?: string } | null> { - const routes = client.workers.routes.list({ zone_id: zoneId }); +function sameStringSet(left: readonly string[], right: readonly string[]): boolean { + const sortedLeft = left.toSorted(); + const sortedRight = right.toSorted(); - for await (const route of routes) { - if (route.pattern === pattern) { - return route; - } - } + return ( + sortedLeft.length === sortedRight.length && + sortedLeft.every((value, index) => value === sortedRight[index]) + ); +} - return null; +export function createWorkerModuleUpload(input: CloudflareWorkerModuleInput): WorkerModuleUpload { + return { + files: [ + new File([input.scriptContent], input.mainModuleName, { + type: "application/javascript+module", + }), + ], + metadata: { + bindings: Object.entries(input.vars).map(([name, text]) => ({ + name, + text, + type: "secret_text", + })), + compatibility_date: input.compatibilityDate, + main_module: input.mainModuleName, + tags: input.tags, + }, + }; } diff --git a/apps/api/src/modules/apps/application/app-deployment-detector.ts b/apps/api/src/modules/apps/application/app-deployment-detector.ts index 80de1a29..bbfae33b 100644 --- a/apps/api/src/modules/apps/application/app-deployment-detector.ts +++ b/apps/api/src/modules/apps/application/app-deployment-detector.ts @@ -1,10 +1,10 @@ -import type { AppDeploymentTargetKind } from "@mosoo/db"; import type { ParseError } from "jsonc-parser"; import { parse as parseJsonc } from "jsonc-parser"; import { parse as parseToml, stringify } from "smol-toml"; export type AppDeploymentPackageManager = "bun" | "none" | "npm" | "pnpm" | "yarn"; -export type AppDeploymentTargetMode = "static_assets" | "worker_module" | "worker_with_assets"; +export type AppDeploymentTargetKind = "cloudflare_static_assets" | "cloudflare_worker"; +export type AppDeploymentTargetMode = "static_assets" | "worker_module"; export type AppDeploymentDetectionErrorCode = | "deployment_config_required" | "deployment_shape_unsupported"; @@ -35,10 +35,6 @@ export interface AppDeploymentRepositorySnapshot { files: Readonly>; } -export interface AppDeploymentDetectionOptions { - resourceName: string; -} - interface PackageJson { dependencies: Readonly>; devDependencies: Readonly>; @@ -62,6 +58,7 @@ interface MosooConfig { interface RepositoryFiles { has(path: string): boolean; + paths(): readonly string[]; read(path: string): string | null; } @@ -80,24 +77,18 @@ export class AppDeploymentDetectionError extends Error { export function detectAppDeploymentPlan( snapshot: AppDeploymentRepositorySnapshot, - options: AppDeploymentDetectionOptions, ): AppDeploymentPlan { const files = createRepositoryFiles(snapshot.files); const mosooConfig = files.read(".mosoo.toml"); - const resourceName = normalizeResourceName(options.resourceName); if (mosooConfig !== null) { - return detectFromMosooConfig(files, mosooConfig, resourceName); + return detectFromMosooConfig(files, mosooConfig); } - return detectFromRepository(files, ".", resourceName); + return detectFromRepository(files, "."); } -function detectFromMosooConfig( - files: RepositoryFiles, - source: string, - resourceName: string, -): AppDeploymentPlan { +function detectFromMosooConfig(files: RepositoryFiles, source: string): AppDeploymentPlan { const config = parseMosooConfig(source); const packageJson = readPackageJson(files, config.rootDir); const packageManager = detectPackageManager(files, config.rootDir, packageJson); @@ -117,14 +108,15 @@ function detectFromMosooConfig( config.outputDir ?? fail("deployment_config_required", "static deployment requires build.output"); - return pagesPlan({ + assertStaticAssetsCompatible(files, config.rootDir); + + return staticAssetsPlan({ agentBindings: config.agents, buildCommand, installCommand, mosooConfigPath: ".mosoo.toml", outputDir, packageManager, - resourceName, routesFallback: config.routesFallback, rootDir: config.rootDir, }); @@ -148,17 +140,12 @@ function detectFromMosooConfig( installCommand, mosooConfigPath: ".mosoo.toml", packageManager, - resourceName, rootDir: config.rootDir, workerEntry, }); } -function detectFromRepository( - files: RepositoryFiles, - rootDir: string, - resourceName: string, -): AppDeploymentPlan { +function detectFromRepository(files: RepositoryFiles, rootDir: string): AppDeploymentPlan { const packageJson = readPackageJson(files, rootDir); const packageManager = detectPackageManager(files, rootDir, packageJson); const wranglerMain = readWranglerMain(files, rootDir); @@ -170,7 +157,6 @@ function detectFromRepository( installCommand: installCommandFor(packageManager, files, rootDir), mosooConfigPath: null, packageManager, - resourceName, rootDir, workerEntry: wranglerMain, }); @@ -178,14 +164,15 @@ function detectFromRepository( if (packageJson === null) { if (files.has("index.html")) { - return pagesPlan({ + assertStaticAssetsCompatible(files, rootDir); + + return staticAssetsPlan({ agentBindings: [], buildCommand: null, installCommand: null, mosooConfigPath: null, outputDir: ".", packageManager: "none", - resourceName, routesFallback: null, rootDir, }); @@ -198,20 +185,20 @@ function detectFromRepository( } if (hasDependency(packageJson, "vite")) { - return packagePagesPlan(files, rootDir, packageJson, packageManager, "dist", resourceName); + return packageStaticAssetsPlan(files, rootDir, packageJson, packageManager, "dist"); } if (hasDependency(packageJson, "astro")) { - return packagePagesPlan(files, rootDir, packageJson, packageManager, "dist", resourceName); + return packageStaticAssetsPlan(files, rootDir, packageJson, packageManager, "dist"); } if (hasDependency(packageJson, "@docusaurus/core")) { - return packagePagesPlan(files, rootDir, packageJson, packageManager, "build", resourceName); + return packageStaticAssetsPlan(files, rootDir, packageJson, packageManager, "build"); } if (hasDependency(packageJson, "next")) { if (isNextStaticExport(files, rootDir, packageJson)) { - return packagePagesPlan(files, rootDir, packageJson, packageManager, "out", resourceName); + return packageStaticAssetsPlan(files, rootDir, packageJson, packageManager, "out"); } throw new AppDeploymentDetectionError( @@ -221,14 +208,15 @@ function detectFromRepository( } if (files.has(pathInRoot(rootDir, "index.html")) && packageJson.scripts["build"] === undefined) { - return pagesPlan({ + assertStaticAssetsCompatible(files, rootDir); + + return staticAssetsPlan({ agentBindings: [], buildCommand: null, installCommand: null, mosooConfigPath: null, outputDir: ".", packageManager: "none", - resourceName, routesFallback: null, rootDir, }); @@ -240,39 +228,38 @@ function detectFromRepository( ); } -function packagePagesPlan( +function packageStaticAssetsPlan( files: RepositoryFiles, rootDir: string, packageJson: PackageJson, packageManager: AppDeploymentPackageManager, outputDir: string, - resourceName: string, ): AppDeploymentPlan { const buildCommand = buildCommandFor(packageManager, packageJson) ?? fail("deployment_config_required", "static framework deployment requires scripts.build"); - return pagesPlan({ + assertStaticAssetsCompatible(files, rootDir); + + return staticAssetsPlan({ agentBindings: [], buildCommand, installCommand: installCommandFor(packageManager, files, rootDir), mosooConfigPath: null, outputDir, packageManager, - resourceName, routesFallback: null, rootDir, }); } -function pagesPlan(input: { +function staticAssetsPlan(input: { agentBindings: AppDeploymentAgentBinding[]; buildCommand: string | null; installCommand: string | null; mosooConfigPath: ".mosoo.toml" | null; outputDir: string; packageManager: AppDeploymentPackageManager; - resourceName: string; routesFallback: string | null; rootDir: string; }): AppDeploymentPlan { @@ -281,8 +268,7 @@ function pagesPlan(input: { buildCommand: input.buildCommand, generatedWranglerConfig: stringify({ compatibility_date: APP_DEPLOYMENT_COMPATIBILITY_DATE, - name: input.resourceName, - pages_build_output_dir: input.outputDir, + assets: { directory: "artifact" }, }), installCommand: input.installCommand, mosooConfigPath: input.mosooConfigPath, @@ -290,7 +276,7 @@ function pagesPlan(input: { packageManager: input.packageManager, routesFallback: input.routesFallback, rootDir: input.rootDir, - targetKind: "cloudflare_pages", + targetKind: "cloudflare_static_assets", targetMode: "static_assets", warnings: [], workerEntry: null, @@ -303,7 +289,6 @@ function workerPlan(input: { installCommand: string | null; mosooConfigPath: ".mosoo.toml" | null; packageManager: AppDeploymentPackageManager; - resourceName: string; rootDir: string; workerEntry: string; }): AppDeploymentPlan { @@ -320,7 +305,6 @@ function workerPlan(input: { generatedWranglerConfig: stringify({ compatibility_date: APP_DEPLOYMENT_COMPATIBILITY_DATE, main: input.workerEntry, - name: input.resourceName, }), installCommand: input.installCommand, mosooConfigPath: input.mosooConfigPath, @@ -346,12 +330,38 @@ function createRepositoryFiles(files: Readonly>): Reposit has(path) { return normalized.has(normalizePath(path)); }, + paths() { + return [...normalized.keys()]; + }, read(path) { return normalized.get(normalizePath(path)) ?? null; }, }; } +function assertStaticAssetsCompatible(files: RepositoryFiles, rootDir: string): void { + const prefix = rootDir === "." ? "" : `${rootDir}/`; + const unsupportedPath = files.paths().find((path) => { + if (!path.startsWith(prefix)) { + return false; + } + + const relativePath = path.slice(prefix.length); + return ( + relativePath === "_routes.json" || + relativePath === "_worker.js" || + relativePath.startsWith("functions/") + ); + }); + + if (unsupportedPath !== undefined) { + throw new AppDeploymentDetectionError( + "deployment_shape_unsupported", + `${unsupportedPath} is not supported by isolated Workers Static Assets; migrate it to a Worker module.`, + ); + } +} + function parseMosooConfig(source: string): MosooConfig { const value = parseTomlObject(source, ".mosoo.toml"); requireAllowedKeys( @@ -888,19 +898,6 @@ function normalizeOptionalRelativePath(path: string | null, field: string): stri return normalizeRelativePath(path, field); } -function normalizeResourceName(value: string): string { - const name = value.trim(); - - if (name.length === 0) { - throw new AppDeploymentDetectionError( - "deployment_config_required", - "deployment resource name is required", - ); - } - - return name; -} - function normalizeRelativePath(path: string, field: string): string { const rawPath = path.replaceAll("\\", "/"); const parts = rawPath.split("/").filter((part) => part !== "" && part !== "."); diff --git a/apps/api/src/modules/apps/application/app-deployment-executor.service.ts b/apps/api/src/modules/apps/application/app-deployment-executor.service.ts index 87ca0bb5..43517e6b 100644 --- a/apps/api/src/modules/apps/application/app-deployment-executor.service.ts +++ b/apps/api/src/modules/apps/application/app-deployment-executor.service.ts @@ -1,19 +1,27 @@ import type { AppDeploymentRunStatus } from "@mosoo/contracts/app"; import type { AppDeploymentRunRow, AppDeploymentRow } from "@mosoo/db"; -import { appDeploymentRunsTable, appDeploymentsTable } from "@mosoo/db"; +import { apiCommandsTable, appDeploymentRunsTable, appDeploymentsTable } from "@mosoo/db"; import { parsePlatformId } from "@mosoo/id"; import type { AgentId, AppDeploymentRunId } from "@mosoo/id"; -import { and, eq, inArray, isNotNull, isNull } from "drizzle-orm"; +import { and, eq, exists, inArray, isNull, or, sql } from "drizzle-orm"; +import createIgnore from "ignore"; import { createErrorLogContext, logError } from "../../../platform/cloudflare/logger"; import type { ApiBindings } from "../../../platform/cloudflare/worker-types"; import { getAppDatabase, getD1ChangeCount } from "../../../platform/db/drizzle"; +import { quoteShellArg } from "../../../shared/shell"; import { currentTimestampMs } from "../../../time"; import { listAppOwnerAgentRows } from "../../agents/application/agent-repository"; +import type { ApiCommandClaimAuthority } from "../../api-command/application/api-command-ledger"; +import { exactApiCommandClaimPredicate } from "../../api-command/application/api-command-ledger"; import { boundAgentUrl, mintAppAgentCapabilityToken } from "../../public-api/app-agent-capability"; import { - destroyRuntimeSubjectContainer, - getRuntimeSubjectKeepAliveHandle, + createRuntimeSandboxBucketMountOptions, + resolveRuntimeSandboxBucketMountTarget, +} from "../../runtime/infrastructure/runtime-sandbox-bucket-mount"; +import { + destroyUnversionedSandboxContainer, + getEphemeralUnversionedSandboxHandle, } from "../../runtime/infrastructure/runtime-subject-lifecycle/runtime-subject-platform"; import type { ExecutionSessionHandle, @@ -25,17 +33,21 @@ import { resolveAppAgentBindings, } from "./app-agent-binding-resolution"; import type { ResolvableAppAgent } from "./app-agent-binding-resolution"; -import type { CloudflareDeploymentClient } from "./app-deployment-cloudflare-client"; -import { - createCloudflareDeploymentClient, - deleteCloudflareDeploymentResources, - logCloudflareDeploymentResourceDeleteFailures, +import type { + CloudflareDeploymentClient, + CloudflareStaticAssetManifest, } from "./app-deployment-cloudflare-client"; +import { createCloudflareDeploymentClient } from "./app-deployment-cloudflare-client"; import { APP_DEPLOYMENT_COMPATIBILITY_DATE, + AppDeploymentDetectionError, detectAppDeploymentPlan, } from "./app-deployment-detector"; import type { AppDeploymentPlan, AppDeploymentRepositorySnapshot } from "./app-deployment-detector"; +import { + markAppDeploymentScriptUploadStarted, + registerAppDeploymentScriptCandidate, +} from "./app-deployment-script-reconciliation.service"; interface AppDeploymentDispatchContext { deployment: AppDeploymentRow; @@ -47,13 +59,34 @@ interface PreparedAppDeploymentRepository { snapshot: AppDeploymentRepositorySnapshot; } -interface AppDeploymentDeployResult { +export interface AppDeploymentDeployResult { + activeScriptName: string; externalDeploymentId: string | null; externalProjectId: string | null; externalVersionId: string | null; url: string; } +type AppDeploymentUploadedResult = Omit; + +export interface AppDeploymentAttemptAuthority extends ApiCommandClaimAuthority { + requireOwnership: () => Promise; +} + +export type AppDeploymentDispatchOutcome = + | { kind: "skipped" } + | ({ + deploymentId: AppDeploymentRow["id"]; + kind: "succeeded"; + runId: AppDeploymentRunId; + } & AppDeploymentDeployResult) + | { + errorCode: string; + errorMessage: string; + kind: "terminal_failure"; + runId: AppDeploymentRunId; + }; + export interface AppDeploymentBuildRunner { build(input: { plan: AppDeploymentPlan; @@ -61,12 +94,13 @@ export interface AppDeploymentBuildRunner { }): Promise; cleanup?(): Promise; deploy(input: { + activeScriptName: string; deployment: AppDeploymentRow; envVars: Record; plan: AppDeploymentPlan; prepared: PreparedAppDeploymentRepository; run: AppDeploymentRunRow; - }): Promise; + }): Promise; prepare(input: { deployment: AppDeploymentRow; run: AppDeploymentRunRow; @@ -75,6 +109,7 @@ export interface AppDeploymentBuildRunner { export interface DispatchAppDeploymentRunOptions { cloudflareClient?: CloudflareDeploymentClient; + deadlineMs?: number; runner?: AppDeploymentBuildRunner; } @@ -103,16 +138,156 @@ const SNAPSHOT_FILE_NAMES = new Set([ "yarn.lock", ]); const WORKER_JS_ENTRY_PATTERN = /\.(?:mjs|js)$/u; -export function appDeploymentBuildSandboxId(runId: AppDeploymentRunId): string { - return `${runId}-build`; +const APP_DEPLOYMENT_SANDBOX_SLEEP_AFTER_SECONDS = 15 * 60; +const APP_DEPLOYMENT_SANDBOX_COMMAND_TIMEOUT_MS = 10 * 60 * 1000; +const APP_DEPLOYMENT_EXECUTION_TIMEOUT_MS = 29 * 60 * 1000; +const APP_DEPLOYMENT_RPC_TIMEOUT_MS = 10 * 60 * 1000; +const APP_DEPLOYMENT_MANAGED_TAG = "mosoo-managed"; +const APP_DEPLOYMENT_MAX_SCRIPT_TAGS = 8; +const APP_DEPLOYMENT_ARTIFACT_MOUNT_PATH = "/mnt/mosoo-app-deployment-artifact"; +const APP_DEPLOYMENT_ARTIFACT_FILE_NAME = "artifact.tar"; +const APP_DEPLOYMENT_STATIC_ASSET_TOOL_FILE_NAME = "static-assets.mjs"; +const APP_DEPLOYMENT_STATIC_ASSET_INVENTORY_FILE_NAME = "inventory.json"; +const APP_DEPLOYMENT_STATIC_ASSET_SELECTION_FILE_NAME = "selection.json"; +const APP_DEPLOYMENT_STATIC_ASSET_MANIFEST_FILE_NAME = "manifest.json"; +const APP_DEPLOYMENT_STATIC_ASSET_BUCKETS_FILE_NAME = "buckets.json"; +const APP_DEPLOYMENT_STATIC_ASSET_COMPLETION_FILE_NAME = "completion-token"; +export type AppDeploymentAttemptIdentity = Pick< + AppDeploymentAttemptAuthority, + "attemptCount" | "deliveryGeneration" +>; +type AppDeploymentCandidateIdentity = Pick< + AppDeploymentAttemptAuthority, + "attemptCount" | "commandId" | "deliveryGeneration" +>; + +interface AppDeploymentDispatcher { + get(scriptName: string): { fetch(request: Request): Promise }; +} + +class AppDeploymentExecutionBudget { + readonly #deadlineMs: number; + + constructor(deadlineMs: number) { + if (!Number.isFinite(deadlineMs) || deadlineMs <= Date.now()) { + throw new Error("App deployment execution deadline has already expired."); + } + + this.#deadlineMs = deadlineMs; + } + + remaining(maximumMs = APP_DEPLOYMENT_RPC_TIMEOUT_MS): number { + const remainingMs = Math.floor(this.#deadlineMs - Date.now()); + + if (remainingMs <= 0) { + throw new Error("App deployment exceeded its execution deadline."); + } + + return Math.max(1, Math.min(remainingMs, maximumMs)); + } + + async run( + label: string, + effect: (timeoutMs: number) => Promise, + maximumMs = APP_DEPLOYMENT_RPC_TIMEOUT_MS, + ): Promise { + const timeoutMs = this.remaining(maximumMs); + let timeout: ReturnType | undefined; + + try { + return await Promise.race([ + effect(timeoutMs), + new Promise((_, reject) => { + timeout = setTimeout( + () => reject(new Error(`${label} exceeded the App deployment execution budget.`)), + timeoutMs, + ); + }), + ]); + } finally { + if (timeout !== undefined) { + clearTimeout(timeout); + } + } + } +} + +function appDeploymentAttemptSuffix(attempt: AppDeploymentAttemptIdentity): string { + return `g${attempt.deliveryGeneration.toString(36)}-a${attempt.attemptCount.toString(36)}`; +} + +export function appDeploymentCandidateScriptName(attempt: AppDeploymentCandidateIdentity): string { + const scriptName = `app-${attempt.commandId.toLowerCase()}-${appDeploymentAttemptSuffix(attempt)}`; + + if (!/^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/u.test(scriptName)) { + throw new Error("App deployment attempt cannot be represented as a Cloudflare script name."); + } + + return scriptName; +} + +function appDeploymentCandidateScriptTags(input: { + deploymentId: AppDeploymentRow["id"]; + runId: AppDeploymentRunId; +}): string[] { + const tags = [ + APP_DEPLOYMENT_MANAGED_TAG, + `d-${input.deploymentId.toLowerCase()}`, + `r-${input.runId.toLowerCase()}`, + ]; + + if ( + tags.length > APP_DEPLOYMENT_MAX_SCRIPT_TAGS || + tags.some((tag) => !/^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/u.test(tag)) + ) { + throw new Error("App deployment identifiers cannot be represented as Cloudflare tags."); + } + + return tags; +} + +function appDeploymentArtifactPrefix(scriptName: string): string { + return `app-deployments/${scriptName}`; +} + +function mountAppDeploymentArtifactBucket( + bindings: ApiBindings, + sandbox: SandboxHandle, + scriptName: string, + readOnly: boolean, +): Promise { + return sandbox.mountBucket( + resolveRuntimeSandboxBucketMountTarget(bindings), + APP_DEPLOYMENT_ARTIFACT_MOUNT_PATH, + createRuntimeSandboxBucketMountOptions(bindings, { + prefix: appDeploymentArtifactPrefix(scriptName), + readOnly, + }), + ); } -export function appDeploymentDeploySandboxId(runId: AppDeploymentRunId): string { - return `${runId}-deploy`; +export function appDeploymentBuildSandboxId( + runId: AppDeploymentRunId, + attempt: AppDeploymentAttemptIdentity, +): string { + return `${runId}-${appDeploymentAttemptSuffix(attempt)}-build`; } -function quoteShellArg(value: string): string { - return `'${value.replaceAll("'", `'"'"'`)}'`; +export function appDeploymentDeploySandboxId( + runId: AppDeploymentRunId, + attempt: AppDeploymentAttemptIdentity, +): string { + return `${runId}-${appDeploymentAttemptSuffix(attempt)}-deploy`; +} + +async function runOwnedEffect( + requireOwnership: () => Promise, + effect: () => Promise, +): Promise { + await requireOwnership(); + const result = await effect(); + await requireOwnership(); + return result; } function deploymentHostname(deployment: AppDeploymentRow, domain: string): string { @@ -123,6 +298,30 @@ function deploymentUrl(deployment: AppDeploymentRow, domain: string): string { return `https://${deploymentHostname(deployment, domain)}`; } +function requiredBinding(bindings: ApiBindings, name: string): unknown { + const value = Reflect.get(bindings, name); + + if (value === undefined || value === null || value === "") { + throw new Error(`${name} is required for App deployment.`); + } + + return value; +} + +function appDeploymentDispatcher(bindings: ApiBindings): AppDeploymentDispatcher { + const dispatcher = requiredBinding(bindings, "APP_DEPLOYMENT_DISPATCHER"); + + if ( + typeof dispatcher !== "object" || + dispatcher === null || + typeof Reflect.get(dispatcher, "get") !== "function" + ) { + throw new Error("APP_DEPLOYMENT_DISPATCHER is invalid."); + } + + return dispatcher as AppDeploymentDispatcher; +} + function isActiveRunStatus(status: AppDeploymentRunStatus): boolean { return (ACTIVE_APP_DEPLOYMENT_RUN_STATUSES as readonly AppDeploymentRunStatus[]).includes(status); } @@ -153,10 +352,15 @@ async function execChecked( session: ExecutionSessionHandle, command: string, label: string, + budget: AppDeploymentExecutionBudget, options: { retryable?: boolean } = {}, ): Promise { const message = commandFailureMessage( - await session.exec(`sh -lc ${quoteShellArg(command)}`), + await budget.run(label, (timeoutMs) => + session.exec(`sh -lc ${quoteShellArg(command)}`, { + timeout: Math.min(timeoutMs, APP_DEPLOYMENT_SANDBOX_COMMAND_TIMEOUT_MS), + }), + ), label, ); @@ -171,14 +375,6 @@ async function execChecked( throw new Error(message); } -function assertSelfContainedWorkerModule(scriptContent: string): void { - if (/^\s*import\s/mu.test(scriptContent) || /\bimport\s*\(/u.test(scriptContent)) { - throw new AppDeploymentNonRetryableError( - "Worker deployment only supports self-contained JavaScript modules in the first cut.", - ); - } -} - function assertRequestedMosooConfigPresent( run: AppDeploymentRunRow, snapshot: AppDeploymentRepositorySnapshot, @@ -208,28 +404,25 @@ function assertRequestedMosooConfigPresent( } } -async function assertControlledWranglerAvailable(sandbox: SandboxHandle): Promise { - await execChecked( - sandbox, - "command -v wrangler >/dev/null && wrangler --version >/dev/null", - "Controlled Wrangler availability", - { retryable: false }, - ); -} - export async function destroyAppDeploymentRunSandboxesBestEffort( bindings: ApiBindings, runId: AppDeploymentRunId, + attempt?: AppDeploymentAttemptIdentity, ): Promise { + const buildSandboxId = + attempt === undefined ? `${runId}-build` : appDeploymentBuildSandboxId(runId, attempt); + const deploySandboxId = + attempt === undefined ? `${runId}-deploy` : appDeploymentDeploySandboxId(runId, attempt); + await Promise.all([ destroyDeploymentSandboxBestEffort( bindings, - appDeploymentBuildSandboxId(runId), + buildSandboxId, "app-deployment.build_sandbox_destroy_failed", ), destroyDeploymentSandboxBestEffort( bindings, - appDeploymentDeploySandboxId(runId), + deploySandboxId, "app-deployment.deploy_sandbox_destroy_failed", ), ]); @@ -241,7 +434,7 @@ async function destroyDeploymentSandboxBestEffort( eventName: string, ): Promise { try { - await destroyRuntimeSubjectContainer(bindings, sandboxId); + await destroyUnversionedSandboxContainer(bindings, sandboxId); } catch (error) { logError(eventName, { ...createErrorLogContext(error), @@ -283,193 +476,163 @@ async function readCurrentDispatchContext( return deployment === null ? null : { deployment, run }; } -async function updateRunStatus( - database: D1Database, - runId: AppDeploymentRunId, - status: Extract< - AppDeploymentRunStatus, - "activating" | "building" | "preparing" | "submitted" | "submitting" - >, -): Promise { - const result = await getAppDatabase(database) - .update(appDeploymentRunsTable) - .set({ status, updatedAt: currentTimestampMs() }) - .where( - and( - eq(appDeploymentRunsTable.id, runId), - inArray(appDeploymentRunsTable.status, ACTIVE_APP_DEPLOYMENT_RUN_STATUSES), - ), - ) - .run(); +type AppDeploymentProgressStatus = Extract< + AppDeploymentRunStatus, + "activating" | "building" | "preparing" | "queued" | "submitted" | "submitting" +>; - return getD1ChangeCount(result) > 0; -} - -async function storeDeploymentPlan(input: { - database: D1Database; - plan: AppDeploymentPlan; - runId: AppDeploymentRunId; - targetName: string; -}): Promise { - const targetKind = input.plan.targetKind; - const result = await getAppDatabase(input.database) - .update(appDeploymentRunsTable) - .set({ - generatedWranglerConfigJson: JSON.stringify({ toml: input.plan.generatedWranglerConfig }), - planJson: JSON.stringify(input.plan), - targetKind, - targetProjectName: targetKind === "cloudflare_pages" ? input.targetName : null, - targetScriptName: targetKind === "cloudflare_worker" ? input.targetName : null, - updatedAt: currentTimestampMs(), - }) - .where( - and( - eq(appDeploymentRunsTable.id, input.runId), - inArray(appDeploymentRunsTable.status, ACTIVE_APP_DEPLOYMENT_RUN_STATUSES), - ), - ) - .run(); +const APP_DEPLOYMENT_STATUS_INDEX = new Map( + ACTIVE_APP_DEPLOYMENT_RUN_STATUSES.map((status, index) => [status, index]), +); - return getD1ChangeCount(result) > 0; +function activeRunStatusIndex(status: AppDeploymentRunStatus): number | null { + return APP_DEPLOYMENT_STATUS_INDEX.get(status as AppDeploymentProgressStatus) ?? null; } -async function failDeploymentRunIfActive(input: { - database: D1Database; - errorCode: string; - errorMessage: string; - runId: AppDeploymentRunId; -}): Promise { - await getAppDatabase(input.database) - .update(appDeploymentRunsTable) - .set({ - errorCode: input.errorCode, - errorMessage: input.errorMessage, - status: "failed", - updatedAt: currentTimestampMs(), - }) - .where( - and( - eq(appDeploymentRunsTable.id, input.runId), - inArray(appDeploymentRunsTable.status, ACTIVE_APP_DEPLOYMENT_RUN_STATUSES), +function exactAppDeploymentAttempt( + database: D1Database, + authority: AppDeploymentAttemptAuthority, + runId: AppDeploymentRunId, + nowMs: number, +) { + return exists( + getAppDatabase(database) + .select({ id: apiCommandsTable.id }) + .from(apiCommandsTable) + .where( + and( + exactApiCommandClaimPredicate(authority, nowMs), + eq(apiCommandsTable.kind, "app_deployment_run_dispatch"), + sql`json_extract(${apiCommandsTable.payloadJson}, '$.appDeploymentRunId') = ${runId}`, + ), ), - ) - .run(); + ); } -async function completeDeploymentRun(input: { +async function advanceRunStatus(input: { + authority: AppDeploymentAttemptAuthority; database: D1Database; - deployment: AppDeploymentRow; - result: AppDeploymentDeployResult; - run: AppDeploymentRunRow; + expected: AppDeploymentProgressStatus; + next: Exclude; + runId: AppDeploymentRunId; }): Promise { - const nowMs = currentTimestampMs(); - const deploymentUpdate = await getAppDatabase(input.database) - .update(appDeploymentsTable) - .set({ - lastSuccessfulUrl: input.result.url, - updatedAt: nowMs, - }) - .where( - and( - eq(appDeploymentsTable.id, input.deployment.id), - eq(appDeploymentsTable.latestRunId, input.run.id), - isNull(appDeploymentsTable.deletedAt), - ), - ) - .run(); + const changed = await runOwnedEffect(input.authority.requireOwnership, async () => { + const nowMs = currentTimestampMs(); + const result = await getAppDatabase(input.database) + .update(appDeploymentRunsTable) + .set({ status: input.next, updatedAt: nowMs }) + .where( + and( + eq(appDeploymentRunsTable.id, input.runId), + eq(appDeploymentRunsTable.status, input.expected), + exactAppDeploymentAttempt(input.database, input.authority, input.runId, nowMs), + ), + ) + .run(); - if (getD1ChangeCount(deploymentUpdate) === 0) { - return false; + return getD1ChangeCount(result) > 0; + }); + + if (changed) { + return true; } - const runUpdate = await getAppDatabase(input.database) - .update(appDeploymentRunsTable) - .set({ - errorCode: null, - errorMessage: null, - externalDeploymentId: input.result.externalDeploymentId, - externalProjectId: input.result.externalProjectId, - externalVersionId: input.result.externalVersionId, - status: "success", - updatedAt: nowMs, - url: input.result.url, - }) - .where( - and( - eq(appDeploymentRunsTable.id, input.run.id), - inArray(appDeploymentRunsTable.status, ACTIVE_APP_DEPLOYMENT_RUN_STATUSES), - ), - ) - .run(); + const currentStatus = await runOwnedEffect(input.authority.requireOwnership, async () => { + const row = await getAppDatabase(input.database) + .select({ status: appDeploymentRunsTable.status }) + .from(appDeploymentRunsTable) + .where(eq(appDeploymentRunsTable.id, input.runId)) + .limit(1) + .get(); + return row?.status ?? null; + }); + const currentIndex = currentStatus === null ? null : activeRunStatusIndex(currentStatus); + const nextIndex = APP_DEPLOYMENT_STATUS_INDEX.get(input.next); - return getD1ChangeCount(runUpdate) > 0; + return currentIndex !== null && nextIndex !== undefined && currentIndex >= nextIndex; } -async function shouldCompensateDeletedDeployment(input: { +async function storeDeploymentPlan(input: { + authority: AppDeploymentAttemptAuthority; database: D1Database; - deployment: AppDeploymentRow; + plan: AppDeploymentPlan; + runId: AppDeploymentRunId; + targetScriptName: string; }): Promise { - const deletedDeployment = - (await getAppDatabase(input.database) - .select({ id: appDeploymentsTable.id }) - .from(appDeploymentsTable) + const targetKind = input.plan.targetKind; + const planJson = JSON.stringify(input.plan); + const generatedWranglerConfigJson = JSON.stringify({ + toml: input.plan.generatedWranglerConfig, + }); + const changed = await runOwnedEffect(input.authority.requireOwnership, async () => { + const nowMs = currentTimestampMs(); + const result = await getAppDatabase(input.database) + .update(appDeploymentRunsTable) + .set({ + generatedWranglerConfigJson, + planJson, + targetKind, + targetProjectName: null, + targetScriptName: input.targetScriptName, + updatedAt: nowMs, + }) .where( and( - eq(appDeploymentsTable.id, input.deployment.id), - isNotNull(appDeploymentsTable.deletedAt), + eq(appDeploymentRunsTable.id, input.runId), + inArray(appDeploymentRunsTable.status, ACTIVE_APP_DEPLOYMENT_RUN_STATUSES), + exactAppDeploymentAttempt(input.database, input.authority, input.runId, nowMs), + or( + isNull(appDeploymentRunsTable.planJson), + and( + eq(appDeploymentRunsTable.planJson, planJson), + eq(appDeploymentRunsTable.generatedWranglerConfigJson, generatedWranglerConfigJson), + eq(appDeploymentRunsTable.targetKind, targetKind), + isNull(appDeploymentRunsTable.targetProjectName), + ), + ), ), ) - .limit(1) - .get()) ?? null; + .run(); + return getD1ChangeCount(result) > 0; + }); - if (deletedDeployment === null) { - return false; + if (changed) { + return true; } - const replacement = - (await getAppDatabase(input.database) - .select({ id: appDeploymentsTable.id }) - .from(appDeploymentsTable) - .where( - and( - eq(appDeploymentsTable.appId, input.deployment.appId), - isNull(appDeploymentsTable.deletedAt), - ), - ) + const run = await runOwnedEffect(input.authority.requireOwnership, async () => + getAppDatabase(input.database) + .select({ + generatedWranglerConfigJson: appDeploymentRunsTable.generatedWranglerConfigJson, + planJson: appDeploymentRunsTable.planJson, + status: appDeploymentRunsTable.status, + targetKind: appDeploymentRunsTable.targetKind, + targetProjectName: appDeploymentRunsTable.targetProjectName, + targetScriptName: appDeploymentRunsTable.targetScriptName, + }) + .from(appDeploymentRunsTable) + .where(eq(appDeploymentRunsTable.id, input.runId)) .limit(1) - .get()) ?? null; + .get(), + ); - return replacement === null; -} + if (run === undefined || !isActiveRunStatus(run.status)) { + return false; + } -async function compensateDeletedDeploymentResources(input: { - bindings: ApiBindings; - cloudflareClient: CloudflareDeploymentClient | null; - deployment: AppDeploymentRow; -}): Promise { if ( - !(await shouldCompensateDeletedDeployment({ - database: input.bindings.DB, - deployment: input.deployment, - })) + run.planJson !== planJson || + run.generatedWranglerConfigJson !== generatedWranglerConfigJson || + run.targetKind !== targetKind || + run.targetProjectName !== null || + run.targetScriptName !== input.targetScriptName ) { - return; - } - - const deleteFailures = await deleteCloudflareDeploymentResources( - input.cloudflareClient ?? createCloudflareDeploymentClient(input.bindings), - { - hostname: deploymentHostname(input.deployment, input.bindings.MOSOO_APP_DEPLOYMENT_DOMAIN), - resourceName: input.deployment.mosooSubdomain, - }, - ); - - if (deleteFailures.length > 0) { - logCloudflareDeploymentResourceDeleteFailures( - "app-deployment.cloudflare_delete_after_deletion_failed", - deleteFailures, + throw new AppDeploymentNonRetryableError( + "Deployment plan changed while retrying the same source commit.", ); } + + return true; } function shouldIncludeSnapshotPath(path: string): boolean { @@ -478,79 +641,103 @@ function shouldIncludeSnapshotPath(path: string): boolean { return SNAPSHOT_FILE_NAMES.has(fileName); } +function isUnsupportedStaticAssetsPath(path: string): boolean { + const parts = path.split("/"); + const fileName = parts.at(-1); + + return fileName === "_routes.json" || fileName === "_worker.js" || parts.includes("functions"); +} + async function readRepositorySnapshot( sandbox: SandboxHandle, repoDir: string, + budget: AppDeploymentExecutionBudget, ): Promise { - const listResult = await sandbox.exec( - `sh -lc ${quoteShellArg(`cd ${quoteShellArg(repoDir)} && find . -type f -print | sort`)}`, + const listResult = await budget.run("Repository file listing", (timeoutMs) => + sandbox.exec( + `sh -lc ${quoteShellArg(`cd ${quoteShellArg(repoDir)} && find . -type f -print | sort`)}`, + { timeout: Math.min(timeoutMs, APP_DEPLOYMENT_SANDBOX_COMMAND_TIMEOUT_MS) }, + ), ); assertSuccessfulCommand(listResult, "Repository file listing"); const files: Record = {}; - const paths = listResult.stdout + const repositoryPaths = listResult.stdout .split("\n") .map((line) => line.trim().replace(/^\.\//u, "")) - .filter((path) => path.length > 0 && shouldIncludeSnapshotPath(path)); + .filter((path) => path.length > 0); + const paths = repositoryPaths.filter(shouldIncludeSnapshotPath); await Promise.all( paths.map(async (path) => { - files[path] = (await sandbox.readFile(`${repoDir}/${path}`, { encoding: "utf8" })).content; + files[path] = ( + await budget.run(`Repository snapshot read (${path})`, () => + sandbox.readFile(`${repoDir}/${path}`, { encoding: "utf8" }), + ) + ).content; }), ); + for (const path of repositoryPaths.filter(isUnsupportedStaticAssetsPath)) { + files[path] ??= ""; + } + return { files }; } -function pagesRoutesFallbackCommands(plan: AppDeploymentPlan, outputDir: string): string[] { +function staticAssetsRoutesFallbackCommands(plan: AppDeploymentPlan): string[] { if (plan.routesFallback === null) { return []; } return [ - `printf '%s\\n' ${quoteShellArg(`/* /${plan.routesFallback} 200`)} > ${quoteShellArg( - `${outputDir}/_redirects`, - )}`, + `printf '\\n%s\\n' ${quoteShellArg(`/* /${plan.routesFallback} 200`)} >> ${quoteShellArg("_redirects")}`, ]; } -async function createPagesArtifactArchive(input: { +async function createStaticAssetsArtifactArchive(input: { + budget: AppDeploymentExecutionBudget; plan: AppDeploymentPlan; prepared: PreparedAppDeploymentRepository; buildSandbox: SandboxHandle; - workDir: string; -}): Promise { +}): Promise { if (input.plan.outputDir === null) { - throw new AppDeploymentNonRetryableError("Pages deployment plan is missing outputDir."); + throw new AppDeploymentNonRetryableError("Static Assets deployment plan is missing outputDir."); } - const archivePath = `${input.workDir}/artifact.tar`; + const archivePath = `${APP_DEPLOYMENT_ARTIFACT_MOUNT_PATH}/${APP_DEPLOYMENT_ARTIFACT_FILE_NAME}`; + const repoDir = input.prepared.repoDir; const outputDir = `${input.prepared.repoDir}/${input.plan.rootDir}/${input.plan.outputDir}`; await execChecked( input.buildSandbox, [ `rm -f ${quoteShellArg(archivePath)}`, - ...pagesRoutesFallbackCommands(input.plan, outputDir), + `repo_root=$(cd ${quoteShellArg(repoDir)} && pwd -P)`, + `case "$repo_root" in ${quoteShellArg(repoDir)}) ;; *) echo 'Static Assets repository root is not canonical.' >&2; exit 64 ;; esac`, `cd ${quoteShellArg(outputDir)}`, - `find . -type f -print0 | tar --null --no-recursion -cf ${quoteShellArg(archivePath)} -T -`, + `output_root=$(pwd -P)`, + `case "$output_root" in "$repo_root"|"$repo_root"/*) ;; *) echo 'Static Assets output escapes repository root.' >&2; exit 64 ;; esac`, + `test ! -e ${quoteShellArg("_worker.js")} || { echo '_worker.js must be migrated to a Worker module.' >&2; exit 64; }`, + `test ! -e ${quoteShellArg("_routes.json")} || { echo '_routes.json must be migrated to Static Assets routing.' >&2; exit 64; }`, + `test ! -e ${quoteShellArg("functions")} || { echo 'Pages functions must be migrated to a Worker module.' >&2; exit 64; }`, + ...staticAssetsRoutesFallbackCommands(input.plan), + `find . -name .git -prune -o -type f -print0 | tar --null --no-recursion -cf ${quoteShellArg(archivePath)} -T -`, ].join(" && "), - "Pages artifact archive", + "Static Assets artifact archive", + input.budget, { retryable: false }, ); - - return (await input.buildSandbox.readFile(archivePath, { encoding: "base64" })).content; } -async function extractPagesArtifactArchive(input: { - archiveBase64: string; +async function extractStaticAssetsArtifactArchive(input: { + budget: AppDeploymentExecutionBudget; deploySandbox: SandboxHandle; workDir: string; }): Promise<{ artifactDir: string; deployDir: string }> { - const artifactDir = `${input.workDir}/artifact`; - const archiveBase64Path = `${input.workDir}/artifact.tar.b64`; - const archivePath = `${input.workDir}/artifact.tar`; + const archivePath = `${APP_DEPLOYMENT_ARTIFACT_MOUNT_PATH}/${APP_DEPLOYMENT_ARTIFACT_FILE_NAME}`; const deployDir = `${input.workDir}/deploy`; + const artifactDir = `${deployDir}/artifact`; await execChecked( input.deploySandbox, @@ -558,42 +745,391 @@ async function extractPagesArtifactArchive(input: { `rm -rf ${quoteShellArg(input.workDir)}`, `mkdir -p ${quoteShellArg(artifactDir)} ${quoteShellArg(deployDir)}`, ].join(" && "), - "Pages deploy workspace", + "Static Assets deploy workspace", + input.budget, ); - await input.deploySandbox.writeFile(archiveBase64Path, input.archiveBase64); await execChecked( input.deploySandbox, - [ - `base64 -d ${quoteShellArg(archiveBase64Path)} > ${quoteShellArg(archivePath)}`, - `tar -xf ${quoteShellArg(archivePath)} -C ${quoteShellArg(artifactDir)}`, - ].join(" && "), - "Pages artifact extraction", + `tar -xf ${quoteShellArg(archivePath)} -C ${quoteShellArg(artifactDir)}`, + "Static Assets artifact extraction", + input.budget, ); return { artifactDir, deployDir }; } +interface StaticAssetsInventory { + assetsIgnore: string | null; + headers: string | null; + paths: string[]; + redirects: string | null; +} + +interface PreparedStaticAssetsArtifact { + artifactDir: string; + headers: string | null; + manifest: CloudflareStaticAssetManifest; + redirects: string | null; + toolPath: string; + workDir: string; +} + +export const APP_DEPLOYMENT_STATIC_ASSET_TOOL_SOURCE = String.raw` +import { createHash } from "node:crypto"; +import { extname, join } from "node:path"; +import { readFile, readdir, writeFile } from "node:fs/promises"; + +const MAX_ASSET_SIZE = 25 * 1024 * 1024; +const MIME_TYPES = { + avif: "image/avif", css: "text/css", csv: "text/csv", gif: "image/gif", + htm: "text/html", html: "text/html", ico: "image/x-icon", jpeg: "image/jpeg", + jpg: "image/jpeg", js: "application/javascript", json: "application/json", + m4a: "audio/mp4", map: "application/json", mjs: "application/javascript", + mp3: "audio/mpeg", mp4: "video/mp4", ogg: "audio/ogg", otf: "font/otf", + pdf: "application/pdf", png: "image/png", svg: "image/svg+xml", ttf: "font/ttf", + txt: "text/plain", wasm: "application/wasm", wav: "audio/wav", webm: "video/webm", + webmanifest: "application/manifest+json", webp: "image/webp", woff: "font/woff", + woff2: "font/woff2", xml: "application/xml" +}; + +function checkedRelativePath(value) { + if (typeof value !== "string" || value.length === 0 || value.startsWith("/") || value.includes("\\0")) { + throw new Error("Static Assets artifact contains an invalid path."); + } + const parts = value.split("/"); + if (parts.some((part) => part.length === 0 || part === "." || part === "..")) { + throw new Error("Static Assets artifact contains an invalid path."); + } + return value; +} + +async function readOptional(path) { + try { + return await readFile(path, "utf8"); + } catch (error) { + if (error && typeof error === "object" && error.code === "ENOENT") return null; + throw error; + } +} + +async function listFiles(root, relative = "") { + const entries = await readdir(join(root, relative), { withFileTypes: true }); + const paths = []; + for (const entry of entries.sort((left, right) => left.name.localeCompare(right.name))) { + const child = relative.length === 0 ? entry.name : relative + "/" + entry.name; + if (entry.isDirectory()) paths.push(...await listFiles(root, child)); + else if (entry.isFile()) paths.push(child); + } + return paths; +} + +async function createInventory(root, outputPath) { + const paths = await listFiles(root); + await writeFile(outputPath, JSON.stringify({ + assetsIgnore: await readOptional(join(root, ".assetsignore")), + headers: await readOptional(join(root, "_headers")), + paths, + redirects: await readOptional(join(root, "_redirects")) + })); +} + +async function createManifest(root, selectionPath, outputPath, scriptName) { + const selected = JSON.parse(await readFile(selectionPath, "utf8")); + if (!Array.isArray(selected) || typeof scriptName !== "string" || scriptName.length === 0) { + throw new Error("Static Assets manifest input is invalid."); + } + const manifest = {}; + for (const item of selected) { + const relative = checkedRelativePath(item); + const contents = await readFile(join(root, relative)); + if (contents.byteLength > MAX_ASSET_SIZE) { + throw new Error("Static asset exceeds the 25 MiB Cloudflare limit: " + relative); + } + const hash = createHash("sha256") + .update(scriptName) + .update(Buffer.from([0])) + .update(extname(relative).slice(1).toLowerCase()) + .update(Buffer.from([0])) + .update(contents) + .digest("hex") + .slice(0, 32); + manifest["/" + relative] = { hash, size: contents.byteLength }; + } + await writeFile(outputPath, JSON.stringify(manifest)); +} + +function jwtPayload(token) { + const encoded = token.split(".")[1]; + if (!encoded) throw new Error("Static Assets upload token is invalid."); + return JSON.parse(Buffer.from(encoded, "base64url").toString("utf8")); +} + +function contentType(path) { + const type = MIME_TYPES[extname(path).slice(1).toLowerCase()] ?? "application/null"; + return type.startsWith("text/") ? type + "; charset=utf-8" : type; +} + +async function uploadRequest(url, init) { + const response = await fetch(url, init); + let envelope; + try { + envelope = await response.json(); + } catch { + throw new Error("Cloudflare asset upload returned non-JSON (" + response.status + ")."); + } + if (!response.ok || envelope?.success !== true || !envelope.result || typeof envelope.result !== "object") { + throw new Error("Cloudflare asset upload failed (" + response.status + ")."); + } + const completion = envelope.result.jwt; + if (completion !== undefined && (typeof completion !== "string" || completion.length === 0)) { + throw new Error("Cloudflare asset upload returned an invalid completion token."); + } + return completion; +} + +async function uploadAssets(root, manifestPath, bucketsPath, completionPath) { + const accountId = process.env.CLOUDFLARE_ACCOUNT_ID; + const uploadToken = process.env.CLOUDFLARE_ASSET_UPLOAD_JWT; + if (!accountId || !uploadToken) throw new Error("Static Assets upload environment is incomplete."); + const manifest = JSON.parse(await readFile(manifestPath, "utf8")); + const buckets = JSON.parse(await readFile(bucketsPath, "utf8")); + if (!Array.isArray(buckets)) throw new Error("Static Assets upload buckets are invalid."); + const hashPaths = new Map(); + for (const [assetPath, entry] of Object.entries(manifest)) { + if (!entry || typeof entry !== "object" || typeof entry.hash !== "string") { + throw new Error("Static Assets manifest is invalid."); + } + hashPaths.set(entry.hash, checkedRelativePath(assetPath.slice(1))); + } + const single = jwtPayload(uploadToken).wrangler_single_asset_uploads === true; + const baseUrl = "https://api.cloudflare.com/client/v4/accounts/" + encodeURIComponent(accountId) + "/workers/assets/upload"; + let completion; + for (const bucket of buckets) { + if (!Array.isArray(bucket) || bucket.length === 0) throw new Error("Static Assets upload bucket is invalid."); + if (single) { + for (const hash of bucket) { + const relative = hashPaths.get(hash); + if (!relative) throw new Error("Cloudflare requested an unknown Static Assets hash."); + completion = await uploadRequest(baseUrl + "/" + encodeURIComponent(hash), { + body: await readFile(join(root, relative)), + headers: { Authorization: "Bearer " + uploadToken, "Content-Type": contentType(relative) }, + method: "POST" + }) ?? completion; + } + } else { + const form = new FormData(); + for (const hash of bucket) { + const relative = hashPaths.get(hash); + if (!relative) throw new Error("Cloudflare requested an unknown Static Assets hash."); + form.append(hash, (await readFile(join(root, relative))).toString("base64")); + } + completion = await uploadRequest(baseUrl + "?base64=true", { + body: form, + headers: { Authorization: "Bearer " + uploadToken }, + method: "POST" + }) ?? completion; + } + } + if (!completion) throw new Error("Cloudflare did not return a Static Assets completion token."); + await writeFile(completionPath, completion, { mode: 0o600 }); +} + +const [mode, ...args] = process.argv.slice(2); +if (mode === "inventory" && args.length === 2) await createInventory(args[0], args[1]); +else if (mode === "manifest" && args.length === 4) await createManifest(args[0], args[1], args[2], args[3]); +else if (mode === "upload" && args.length === 4) await uploadAssets(args[0], args[1], args[2], args[3]); +else throw new Error("Static Assets tool invocation is invalid."); +`; + +function parseStaticAssetsInventory(source: string): StaticAssetsInventory { + const value = JSON.parse(source) as unknown; + + if (typeof value !== "object" || value === null) { + throw new Error("Static Assets inventory is invalid."); + } + + const assetsIgnore = Reflect.get(value, "assetsIgnore"); + const headers = Reflect.get(value, "headers"); + const paths = Reflect.get(value, "paths"); + const redirects = Reflect.get(value, "redirects"); + + if ( + (assetsIgnore !== null && typeof assetsIgnore !== "string") || + (headers !== null && typeof headers !== "string") || + !Array.isArray(paths) || + !paths.every((path) => typeof path === "string" && path.length > 0) || + (redirects !== null && typeof redirects !== "string") + ) { + throw new Error("Static Assets inventory is invalid."); + } + + return { assetsIgnore, headers, paths, redirects }; +} + +function selectStaticAssetPaths(inventory: StaticAssetsInventory): string[] { + const patterns = ["/.assetsignore", "/_redirects", "/_headers"]; + + if (inventory.assetsIgnore !== null) { + patterns.push(...inventory.assetsIgnore.split("\n")); + } + + const matcher = createIgnore().add(patterns); + return inventory.paths.filter((path) => !matcher.test(path).ignored).toSorted(); +} + +function parseStaticAssetsManifest(source: string): CloudflareStaticAssetManifest { + const value = JSON.parse(source) as unknown; + + if (typeof value !== "object" || value === null || Array.isArray(value)) { + throw new Error("Static Assets manifest is invalid."); + } + + return value as CloudflareStaticAssetManifest; +} + +async function prepareStaticAssetsArtifact(input: { + activeScriptName: string; + budget: AppDeploymentExecutionBudget; + deploySandbox: SandboxHandle; + workDir: string; +}): Promise { + const { artifactDir, deployDir } = await extractStaticAssetsArtifactArchive(input); + const toolPath = `${deployDir}/${APP_DEPLOYMENT_STATIC_ASSET_TOOL_FILE_NAME}`; + const inventoryPath = `${deployDir}/${APP_DEPLOYMENT_STATIC_ASSET_INVENTORY_FILE_NAME}`; + const selectionPath = `${deployDir}/${APP_DEPLOYMENT_STATIC_ASSET_SELECTION_FILE_NAME}`; + const manifestPath = `${deployDir}/${APP_DEPLOYMENT_STATIC_ASSET_MANIFEST_FILE_NAME}`; + + await input.budget.run("Static Assets tool write", () => + input.deploySandbox.writeFile(toolPath, APP_DEPLOYMENT_STATIC_ASSET_TOOL_SOURCE), + ); + await execChecked( + input.deploySandbox, + `node ${quoteShellArg(toolPath)} inventory ${quoteShellArg(artifactDir)} ${quoteShellArg(inventoryPath)}`, + "Static Assets inventory", + input.budget, + { retryable: false }, + ); + const inventory = parseStaticAssetsInventory( + ( + await input.budget.run("Static Assets inventory read", () => + input.deploySandbox.readFile(inventoryPath, { encoding: "utf8" }), + ) + ).content, + ); + await input.budget.run("Static Assets selection write", () => + input.deploySandbox.writeFile(selectionPath, JSON.stringify(selectStaticAssetPaths(inventory))), + ); + await execChecked( + input.deploySandbox, + [ + "node", + quoteShellArg(toolPath), + "manifest", + quoteShellArg(artifactDir), + quoteShellArg(selectionPath), + quoteShellArg(manifestPath), + quoteShellArg(input.activeScriptName), + ].join(" "), + "Static Assets manifest", + input.budget, + { retryable: false }, + ); + const manifest = parseStaticAssetsManifest( + ( + await input.budget.run("Static Assets manifest read", () => + input.deploySandbox.readFile(manifestPath, { encoding: "utf8" }), + ) + ).content, + ); + + return { + artifactDir, + headers: inventory.headers, + manifest, + redirects: inventory.redirects, + toolPath, + workDir: deployDir, + }; +} + +async function bundleWorkerModule(input: { + budget: AppDeploymentExecutionBudget; + plan: AppDeploymentPlan; + prepared: PreparedAppDeploymentRepository; + sandbox: SandboxHandle; +}): Promise<{ mainModuleName: string; scriptContent: string }> { + if (input.plan.workerEntry === null) { + throw new AppDeploymentNonRetryableError("Worker deployment plan is missing workerEntry."); + } + + if (!WORKER_JS_ENTRY_PATTERN.test(input.plan.workerEntry)) { + throw new AppDeploymentNonRetryableError( + "Worker deployment requires a JavaScript module entry.", + ); + } + + const rootDir = `${input.prepared.repoDir}/${input.plan.rootDir}`; + const bundlePath = `${rootDir}/.mosoo-worker-bundle.mjs`; + await execChecked( + input.sandbox, + [ + `cd ${quoteShellArg(rootDir)} &&`, + "bun build", + quoteShellArg(input.plan.workerEntry), + "--target=browser", + "--format=esm", + `--outfile=${quoteShellArg(bundlePath)}`, + `--external=${quoteShellArg("cloudflare:*")}`, + ].join(" "), + "Worker module bundle", + input.budget, + { retryable: false }, + ); + + return { + mainModuleName: "worker.mjs", + scriptContent: ( + await input.budget.run("Worker bundle read", () => + input.sandbox.readFile(bundlePath, { encoding: "utf8" }), + ) + ).content, + }; +} + class SandboxAppDeploymentBuildRunner implements AppDeploymentBuildRunner { + readonly #attempt: AppDeploymentAttemptIdentity; readonly #bindings: ApiBindings; + readonly #budget: AppDeploymentExecutionBudget; readonly #cloudflareClient: CloudflareDeploymentClient; + readonly #requireOwnership: () => Promise; #buildSandbox: SandboxHandle | null = null; - #buildWorkDir: string | null = null; #runId: AppDeploymentRunId | null = null; - constructor(bindings: ApiBindings, cloudflareClient: CloudflareDeploymentClient) { + constructor( + bindings: ApiBindings, + cloudflareClient: CloudflareDeploymentClient, + authority: AppDeploymentAttemptAuthority, + budget: AppDeploymentExecutionBudget, + ) { + this.#attempt = authority; this.#bindings = bindings; + this.#budget = budget; this.#cloudflareClient = cloudflareClient; + this.#requireOwnership = authority.requireOwnership; } async prepare(input: { deployment: AppDeploymentRow; run: AppDeploymentRunRow; }): Promise { - const sandbox = await getRuntimeSubjectKeepAliveHandle( - this.#bindings, - appDeploymentBuildSandboxId(input.run.id), + const sandbox = await this.#budget.run("Build sandbox acquisition", () => + getEphemeralUnversionedSandboxHandle( + this.#bindings, + appDeploymentBuildSandboxId(input.run.id, this.#attempt), + APP_DEPLOYMENT_SANDBOX_SLEEP_AFTER_SECONDS, + ), ); - const workDir = `/tmp/mosoo-app-deployment-build-${input.run.id}`; + const workDir = `/tmp/mosoo-app-deployment-${input.run.id}-${appDeploymentAttemptSuffix(this.#attempt)}-build`; const repoDir = `${workDir}/repo`; const cloneCommand = [ `rm -rf ${quoteShellArg(workDir)}`, @@ -604,16 +1140,14 @@ class SandboxAppDeploymentBuildRunner implements AppDeploymentBuildRunner { `git checkout --detach ${quoteShellArg(input.run.sourceCommitSha)}`, ].join(" && "); - await sandbox.setKeepAlive(true); - await execChecked(sandbox, cloneCommand, "Repository clone"); + await execChecked(sandbox, cloneCommand, "Repository clone", this.#budget); this.#buildSandbox = sandbox; - this.#buildWorkDir = workDir; this.#runId = input.run.id; return { repoDir, - snapshot: await readRepositorySnapshot(sandbox, repoDir), + snapshot: await readRepositorySnapshot(sandbox, repoDir, this.#budget), }; } @@ -630,9 +1164,11 @@ class SandboxAppDeploymentBuildRunner implements AppDeploymentBuildRunner { return; } - const buildSession = await sandbox.createSession({ - cwd: `${input.prepared.repoDir}/${input.plan.rootDir}`, - }); + const buildSession = await this.#budget.run("Build session creation", () => + sandbox.createSession({ + cwd: `${input.prepared.repoDir}/${input.plan.rootDir}`, + }), + ); await execChecked( buildSession, @@ -640,151 +1176,185 @@ class SandboxAppDeploymentBuildRunner implements AppDeploymentBuildRunner { " && ", ), "App deployment build", + this.#budget, { retryable: false }, ); } async deploy(input: { + activeScriptName: string; deployment: AppDeploymentRow; envVars: Record; plan: AppDeploymentPlan; prepared: PreparedAppDeploymentRepository; run: AppDeploymentRunRow; - }): Promise { + }): Promise { const buildSandbox = this.#requireBuildSandbox(); - const buildWorkDir = this.#requireBuildWorkDir(); - const targetName = input.deployment.mosooSubdomain; - const domain = this.#bindings.MOSOO_APP_DEPLOYMENT_DOMAIN; - const hostname = deploymentHostname(input.deployment, domain); - - if (input.plan.targetKind === "cloudflare_pages") { - const project = await this.#cloudflareClient.ensurePagesProject({ - branch: input.run.sourceBranch, - projectName: targetName, - }); - const archiveBase64 = await createPagesArtifactArchive({ + const tags = appDeploymentCandidateScriptTags({ + deploymentId: input.deployment.id, + runId: input.run.id, + }); + + if (input.plan.targetKind === "cloudflare_static_assets") { + await this.#owned(() => + this.#budget.run("Build artifact bucket mount", () => + mountAppDeploymentArtifactBucket( + this.#bindings, + buildSandbox, + input.activeScriptName, + false, + ), + ), + ); + await createStaticAssetsArtifactArchive({ + budget: this.#budget, buildSandbox, plan: input.plan, prepared: input.prepared, - workDir: buildWorkDir, }); + await this.#owned(() => + this.#budget.run("Build artifact bucket unmount", () => + buildSandbox.unmountBucket(APP_DEPLOYMENT_ARTIFACT_MOUNT_PATH), + ), + ); await this.#destroyBuildSandbox(); - const deploySandbox = await getRuntimeSubjectKeepAliveHandle( - this.#bindings, - appDeploymentDeploySandboxId(input.run.id), + const deploySandbox = await this.#budget.run("Deploy sandbox acquisition", () => + getEphemeralUnversionedSandboxHandle( + this.#bindings, + appDeploymentDeploySandboxId(input.run.id, this.#attempt), + APP_DEPLOYMENT_SANDBOX_SLEEP_AFTER_SECONDS, + ), ); - const deployWorkDir = `/tmp/mosoo-app-deployment-deploy-${input.run.id}`; - await deploySandbox.setKeepAlive(true); - await assertControlledWranglerAvailable(deploySandbox); - const { artifactDir, deployDir } = await extractPagesArtifactArchive({ - archiveBase64, + const deployWorkDir = `/tmp/mosoo-app-deployment-${input.run.id}-${appDeploymentAttemptSuffix(this.#attempt)}-deploy`; + await this.#owned(() => + this.#budget.run("Deploy artifact bucket mount", () => + mountAppDeploymentArtifactBucket( + this.#bindings, + deploySandbox, + input.activeScriptName, + true, + ), + ), + ); + const artifact = await prepareStaticAssetsArtifact({ + activeScriptName: input.activeScriptName, + budget: this.#budget, deploySandbox, workDir: deployWorkDir, }); - const deploySession = await deploySandbox.createSession({ - cwd: deployDir, - env: { - CLOUDFLARE_ACCOUNT_ID: this.#bindings.CLOUDFLARE_ACCOUNT_ID, - CLOUDFLARE_API_TOKEN: this.#bindings.CLOUDFLARE_API_TOKEN, - }, - }); - - await execChecked( - deploySession, - [ - "wrangler", - "pages", - "deploy", - quoteShellArg(artifactDir), - "--project-name", - quoteShellArg(targetName), - "--branch", - quoteShellArg(input.run.sourceBranch), - ].join(" "), - "Cloudflare Pages deploy", + const uploadSession = await this.#owned(() => + this.#cloudflareClient.createStaticAssetsUploadSession({ + manifest: artifact.manifest, + scriptName: input.activeScriptName, + timeoutMs: this.#budget.remaining(), + }), ); + let completionToken = uploadSession.uploadToken; + + if (uploadSession.buckets.length > 0) { + const bucketsPath = `${artifact.workDir}/${APP_DEPLOYMENT_STATIC_ASSET_BUCKETS_FILE_NAME}`; + const manifestPath = `${artifact.workDir}/${APP_DEPLOYMENT_STATIC_ASSET_MANIFEST_FILE_NAME}`; + const completionPath = `${artifact.workDir}/${APP_DEPLOYMENT_STATIC_ASSET_COMPLETION_FILE_NAME}`; + await this.#budget.run("Static Assets upload bucket write", () => + deploySandbox.writeFile(bucketsPath, JSON.stringify(uploadSession.buckets)), + ); + const deploySession = await this.#budget.run("Static Assets upload session creation", () => + deploySandbox.createSession({ + cwd: artifact.workDir, + env: { + CLOUDFLARE_ACCOUNT_ID: this.#bindings.CLOUDFLARE_ACCOUNT_ID, + CLOUDFLARE_ASSET_UPLOAD_JWT: uploadSession.uploadToken, + }, + }), + ); + + await this.#owned(() => + execChecked( + deploySession, + [ + "node", + quoteShellArg(artifact.toolPath), + "upload", + quoteShellArg(artifact.artifactDir), + quoteShellArg(manifestPath), + quoteShellArg(bucketsPath), + quoteShellArg(completionPath), + ].join(" "), + "Cloudflare Static Assets upload", + this.#budget, + ), + ); + completionToken = ( + await this.#budget.run("Static Assets completion token read", () => + deploySession.readFile(completionPath, { encoding: "utf8" }), + ) + ).content; + + if (completionToken.length === 0) { + throw new Error("Cloudflare did not return a Static Assets completion token."); + } + + await execChecked( + deploySession, + `rm -f ${quoteShellArg(completionPath)}`, + "Static Assets completion token cleanup", + this.#budget, + ); + } - const [latestDeployment, domainResult] = await Promise.all([ - this.#cloudflareClient.getLatestPagesDeployment({ - projectName: targetName, + const deployed = await this.#owned(() => + this.#cloudflareClient.deployStaticAssets({ + compatibilityDate: APP_DEPLOYMENT_COMPATIBILITY_DATE, + completionToken, + headers: artifact.headers, + redirects: artifact.redirects, + scriptName: input.activeScriptName, + tags, + timeoutMs: this.#budget.remaining(), }), - this.#cloudflareClient.ensurePagesDomain({ - hostname, - projectName: targetName, - }), - ]); - const url = - domainResult.status === "active" - ? deploymentUrl(input.deployment, domain) - : latestDeployment.url; - - if (url === null) { - throw new Error("Cloudflare Pages deployment response did not include a live URL."); - } + ); return { - externalDeploymentId: latestDeployment.deploymentId, - externalProjectId: project.projectId, - externalVersionId: null, - url, + externalDeploymentId: deployed.deploymentId, + externalProjectId: null, + externalVersionId: deployed.versionId, }; } - if (input.plan.workerEntry === null) { - throw new AppDeploymentNonRetryableError("Worker deployment plan is missing workerEntry."); - } - - if (!WORKER_JS_ENTRY_PATTERN.test(input.plan.workerEntry)) { - throw new AppDeploymentNonRetryableError( - "Worker deployment requires a JavaScript module entry.", - ); - } - - const mainModuleName = input.plan.workerEntry.split("/").at(-1) ?? input.plan.workerEntry; - const scriptContent = ( - await buildSandbox.readFile( - `${input.prepared.repoDir}/${input.plan.rootDir}/${input.plan.workerEntry}`, - { - encoding: "utf8", - }, - ) - ).content; - assertSelfContainedWorkerModule(scriptContent); + const { mainModuleName, scriptContent } = await bundleWorkerModule({ + budget: this.#budget, + plan: input.plan, + prepared: input.prepared, + sandbox: buildSandbox, + }); await this.#destroyBuildSandbox(); - const worker = await this.#cloudflareClient.deployWorkerModule({ - compatibilityDate: APP_DEPLOYMENT_COMPATIBILITY_DATE, - mainModuleName, - scriptContent, - scriptName: targetName, - vars: input.envVars, - }); - await this.#cloudflareClient.ensureWorkerRoute({ - hostname, - scriptName: targetName, - }); - await this.#cloudflareClient.ensureWorkerDomain({ - hostname, - scriptName: targetName, - }); + const worker = await this.#owned(() => + this.#cloudflareClient.deployWorkerModule({ + compatibilityDate: APP_DEPLOYMENT_COMPATIBILITY_DATE, + mainModuleName, + scriptContent, + scriptName: input.activeScriptName, + tags, + timeoutMs: this.#budget.remaining(), + vars: input.envVars, + }), + ); return { externalDeploymentId: worker.deploymentId, externalProjectId: null, externalVersionId: worker.versionId, - url: deploymentUrl(input.deployment, domain), }; } async cleanup(): Promise { - if (this.#runId === null) { - return; + if (this.#runId !== null) { + await destroyAppDeploymentRunSandboxesBestEffort(this.#bindings, this.#runId, this.#attempt); } - await destroyAppDeploymentRunSandboxesBestEffort(this.#bindings, this.#runId); this.#buildSandbox = null; - this.#buildWorkDir = null; } async #destroyBuildSandbox(): Promise { @@ -792,13 +1362,15 @@ class SandboxAppDeploymentBuildRunner implements AppDeploymentBuildRunner { return; } - await destroyDeploymentSandboxBestEffort( - this.#bindings, - appDeploymentBuildSandboxId(this.#runId), - "app-deployment.build_sandbox_destroy_failed", + const runId = this.#runId; + await this.#budget.run("Build sandbox destruction", () => + destroyDeploymentSandboxBestEffort( + this.#bindings, + appDeploymentBuildSandboxId(runId, this.#attempt), + "app-deployment.build_sandbox_destroy_failed", + ), ); this.#buildSandbox = null; - this.#buildWorkDir = null; } #requireBuildSandbox(): SandboxHandle { @@ -809,12 +1381,8 @@ class SandboxAppDeploymentBuildRunner implements AppDeploymentBuildRunner { return this.#buildSandbox; } - #requireBuildWorkDir(): string { - if (this.#buildWorkDir === null) { - throw new Error("App deployment work directory was not prepared."); - } - - return this.#buildWorkDir; + #owned(effect: () => Promise): Promise { + return runOwnedEffect(this.#requireOwnership, effect); } } @@ -868,19 +1436,109 @@ async function resolveDeploymentEnvVars( return envVars; } +function toAppDeploymentTerminalFailure( + runId: AppDeploymentRunId, + error: unknown, +): Extract | null { + if ( + error instanceof AppAgentBindingResolutionError || + error instanceof AppDeploymentDetectionError + ) { + return { + errorCode: error.code, + errorMessage: error.message, + kind: "terminal_failure", + runId, + }; + } + + if (error instanceof AppDeploymentNonRetryableError) { + return { + errorCode: error.name, + errorMessage: error.message, + kind: "terminal_failure", + runId, + }; + } + + return null; +} + +async function admitAppDeploymentCandidate(input: { + bindings: ApiBindings; + budget: AppDeploymentExecutionBudget; + scriptName: string; + url: string; +}): Promise { + const response = await input.budget.run( + "App deployment candidate admission", + (timeoutMs) => + appDeploymentDispatcher(input.bindings) + .get(input.scriptName) + .fetch( + new Request(input.url, { + signal: AbortSignal.timeout(timeoutMs), + }), + ), + 30_000, + ); + + if (!(response instanceof Response)) { + throw new Error("App deployment candidate admission did not return a Response."); + } + + await input.budget.run( + "App deployment candidate response disposal", + () => response.body?.cancel().catch(() => undefined) ?? Promise.resolve(), + 30_000, + ); +} + export async function dispatchAppDeploymentRun( bindings: ApiBindings, input: { appDeploymentRunId: AppDeploymentRunId }, + authority: AppDeploymentAttemptAuthority, options: DispatchAppDeploymentRunOptions = {}, -): Promise { - let context = await readCurrentDispatchContext(bindings.DB, input.appDeploymentRunId); +): Promise { + const startedAt = Date.now(); + const budget = new AppDeploymentExecutionBudget( + Math.min( + options.deadlineMs ?? startedAt + APP_DEPLOYMENT_EXECUTION_TIMEOUT_MS, + startedAt + APP_DEPLOYMENT_EXECUTION_TIMEOUT_MS, + ), + ); + let context = await budget.run("App deployment context read", () => + runOwnedEffect(authority.requireOwnership, () => + readCurrentDispatchContext(bindings.DB, input.appDeploymentRunId), + ), + ); if (context === null) { - return; + return { kind: "skipped" }; } - if (!(await updateRunStatus(bindings.DB, input.appDeploymentRunId, "preparing"))) { - return; + const activeScriptName = appDeploymentCandidateScriptName(authority); + const deploymentId = context.deployment.id; + const runId = context.run.id; + await budget.run("App deployment candidate registration", () => + runOwnedEffect(authority.requireOwnership, () => + registerAppDeploymentScriptCandidate(bindings.DB, { + authority, + deploymentId, + nowMs: currentTimestampMs(), + runId, + scriptName: activeScriptName, + }), + ), + ); + context = await budget.run("Registered App deployment context read", () => + runOwnedEffect(authority.requireOwnership, () => + readCurrentDispatchContext(bindings.DB, input.appDeploymentRunId), + ), + ); + + if (context === null) { + return { kind: "skipped" }; } const cloudflareClient = @@ -891,125 +1549,176 @@ export async function dispatchAppDeploymentRun( new SandboxAppDeploymentBuildRunner( bindings, cloudflareClient ?? createCloudflareDeploymentClient(bindings), + authority, + budget, ); - let externallyAttemptedDeployment: AppDeploymentRow | null = null; try { - const prepared = await runner.prepare(context); - const targetName = context.deployment.mosooSubdomain; + const prepared = await budget.run("App deployment preparation", () => + runOwnedEffect(authority.requireOwnership, () => + runner.prepare(context as AppDeploymentDispatchContext), + ), + ); assertRequestedMosooConfigPresent(context.run, prepared.snapshot); - const plan = detectAppDeploymentPlan(prepared.snapshot, { resourceName: targetName }); + const plan = detectAppDeploymentPlan(prepared.snapshot); if ( - !(await storeDeploymentPlan({ - database: bindings.DB, - plan, - runId: context.run.id, - targetName, - })) + !(await budget.run("App deployment plan persistence", () => + storeDeploymentPlan({ + authority, + database: bindings.DB, + plan, + runId: context!.run.id, + targetScriptName: activeScriptName, + }), + )) ) { - return; + await authority.requireOwnership(); + return { kind: "skipped" }; } - let envVars: Record; - try { - envVars = await resolveDeploymentEnvVars(bindings, context.deployment, context.run, plan); - } catch (error) { - if (error instanceof AppAgentBindingResolutionError) { - await failDeploymentRunIfActive({ - database: bindings.DB, - errorCode: error.code, - errorMessage: error.message, - runId: context.run.id, - }); - return; - } - throw error; - } + const envVars = await budget.run("App deployment binding resolution", () => + runOwnedEffect(authority.requireOwnership, () => + resolveDeploymentEnvVars(bindings, context!.deployment, context!.run, plan), + ), + ); - if (!(await updateRunStatus(bindings.DB, input.appDeploymentRunId, "building"))) { - return; + if ( + !(await budget.run("App deployment building transition", () => + advanceRunStatus({ + authority, + database: bindings.DB, + expected: "preparing", + next: "building", + runId: input.appDeploymentRunId, + }), + )) + ) { + await authority.requireOwnership(); + return { kind: "skipped" }; } - await runner.build({ plan, prepared }); + await budget.run("App deployment build", () => + runOwnedEffect(authority.requireOwnership, () => runner.build({ plan, prepared })), + ); - if (!(await updateRunStatus(bindings.DB, input.appDeploymentRunId, "submitting"))) { - await failDeploymentRunIfActive({ - database: bindings.DB, - errorCode: "deployment_submission_lost", - errorMessage: "Deployment built but the deployment run changed.", - runId: context.run.id, - }); - return; + if ( + !(await budget.run("App deployment submitting transition", () => + advanceRunStatus({ + authority, + database: bindings.DB, + expected: "building", + next: "submitting", + runId: input.appDeploymentRunId, + }), + )) + ) { + await authority.requireOwnership(); + return { kind: "skipped" }; } - context = await readCurrentDispatchContext(bindings.DB, input.appDeploymentRunId); + context = await budget.run("Submitting App deployment context read", () => + runOwnedEffect(authority.requireOwnership, () => + readCurrentDispatchContext(bindings.DB, input.appDeploymentRunId), + ), + ); if (context === null) { - await failDeploymentRunIfActive({ - database: bindings.DB, - errorCode: "deployment_context_lost", - errorMessage: "Deployment context was lost after build.", - runId: input.appDeploymentRunId, - }); - return; + return { kind: "skipped" }; } + const submittingContext = context; + + await budget.run("App deployment upload ledger mark", () => + runOwnedEffect(authority.requireOwnership, () => + markAppDeploymentScriptUploadStarted(bindings.DB, { + authority, + deploymentId: submittingContext.deployment.id, + nowMs: currentTimestampMs(), + runId: submittingContext.run.id, + scriptName: activeScriptName, + }), + ), + ); + const uploaded = await budget.run("App deployment upload", () => + runOwnedEffect(authority.requireOwnership, () => + runner.deploy({ activeScriptName, ...submittingContext, envVars, plan, prepared }), + ), + ); + const plannedUrl = deploymentUrl( + submittingContext.deployment, + bindings.MOSOO_APP_DEPLOYMENT_DOMAIN, + ); + const result: AppDeploymentDeployResult = { + ...uploaded, + activeScriptName, + url: plannedUrl, + }; - externallyAttemptedDeployment = context.deployment; - const result = await runner.deploy({ ...context, envVars, plan, prepared }); + await budget.run("App deployment admission ownership", () => + runOwnedEffect(authority.requireOwnership, () => + admitAppDeploymentCandidate({ + bindings, + budget, + scriptName: activeScriptName, + url: plannedUrl, + }), + ), + ); - if (!(await updateRunStatus(bindings.DB, input.appDeploymentRunId, "submitted"))) { - await failDeploymentRunIfActive({ - database: bindings.DB, - errorCode: "deployment_submission_lost", - errorMessage: "Deployment submitted externally but the deployment run changed.", - runId: context.run.id, - }); - return; + if ( + !(await budget.run("App deployment submitted transition", () => + advanceRunStatus({ + authority, + database: bindings.DB, + expected: "submitting", + next: "submitted", + runId: input.appDeploymentRunId, + }), + )) + ) { + await authority.requireOwnership(); + return { kind: "skipped" }; } - if (!(await updateRunStatus(bindings.DB, input.appDeploymentRunId, "activating"))) { - await failDeploymentRunIfActive({ - database: bindings.DB, - errorCode: "deployment_activation_lost", - errorMessage: "Deployment activated externally but the deployment run changed.", - runId: context.run.id, - }); - return; + if ( + !(await budget.run("App deployment activating transition", () => + advanceRunStatus({ + authority, + database: bindings.DB, + expected: "submitted", + next: "activating", + runId: input.appDeploymentRunId, + }), + )) + ) { + await authority.requireOwnership(); + return { kind: "skipped" }; } - const completed = await completeDeploymentRun({ - database: bindings.DB, - deployment: context.deployment, - result, - run: context.run, - }); + await budget.run("App deployment final ownership check", () => authority.requireOwnership()); + return { + ...result, + deploymentId: context.deployment.id, + kind: "succeeded", + runId: context.run.id, + }; + } catch (error) { + const terminalFailure = toAppDeploymentTerminalFailure(input.appDeploymentRunId, error); - if (!completed) { - await failDeploymentRunIfActive({ - database: bindings.DB, - errorCode: "deployment_completion_lost", - errorMessage: "Deployment completed externally but the App deployment row changed.", - runId: context.run.id, - }); + if (terminalFailure === null) { + throw error; } + + await authority.requireOwnership(); + return terminalFailure; } finally { - if (externallyAttemptedDeployment !== null) { - try { - await compensateDeletedDeploymentResources({ - bindings, - cloudflareClient, - deployment: externallyAttemptedDeployment, - }); - } catch (error) { - logError("app-deployment.cloudflare_delete_after_deletion_check_failed", { - ...createErrorLogContext(error), - deploymentId: externallyAttemptedDeployment.id, - runId: input.appDeploymentRunId, - }); - } + try { + await runner.cleanup?.(); + } catch (error) { + logError("app-deployment.sandbox_cleanup_failed", { + ...createErrorLogContext(error), + runId: input.appDeploymentRunId, + }); } - - await runner.cleanup?.(); } } diff --git a/apps/api/src/modules/apps/application/app-deployment-gateway.ts b/apps/api/src/modules/apps/application/app-deployment-gateway.ts new file mode 100644 index 00000000..85cacb0c --- /dev/null +++ b/apps/api/src/modules/apps/application/app-deployment-gateway.ts @@ -0,0 +1,150 @@ +import { PLATFORM_ID_PATTERN } from "@mosoo/id"; + +import { createErrorLogContext, logError } from "../../../platform/cloudflare/logger"; + +export const APP_DEPLOYMENT_PROBE_HOST_LABEL = "probe"; +export const APP_DEPLOYMENT_PROBE_PATH_PREFIX = "/.well-known/mosoo-wfp-probe/"; +export const APP_DEPLOYMENT_PROBE_SCRIPT_NAME = "mosoo-wfp-probe"; + +interface AppDeploymentUserWorker { + fetch(request: Request): Promise | Response; +} + +export interface AppDeploymentDispatchNamespace { + get(scriptName: string): AppDeploymentUserWorker; +} + +export interface AppDeploymentGatewayOptions { + appDeploymentDomain: string; + dispatcher: AppDeploymentDispatchNamespace | undefined; + resolveActiveScriptName(mosooSubdomain: string): Promise; +} + +export async function resolveActiveAppDeploymentScriptName( + database: D1Database, + mosooSubdomain: string, +): Promise { + const deployment = await database + .prepare( + `SELECT active_script_name AS activeScriptName + FROM app_deployment + WHERE mosoo_subdomain = ? + AND deleted_at IS NULL + AND active_script_name IS NOT NULL + LIMIT 1`, + ) + .bind(mosooSubdomain) + .first<{ activeScriptName: string }>(); + + return deployment?.activeScriptName ?? null; +} + +const platformIdPattern = new RegExp(PLATFORM_ID_PATTERN, "u"); + +function normalizeHostname(hostname: string): string { + return hostname.toLowerCase().replace(/\.$/u, ""); +} + +function readDeploymentSubdomain(hostname: string, domain: string): string | null { + const normalizedDomain = normalizeHostname(domain); + const normalizedHostname = normalizeHostname(hostname); + const suffix = `.${normalizedDomain}`; + + if (normalizedDomain.length === 0 || !normalizedHostname.endsWith(suffix)) { + return null; + } + + return normalizedHostname.slice(0, -suffix.length); +} + +export function parseAppDeploymentSubdomain(hostname: string, domain: string): string | null { + const subdomain = readDeploymentSubdomain(hostname, domain); + + if ( + subdomain === null || + subdomain.includes(".") || + !subdomain.startsWith("app-") || + !platformIdPattern.test(subdomain.slice(4).toUpperCase()) + ) { + return null; + } + + return subdomain; +} + +export async function dispatchAppDeploymentGatewayRequest( + request: Request, + options: AppDeploymentGatewayOptions, +): Promise { + const url = new URL(request.url); + const hostname = url.hostname; + const domainSubdomain = readDeploymentSubdomain(hostname, options.appDeploymentDomain); + + if (domainSubdomain === null) { + return null; + } + + const isProbe = + domainSubdomain === APP_DEPLOYMENT_PROBE_HOST_LABEL && + url.pathname.startsWith(APP_DEPLOYMENT_PROBE_PATH_PREFIX); + const mosooSubdomain = isProbe + ? null + : parseAppDeploymentSubdomain(hostname, options.appDeploymentDomain); + + if (!isProbe && mosooSubdomain === null) { + return notFound(); + } + + if (options.dispatcher === undefined) { + logError("app-deployment.gateway_dispatcher_missing", { + hostname, + mosooSubdomain, + }); + return serviceUnavailable(); + } + + let activeScriptName: string | null = APP_DEPLOYMENT_PROBE_SCRIPT_NAME; + + if (mosooSubdomain !== null) { + try { + activeScriptName = await options.resolveActiveScriptName(mosooSubdomain); + } catch (error) { + logError("app-deployment.gateway_lookup_failed", { + ...createErrorLogContext(error), + hostname, + mosooSubdomain, + }); + return serviceUnavailable(); + } + + if (activeScriptName === null) { + return notFound(); + } + } + + try { + return await options.dispatcher.get(activeScriptName).fetch(request); + } catch (error) { + logError("app-deployment.gateway_dispatch_failed", { + ...createErrorLogContext(error), + activeScriptName, + hostname, + mosooSubdomain, + }); + return serviceUnavailable(); + } +} + +function notFound(): Response { + return new Response(null, { + headers: { "Cache-Control": "no-store" }, + status: 404, + }); +} + +function serviceUnavailable(): Response { + return new Response(null, { + headers: { "Cache-Control": "no-store" }, + status: 503, + }); +} diff --git a/apps/api/src/modules/apps/application/app-deployment-script-reconciliation.service.ts b/apps/api/src/modules/apps/application/app-deployment-script-reconciliation.service.ts new file mode 100644 index 00000000..04e8efba --- /dev/null +++ b/apps/api/src/modules/apps/application/app-deployment-script-reconciliation.service.ts @@ -0,0 +1,629 @@ +import { + apiCommandsTable, + appDeploymentRunsTable, + appDeploymentScriptsTable, + appDeploymentsTable, +} from "@mosoo/db"; +import type { AppDeploymentScriptRow } from "@mosoo/db"; +import type { AppDeploymentId, AppDeploymentRunId } from "@mosoo/id"; +import { and, asc, eq, exists, gt, inArray, isNotNull, isNull, lte, or, sql } from "drizzle-orm"; + +import { getAppDatabase, getD1ChangeCount } from "../../../platform/db/drizzle"; +import type { ApiCommandClaimAuthority } from "../../api-command/application/api-command-ledger"; +import { + API_COMMAND_LEASE_MS, + exactApiCommandClaimPredicate, +} from "../../api-command/application/api-command-ledger"; +import { ACTIVE_APP_DEPLOYMENT_RUN_STATUSES } from "../domain/app-deployment-lifecycle"; + +export const APP_DEPLOYMENT_SCRIPT_GRACE_MS = 24 * 60 * 60_000; + +// ponytail: Keep one external cleanup per Queue command until measured backlog proves +// that bounded parallelism is needed; four remote effects cannot fit a serial 64-row page. +const RECONCILIATION_PAGE_SIZE = 1; +const TOMBSTONE_BACKOFF_DAYS = [1, 2, 4, 8, 16, 30] as const; +const DATABASE_NOW_MS = sql`CAST(unixepoch('subsec') * 1000 AS INTEGER)`; + +function databaseDeadlineMs(delayMs: number) { + return sql`min(${DATABASE_NOW_MS} + ${delayMs}, ${Number.MAX_SAFE_INTEGER})`; +} + +export interface AppDeploymentScriptCandidateAuthority extends ApiCommandClaimAuthority { + readonly requireOwnership: () => Promise; +} + +export interface AppDeploymentScriptCleanupAction { + readonly artifactPrefix: string; + readonly attemptCount: number; + readonly buildSandboxId: string; + readonly commandId: AppDeploymentScriptRow["commandId"]; + readonly deliveryGeneration: number; + readonly deploySandboxId: string; + readonly deploymentId: AppDeploymentId; + readonly reconcileCount: number; + readonly runId: AppDeploymentRunId; + readonly scriptName: string; + readonly uploadStartedAt: number | null; +} + +export interface AppDeploymentScriptReconciliationFailure { + readonly error: unknown; + readonly scriptName: string; +} + +export interface AppDeploymentScriptReconciliationResult { + readonly armed: number; + readonly cleaned: number; + readonly deferred: number; + readonly failures: readonly AppDeploymentScriptReconciliationFailure[]; + readonly hasMore: boolean; + readonly nextCursor: string | null; + readonly processed: number; +} + +export type AppDeploymentScriptReconciliationAuthority = AppDeploymentScriptCandidateAuthority; + +type ReconciliationDisposition = + | { readonly kind: "armed" } + | { readonly kind: "deferred" } + | { + readonly action: AppDeploymentScriptCleanupAction; + readonly kind: "cleanup"; + readonly reconcileOwner: string; + }; + +function exactAppDeploymentCommand( + database: D1Database, + authority: ApiCommandClaimAuthority, + nowMs: number, + runId: AppDeploymentRunId, +) { + return exists( + getAppDatabase(database) + .select({ id: apiCommandsTable.id }) + .from(apiCommandsTable) + .where( + and( + exactApiCommandClaimPredicate(authority, nowMs), + eq(apiCommandsTable.kind, "app_deployment_run_dispatch"), + gt(apiCommandsTable.claimExpiresAt, DATABASE_NOW_MS), + sql`json_valid(${apiCommandsTable.payloadJson}) = 1`, + sql`json_extract(${apiCommandsTable.payloadJson}, '$.appDeploymentRunId') = ${runId}`, + ), + ), + ); +} + +function exactReconciliationCommand(database: D1Database, authority: ApiCommandClaimAuthority) { + return exists( + getAppDatabase(database) + .select({ id: apiCommandsTable.id }) + .from(apiCommandsTable) + .where( + and( + eq(apiCommandsTable.id, authority.commandId), + eq(apiCommandsTable.deliveryGeneration, authority.deliveryGeneration), + eq(apiCommandsTable.attemptCount, authority.attemptCount), + eq(apiCommandsTable.status, "running"), + eq(apiCommandsTable.claimOwner, authority.claimOwner), + eq(apiCommandsTable.kind, "app_deployment_script_reconciliation"), + gt(apiCommandsTable.claimExpiresAt, DATABASE_NOW_MS), + ), + ), + ); +} + +function currentDeploymentRun( + database: D1Database, + deploymentId: AppDeploymentId, + runId: AppDeploymentRunId, +) { + return exists( + getAppDatabase(database) + .select({ id: appDeploymentsTable.id }) + .from(appDeploymentsTable) + .where( + and( + eq(appDeploymentsTable.id, deploymentId), + eq(appDeploymentsTable.latestRunId, runId), + isNull(appDeploymentsTable.deletedAt), + ), + ), + ); +} + +function scriptIsActive(database: D1Database, scriptName: string) { + return exists( + getAppDatabase(database) + .select({ id: appDeploymentsTable.id }) + .from(appDeploymentsTable) + .where(eq(appDeploymentsTable.activeScriptName, scriptName)), + ); +} + +function scriptHasTrafficAuthority(database: D1Database, scriptName: string) { + return or( + scriptIsActive(database, scriptName), + exists( + getAppDatabase(database) + .select({ scriptName: appDeploymentScriptsTable.scriptName }) + .from(appDeploymentScriptsTable) + .innerJoin( + appDeploymentRunsTable, + and( + eq(appDeploymentRunsTable.id, appDeploymentScriptsTable.runId), + eq(appDeploymentRunsTable.deploymentId, appDeploymentScriptsTable.deploymentId), + eq(appDeploymentRunsTable.targetScriptName, appDeploymentScriptsTable.scriptName), + ), + ) + .innerJoin( + appDeploymentsTable, + and( + eq(appDeploymentsTable.id, appDeploymentScriptsTable.deploymentId), + eq(appDeploymentsTable.latestRunId, appDeploymentScriptsTable.runId), + isNull(appDeploymentsTable.deletedAt), + ), + ) + .innerJoin(apiCommandsTable, eq(apiCommandsTable.id, appDeploymentScriptsTable.commandId)) + .where( + and( + eq(appDeploymentScriptsTable.scriptName, scriptName), + isNull(appDeploymentScriptsTable.retireAfter), + inArray(appDeploymentRunsTable.status, ACTIVE_APP_DEPLOYMENT_RUN_STATUSES), + eq(apiCommandsTable.kind, "app_deployment_run_dispatch"), + eq(apiCommandsTable.status, "running"), + eq(apiCommandsTable.deliveryGeneration, appDeploymentScriptsTable.deliveryGeneration), + eq(apiCommandsTable.attemptCount, appDeploymentScriptsTable.attemptCount), + eq(apiCommandsTable.claimOwner, appDeploymentScriptsTable.registeredClaimOwner), + gt(apiCommandsTable.claimExpiresAt, DATABASE_NOW_MS), + sql`json_valid(${apiCommandsTable.payloadJson}) = 1`, + sql`json_extract(${apiCommandsTable.payloadJson}, '$.appDeploymentRunId') = ${appDeploymentScriptsTable.runId}`, + ), + ), + ), + ); +} + +function attemptSuffix(attempt: { attemptCount: number; deliveryGeneration: number }): string { + return `g${attempt.deliveryGeneration.toString(36)}-a${attempt.attemptCount.toString(36)}`; +} + +function cleanupAction( + row: Omit< + AppDeploymentScriptCleanupAction, + "artifactPrefix" | "buildSandboxId" | "deploySandboxId" + >, +): AppDeploymentScriptCleanupAction { + const suffix = attemptSuffix(row); + return { + ...row, + artifactPrefix: `app-deployments/${row.scriptName}/`, + buildSandboxId: `${row.runId}-${suffix}-build`, + deploySandboxId: `${row.runId}-${suffix}-deploy`, + }; +} + +export async function registerAppDeploymentScriptCandidate( + database: D1Database, + input: { + readonly authority: AppDeploymentScriptCandidateAuthority; + readonly deploymentId: AppDeploymentId; + readonly nowMs: number; + readonly runId: AppDeploymentRunId; + readonly scriptName: string; + }, +): Promise { + await input.authority.requireOwnership(); + + const row: AppDeploymentScriptRow = { + attemptCount: input.authority.attemptCount, + commandId: input.authority.commandId, + deliveryGeneration: input.authority.deliveryGeneration, + deploymentId: input.deploymentId, + externalDeletedAt: null, + lastReconciledAt: null, + nextReconcileAt: input.nowMs + APP_DEPLOYMENT_SCRIPT_GRACE_MS, + reconcileCount: 0, + reconcileExpiresAt: null, + reconcileOwner: null, + registeredAt: input.nowMs, + registeredClaimOwner: input.authority.claimOwner, + retireAfter: null, + runId: input.runId, + scriptName: input.scriptName, + uploadStartedAt: null, + }; + + await getAppDatabase(database).insert(appDeploymentScriptsTable).values(row).run(); + + const registered = await getAppDatabase(database) + .select() + .from(appDeploymentScriptsTable) + .where(eq(appDeploymentScriptsTable.scriptName, input.scriptName)) + .limit(1) + .get(); + + if ( + registered === undefined || + registered.commandId !== row.commandId || + registered.deliveryGeneration !== row.deliveryGeneration || + registered.attemptCount !== row.attemptCount || + registered.registeredClaimOwner !== row.registeredClaimOwner || + registered.deploymentId !== row.deploymentId || + registered.runId !== row.runId || + registered.retireAfter !== null + ) { + throw new Error( + "App deployment script candidate registration conflicts with durable authority.", + ); + } + + return registered; +} + +export async function markAppDeploymentScriptUploadStarted( + database: D1Database, + input: { + readonly authority: AppDeploymentScriptCandidateAuthority; + readonly deploymentId: AppDeploymentId; + readonly nowMs: number; + readonly runId: AppDeploymentRunId; + readonly scriptName: string; + }, +): Promise { + await input.authority.requireOwnership(); + + const result = await getAppDatabase(database) + .update(appDeploymentScriptsTable) + .set({ + uploadStartedAt: sql`coalesce(${appDeploymentScriptsTable.uploadStartedAt}, ${input.nowMs})`, + }) + .where( + and( + eq(appDeploymentScriptsTable.scriptName, input.scriptName), + eq(appDeploymentScriptsTable.deploymentId, input.deploymentId), + eq(appDeploymentScriptsTable.runId, input.runId), + eq(appDeploymentScriptsTable.commandId, input.authority.commandId), + eq(appDeploymentScriptsTable.deliveryGeneration, input.authority.deliveryGeneration), + eq(appDeploymentScriptsTable.attemptCount, input.authority.attemptCount), + eq(appDeploymentScriptsTable.registeredClaimOwner, input.authority.claimOwner), + isNull(appDeploymentScriptsTable.retireAfter), + exactAppDeploymentCommand(database, input.authority, input.nowMs, input.runId), + currentDeploymentRun(database, input.deploymentId, input.runId), + exists( + getAppDatabase(database) + .select({ id: appDeploymentRunsTable.id }) + .from(appDeploymentRunsTable) + .where( + and( + eq(appDeploymentRunsTable.id, input.runId), + eq(appDeploymentRunsTable.targetScriptName, input.scriptName), + inArray(appDeploymentRunsTable.status, ACTIVE_APP_DEPLOYMENT_RUN_STATUSES), + ), + ), + ), + ), + ) + .run(); + + if (getD1ChangeCount(result) !== 1) { + throw new Error("App deployment script upload lost durable authority."); + } +} + +function decodeCursor( + cursor: string | null, +): { nextReconcileAt: number; scriptName: string } | null { + if (cursor === null) { + return null; + } + + const separator = cursor.indexOf(":"); + const nextReconcileAt = Number(cursor.slice(0, separator)); + const scriptName = cursor.slice(separator + 1); + + if ( + separator < 1 || + !Number.isSafeInteger(nextReconcileAt) || + nextReconcileAt < 0 || + !/^app-[0-9a-z-]+$/u.test(scriptName) + ) { + throw new Error("App deployment script reconciliation cursor is invalid."); + } + + return { nextReconcileAt, scriptName }; +} + +function encodeCursor(row: { nextReconcileAt: number | null; scriptName: string }): string { + if (row.nextReconcileAt === null) { + throw new Error("App deployment script reconciliation row has no cursor timestamp."); + } + return `${row.nextReconcileAt}:${row.scriptName}`; +} + +async function prepareReconciliation( + database: D1Database, + input: { + readonly authority: AppDeploymentScriptReconciliationAuthority; + readonly scriptName: string; + }, +): Promise { + const db = getAppDatabase(database); + const exactCommand = exactReconciliationCommand(database, input.authority); + const due = and( + eq(appDeploymentScriptsTable.scriptName, input.scriptName), + isNotNull(appDeploymentScriptsTable.nextReconcileAt), + lte(appDeploymentScriptsTable.nextReconcileAt, DATABASE_NOW_MS), + exactCommand, + ); + const protectedScript = scriptHasTrafficAuthority(database, input.scriptName); + const activeScript = scriptIsActive(database, input.scriptName); + + const protectedResult = await db + .update(appDeploymentScriptsTable) + .set({ + lastReconciledAt: DATABASE_NOW_MS, + nextReconcileAt: sql`CASE + WHEN ${activeScript} THEN NULL + ELSE ${databaseDeadlineMs(APP_DEPLOYMENT_SCRIPT_GRACE_MS)} + END`, + reconcileExpiresAt: null, + reconcileOwner: null, + }) + .where(and(due, protectedScript)) + .run(); + if (getD1ChangeCount(protectedResult) === 1) { + return { kind: "deferred" }; + } + + const retireAfter = sql`CAST(unixepoch('subsec') * 1000 AS INTEGER) + ${APP_DEPLOYMENT_SCRIPT_GRACE_MS}`; + const armedResult = await db + .update(appDeploymentScriptsTable) + .set({ + lastReconciledAt: DATABASE_NOW_MS, + nextReconcileAt: retireAfter, + reconcileExpiresAt: null, + reconcileOwner: null, + retireAfter, + }) + .where(and(due, isNull(appDeploymentScriptsTable.retireAfter), sql`NOT ${protectedScript}`)) + .run(); + if (getD1ChangeCount(armedResult) === 1) { + return { kind: "armed" }; + } + + const graceResult = await db + .update(appDeploymentScriptsTable) + .set({ nextReconcileAt: appDeploymentScriptsTable.retireAfter }) + .where( + and( + due, + isNotNull(appDeploymentScriptsTable.retireAfter), + gt(appDeploymentScriptsTable.retireAfter, DATABASE_NOW_MS), + ), + ) + .run(); + if (getD1ChangeCount(graceResult) === 1) { + return { kind: "deferred" }; + } + + const reconcileOwner = `${input.authority.commandId}:${crypto.randomUUID()}`; + const claimResult = await db + .update(appDeploymentScriptsTable) + .set({ + reconcileExpiresAt: databaseDeadlineMs(API_COMMAND_LEASE_MS), + reconcileOwner, + }) + .where( + and( + due, + isNotNull(appDeploymentScriptsTable.retireAfter), + lte(appDeploymentScriptsTable.retireAfter, DATABASE_NOW_MS), + sql`NOT ${protectedScript}`, + or( + isNull(appDeploymentScriptsTable.reconcileOwner), + lte(appDeploymentScriptsTable.reconcileExpiresAt, DATABASE_NOW_MS), + ), + ), + ) + .run(); + if (getD1ChangeCount(claimResult) !== 1) { + return { kind: "deferred" }; + } + + const row = await db + .select({ + attemptCount: appDeploymentScriptsTable.attemptCount, + commandId: appDeploymentScriptsTable.commandId, + deliveryGeneration: appDeploymentScriptsTable.deliveryGeneration, + deploymentId: appDeploymentScriptsTable.deploymentId, + reconcileCount: appDeploymentScriptsTable.reconcileCount, + runId: appDeploymentScriptsTable.runId, + scriptName: appDeploymentScriptsTable.scriptName, + uploadStartedAt: appDeploymentScriptsTable.uploadStartedAt, + }) + .from(appDeploymentScriptsTable) + .where( + and( + eq(appDeploymentScriptsTable.scriptName, input.scriptName), + eq(appDeploymentScriptsTable.reconcileOwner, reconcileOwner), + ), + ) + .limit(1) + .get(); + if (row === undefined) { + throw new Error("Claimed App deployment script reconciliation row disappeared."); + } + + return { action: cleanupAction(row), kind: "cleanup", reconcileOwner }; +} + +function tombstoneBackoffMs(reconcileCount: number): number { + const index = Math.min(reconcileCount, TOMBSTONE_BACKOFF_DAYS.length - 1); + return (TOMBSTONE_BACKOFF_DAYS[index] ?? 30) * APP_DEPLOYMENT_SCRIPT_GRACE_MS; +} + +async function completeReconciliation( + database: D1Database, + input: { + readonly action: AppDeploymentScriptCleanupAction; + readonly authority: AppDeploymentScriptReconciliationAuthority; + readonly reconcileOwner: string; + }, +): Promise { + const result = await getAppDatabase(database) + .update(appDeploymentScriptsTable) + .set({ + externalDeletedAt: + input.action.uploadStartedAt === null + ? appDeploymentScriptsTable.externalDeletedAt + : sql`coalesce(${appDeploymentScriptsTable.externalDeletedAt}, ${DATABASE_NOW_MS})`, + lastReconciledAt: DATABASE_NOW_MS, + nextReconcileAt: databaseDeadlineMs(tombstoneBackoffMs(input.action.reconcileCount)), + reconcileCount: sql`min(${appDeploymentScriptsTable.reconcileCount} + 1, 9007199254740991)`, + reconcileExpiresAt: null, + reconcileOwner: null, + }) + .where( + and( + eq(appDeploymentScriptsTable.scriptName, input.action.scriptName), + eq(appDeploymentScriptsTable.reconcileOwner, input.reconcileOwner), + gt(appDeploymentScriptsTable.reconcileExpiresAt, DATABASE_NOW_MS), + isNotNull(appDeploymentScriptsTable.retireAfter), + sql`NOT ${scriptHasTrafficAuthority(database, input.action.scriptName)}`, + exactReconciliationCommand(database, input.authority), + ), + ) + .run(); + + return getD1ChangeCount(result) === 1; +} + +async function failReconciliation( + database: D1Database, + input: { + readonly authority: AppDeploymentScriptReconciliationAuthority; + readonly reconcileOwner: string; + readonly scriptName: string; + }, +): Promise { + const result = await getAppDatabase(database) + .update(appDeploymentScriptsTable) + .set({ + lastReconciledAt: DATABASE_NOW_MS, + nextReconcileAt: databaseDeadlineMs(APP_DEPLOYMENT_SCRIPT_GRACE_MS), + reconcileExpiresAt: null, + reconcileOwner: null, + }) + .where( + and( + eq(appDeploymentScriptsTable.scriptName, input.scriptName), + eq(appDeploymentScriptsTable.reconcileOwner, input.reconcileOwner), + gt(appDeploymentScriptsTable.reconcileExpiresAt, DATABASE_NOW_MS), + exactReconciliationCommand(database, input.authority), + ), + ) + .run(); + + return getD1ChangeCount(result) === 1; +} + +export async function reconcileAppDeploymentScriptPage( + database: D1Database, + input: { + readonly authority: AppDeploymentScriptReconciliationAuthority; + readonly cursor: string | null; + }, + cleanup: (action: AppDeploymentScriptCleanupAction) => Promise, +): Promise { + const cursor = decodeCursor(input.cursor); + await input.authority.requireOwnership(); + + const rows = await getAppDatabase(database) + .select({ + nextReconcileAt: appDeploymentScriptsTable.nextReconcileAt, + scriptName: appDeploymentScriptsTable.scriptName, + }) + .from(appDeploymentScriptsTable) + .where( + and( + isNotNull(appDeploymentScriptsTable.nextReconcileAt), + lte(appDeploymentScriptsTable.nextReconcileAt, DATABASE_NOW_MS), + cursor === null + ? undefined + : or( + gt(appDeploymentScriptsTable.nextReconcileAt, cursor.nextReconcileAt), + and( + eq(appDeploymentScriptsTable.nextReconcileAt, cursor.nextReconcileAt), + gt(appDeploymentScriptsTable.scriptName, cursor.scriptName), + ), + ), + ), + ) + .orderBy( + asc(appDeploymentScriptsTable.nextReconcileAt), + asc(appDeploymentScriptsTable.scriptName), + ) + .limit(RECONCILIATION_PAGE_SIZE + 1) + .all(); + + const page = rows.slice(0, RECONCILIATION_PAGE_SIZE); + const failures: AppDeploymentScriptReconciliationFailure[] = []; + let armed = 0; + let cleaned = 0; + let deferred = 0; + + for (const row of page) { + await input.authority.requireOwnership(); + const disposition = await prepareReconciliation(database, { + authority: input.authority, + scriptName: row.scriptName, + }); + + if (disposition.kind === "armed") { + armed += 1; + continue; + } + if (disposition.kind === "deferred") { + deferred += 1; + continue; + } + + try { + await cleanup(disposition.action); + await input.authority.requireOwnership(); + const completed = await completeReconciliation(database, { + action: disposition.action, + authority: input.authority, + reconcileOwner: disposition.reconcileOwner, + }); + if (!completed) { + throw new Error("App deployment script reconciliation lost durable authority."); + } + cleaned += 1; + } catch (error) { + await input.authority.requireOwnership(); + const released = await failReconciliation(database, { + authority: input.authority, + reconcileOwner: disposition.reconcileOwner, + scriptName: disposition.action.scriptName, + }); + if (!released) { + throw error; + } + failures.push({ error, scriptName: disposition.action.scriptName }); + } + } + + const hasMore = rows.length > RECONCILIATION_PAGE_SIZE; + const lastRow = page.at(-1); + return { + armed, + cleaned, + deferred, + failures, + hasMore, + nextCursor: hasMore && lastRow !== undefined ? encodeCursor(lastRow) : null, + processed: page.length, + }; +} diff --git a/apps/api/src/modules/apps/application/app-deployment.service.ts b/apps/api/src/modules/apps/application/app-deployment.service.ts index b87cc9fa..61a75477 100644 --- a/apps/api/src/modules/apps/application/app-deployment.service.ts +++ b/apps/api/src/modules/apps/application/app-deployment.service.ts @@ -4,7 +4,7 @@ import type { DeleteAppDeploymentInput, DeployAppInput, } from "@mosoo/contracts/app"; -import type { ApiCommandId, AppDeploymentRunRow, AppDeploymentRow } from "@mosoo/db"; +import type { AppDeploymentRunRow, AppDeploymentRow } from "@mosoo/db"; import { apiCommandsTable, appDeploymentRunsTable, appDeploymentsTable } from "@mosoo/db"; import type { AppDeploymentId, AppDeploymentRunId, AppId } from "@mosoo/id"; import { createPlatformId } from "@mosoo/id"; @@ -12,42 +12,25 @@ import { and, desc, eq, inArray, isNull } from "drizzle-orm"; import type { ApiBindings } from "../../../platform/cloudflare/worker-types"; import { getAppDatabase, getD1ChangeCount } from "../../../platform/db/drizzle"; -import { API_ERROR_CODE, createApiError, validationError } from "../../../platform/errors"; +import { validationError } from "../../../platform/errors"; import { currentTimestampMs, toIsoString } from "../../../time"; import { createAppDeploymentRunDispatchDedupeKey, enqueueAppDeploymentRunDispatchCommand, } from "../../api-command/application/api-command-enqueue"; import { API_COMMAND_LEASE_MS } from "../../api-command/application/api-command-ledger"; -import { - APP_DEPLOYMENT_RUN_DISPATCH_MAX_ATTEMPTS, - APP_DEPLOYMENT_RUN_DISPATCH_RETRY_EXHAUSTED_CODE, - createAppDeploymentDispatchRetryExhaustedMessage, -} from "../../api-command/application/api-command-policy"; import type { AuthenticatedViewer } from "../../auth/application/viewer-auth.service"; import { ACTIVE_APP_DEPLOYMENT_RUN_STATUSES } from "../domain/app-deployment-lifecycle"; -import { - createCloudflareDeploymentClient, - deleteCloudflareDeploymentResources, - logCloudflareDeploymentResourceDeleteFailures, -} from "./app-deployment-cloudflare-client"; -import type { - CloudflareClientBindings, - CloudflareDeploymentClient, -} from "./app-deployment-cloudflare-client"; +import type { AppDeploymentAttemptIdentity } from "./app-deployment-executor.service"; import { destroyAppDeploymentRunSandboxesBestEffort } from "./app-deployment-executor.service"; import { ensureAppOwnership } from "./app.service"; import { normalizeLimit } from "./normalize-limit"; type AppDeploymentBindings = Pick< ApiBindings, - "API_COMMAND_QUEUE" | "DB" | "MOSOO_APP_DEPLOYMENT_DOMAIN" + "API_COMMAND_QUEUE" | "APP_DEPLOYMENT_WORKFLOW" | "DB" | "MOSOO_APP_DEPLOYMENT_DOMAIN" >; -type AppDeploymentDeleteBindings = Pick< - AppDeploymentBindings, - "DB" | "MOSOO_APP_DEPLOYMENT_DOMAIN" -> & - CloudflareClientBindings & +type AppDeploymentDeleteBindings = Pick & Partial>; export type AppDeploymentReadBindings = Pick< @@ -56,7 +39,6 @@ export type AppDeploymentReadBindings = Pick< >; interface AppDeploymentServiceOptions { - cloudflareClient?: CloudflareDeploymentClient; fetch?: typeof fetch; nowMs?: () => number; } @@ -75,7 +57,7 @@ export async function readAppDeploymentForOwnedApp( return null; } - await recoverStaleActiveDeploymentRun(bindings.DB, appId); + await recoverActiveDeploymentRun(bindings.DB, appId); const latestRun = await readLatestDeploymentRun(bindings.DB, appId); @@ -97,7 +79,7 @@ export async function getAppDeploymentStatus( appId: AppId, ): Promise { await ensureAppOwnership(bindings.DB, viewer.id, appId); - await recoverStaleActiveDeploymentRun(bindings.DB, appId); + await recoverActiveDeploymentRun(bindings.DB, appId); const run = await readLatestDeploymentRun(bindings.DB, appId); @@ -117,7 +99,7 @@ export async function listAppDeploymentRuns( limit?: number | null, ): Promise { await ensureAppOwnership(bindings.DB, viewer.id, appId); - await recoverStaleActiveDeploymentRun(bindings.DB, appId); + await recoverActiveDeploymentRun(bindings.DB, appId); const runLimit = normalizeLimit(limit, "limit", RUN_LIST_LIMITS); const runs = await getAppDatabase(bindings.DB) @@ -159,9 +141,7 @@ export async function deployApp( input.repoUrl, options.fetch ?? globalThis.fetch, ); - const activeRun = await readActiveDeploymentRun(bindings.DB, input.appId, { - recoverMissingDispatch: true, - }); + const activeRun = await recoverActiveDeploymentRun(bindings.DB, input.appId); if (activeRun !== null) { throw validationError("An App deployment run is already active."); @@ -172,6 +152,7 @@ export async function deployApp( const deployment = existingDeployment ?? ({ + activeScriptName: null, appId: input.appId, createdAt: nowMs, defaultBranch: repository.defaultBranch, @@ -199,18 +180,6 @@ export async function deployApp( if (getD1ChangeCount(insertDeploymentResult) === 0) { throw validationError("An App deployment is already active."); } - } else { - await getAppDatabase(bindings.DB) - .update(appDeploymentsTable) - .set({ - defaultBranch: repository.defaultBranch, - repoName: repository.repoName, - repoOwner: repository.repoOwner, - repoUrl: repository.repoUrl, - updatedAt: nowMs, - }) - .where(eq(appDeploymentsTable.id, deployment.id)) - .run(); } const insertRunResult = await getAppDatabase(bindings.DB) @@ -249,7 +218,14 @@ export async function deployApp( try { linkRunResult = await getAppDatabase(bindings.DB) .update(appDeploymentsTable) - .set({ latestRunId: runId, updatedAt: nowMs }) + .set({ + defaultBranch: repository.defaultBranch, + latestRunId: runId, + repoName: repository.repoName, + repoOwner: repository.repoOwner, + repoUrl: repository.repoUrl, + updatedAt: nowMs, + }) .where(and(eq(appDeploymentsTable.id, deployment.id), isNull(appDeploymentsTable.deletedAt))) .run(); } catch (error) { @@ -326,70 +302,79 @@ export async function deleteAppDeployment( return { ok: true }; } - const activeRunIds = await readActiveDeploymentRunIds(bindings.DB, input.appId); - const nowMs = currentTimestampMs(); - - await getAppDatabase(bindings.DB) - .update(appDeploymentRunsTable) - .set({ - errorCode: "deployment_deleted", - errorMessage: "Deployment was deleted.", - status: "failed", - updatedAt: nowMs, - }) - .where( - and( - eq(appDeploymentRunsTable.appId, input.appId), - inArray(appDeploymentRunsTable.status, ACTIVE_APP_DEPLOYMENT_RUN_STATUSES), - ), - ) - .run(); - - await destroyActiveDeploymentRunSandboxes(bindings, activeRunIds); - - const deleteFailures = await deleteCloudflareDeploymentResources( - options.cloudflareClient ?? createCloudflareDeploymentClient(bindings), - { - hostname: createPlannedHost(deployment.mosooSubdomain, bindings.MOSOO_APP_DEPLOYMENT_DOMAIN), - resourceName: deployment.mosooSubdomain, - }, - ); + const activeRunAttempts = await readActiveDeploymentRunAttempts(bindings.DB, deployment.id); + const nowMs = options.nowMs?.() ?? currentTimestampMs(); - if (deleteFailures.length > 0) { - logCloudflareDeploymentResourceDeleteFailures( - "app-deployment.cloudflare_delete_failed", - deleteFailures, - ); - throw createApiError( - API_ERROR_CODE.appDeploymentCleanupFailed, - "Cloudflare deployment cleanup failed. Retry deletion.", - ); - } + await tombstoneAppDeployment(bindings.DB, deployment.id, nowMs); - await getAppDatabase(bindings.DB) - .update(appDeploymentsTable) - .set({ - deletedAt: nowMs, - lastSuccessfulUrl: null, - updatedAt: nowMs, - }) - .where(eq(appDeploymentsTable.id, deployment.id)) - .run(); + await destroyActiveDeploymentRunSandboxes(bindings, activeRunAttempts); return { ok: true }; } +async function tombstoneAppDeployment( + database: D1Database, + deploymentId: AppDeploymentId, + nowMs: number, +): Promise { + await database.batch([ + database + .prepare( + `UPDATE app_deployment + SET active_script_name = NULL, + deleted_at = ?, + last_successful_url = NULL, + updated_at = ? + WHERE id = ? AND deleted_at IS NULL`, + ) + .bind(nowMs, nowMs, deploymentId), + database + .prepare( + `UPDATE api_command + SET claim_expires_at = NULL, + claim_owner = NULL, + completed_at = ?, + last_error_code = 'deployment_deleted', + last_error_message = 'Deployment was deleted.', + status = 'failed', + updated_at = ? + WHERE kind = 'app_deployment_run_dispatch' + AND status IN ('queued', 'running') + AND EXISTS ( + SELECT 1 + FROM app_deployment_run AS run + WHERE run.deployment_id = ? + AND run.status IN ('queued', 'preparing', 'building', 'submitting', 'submitted', 'activating') + AND json_valid(api_command.payload_json) = 1 + AND json_extract(api_command.payload_json, '$.appDeploymentRunId') = run.id + )`, + ) + .bind(nowMs, nowMs, deploymentId), + database + .prepare( + `UPDATE app_deployment_run + SET error_code = 'deployment_deleted', + error_message = 'Deployment was deleted.', + status = 'failed', + updated_at = ? + WHERE deployment_id = ? + AND status IN ('queued', 'preparing', 'building', 'submitting', 'submitted', 'activating')`, + ) + .bind(nowMs, deploymentId), + ]); +} + async function destroyActiveDeploymentRunSandboxes( bindings: AppDeploymentDeleteBindings, - runIds: readonly AppDeploymentRunId[], + runs: ReadonlyArray<{ attempt: AppDeploymentAttemptIdentity; runId: AppDeploymentRunId }>, ): Promise { if (!hasRuntimeSubjectDestroyBinding(bindings)) { return; } await Promise.all( - runIds.map((runId) => - destroyAppDeploymentRunSandboxesBestEffort(bindings as ApiBindings, runId), + runs.map(({ attempt, runId }) => + destroyAppDeploymentRunSandboxesBestEffort(bindings as ApiBindings, runId, attempt), ), ); } @@ -412,22 +397,38 @@ async function readActiveDeployment( ); } -async function readActiveDeploymentRunIds( +async function readActiveDeploymentRunAttempts( database: D1Database, - appId: AppId, -): Promise { - const rows = await getAppDatabase(database) - .select({ id: appDeploymentRunsTable.id }) - .from(appDeploymentRunsTable) - .where( - and( - eq(appDeploymentRunsTable.appId, appId), - inArray(appDeploymentRunsTable.status, ACTIVE_APP_DEPLOYMENT_RUN_STATUSES), - ), + deploymentId: AppDeploymentId, +): Promise> { + const { results } = await database + .prepare( + `SELECT command.attempt_count AS attemptCount, + command.delivery_generation AS deliveryGeneration, + run.id AS runId + FROM app_deployment_run AS run + JOIN api_command AS command + ON command.kind = 'app_deployment_run_dispatch' + AND json_valid(command.payload_json) = 1 + AND json_extract(command.payload_json, '$.appDeploymentRunId') = run.id + WHERE run.deployment_id = ? + AND run.status IN ('queued', 'preparing', 'building', 'submitting', 'submitted', 'activating') + AND command.attempt_count > 0`, ) - .all(); - - return rows.map((row) => row.id); + .bind(deploymentId) + .all<{ + attemptCount: number; + deliveryGeneration: number; + runId: AppDeploymentRunId; + }>(); + + return results.map((row) => ({ + attempt: { + attemptCount: row.attemptCount, + deliveryGeneration: row.deliveryGeneration, + }, + runId: row.runId, + })); } async function readDeploymentById( @@ -477,10 +478,9 @@ async function readLatestDeploymentRun( ); } -async function readActiveDeploymentRun( +async function recoverActiveDeploymentRun( database: D1Database, appId: AppId, - options: { recoverMissingDispatch?: boolean } = {}, ): Promise | null> { const run = (await getAppDatabase(database) @@ -503,19 +503,10 @@ async function readActiveDeploymentRun( return null; } - if (options.recoverMissingDispatch !== true) { - return run; - } - const nowMs = currentTimestampMs(); const dispatchCommand = (await getAppDatabase(database) .select({ - attemptCount: apiCommandsTable.attemptCount, - claimExpiresAt: apiCommandsTable.claimExpiresAt, - id: apiCommandsTable.id, - lastErrorCode: apiCommandsTable.lastErrorCode, - lastErrorMessage: apiCommandsTable.lastErrorMessage, status: apiCommandsTable.status, }) .from(apiCommandsTable) @@ -523,40 +514,7 @@ async function readActiveDeploymentRun( .limit(1) .get()) ?? null; - const dispatchRetryExhausted = - dispatchCommand !== null && - (dispatchCommand.status === "queued" || dispatchCommand.status === "running") && - dispatchCommand.attemptCount >= APP_DEPLOYMENT_RUN_DISPATCH_MAX_ATTEMPTS && - dispatchCommand.lastErrorCode !== null; - - if (dispatchRetryExhausted) { - const errorMessage = createAppDeploymentDispatchRetryExhaustedMessage({ - attemptCount: dispatchCommand.attemptCount, - lastErrorMessage: dispatchCommand.lastErrorMessage ?? dispatchCommand.lastErrorCode, - }); - - await markDeploymentRunFailed( - database, - run.id, - APP_DEPLOYMENT_RUN_DISPATCH_RETRY_EXHAUSTED_CODE, - new Error(errorMessage), - nowMs, - ); - await markDeploymentDispatchCommandFailed(database, dispatchCommand.id, { - errorCode: APP_DEPLOYMENT_RUN_DISPATCH_RETRY_EXHAUSTED_CODE, - errorMessage, - nowMs, - }); - - return null; - } - - if ( - dispatchCommand?.status === "queued" || - (dispatchCommand?.status === "running" && - dispatchCommand.claimExpiresAt !== null && - dispatchCommand.claimExpiresAt > nowMs) - ) { + if (dispatchCommand?.status === "queued" || dispatchCommand?.status === "running") { return run; } @@ -564,55 +522,17 @@ async function readActiveDeploymentRun( return run; } - const staleDispatch = - dispatchCommand?.status === "running" && - dispatchCommand.claimExpiresAt !== null && - dispatchCommand.claimExpiresAt <= nowMs; - await markDeploymentRunFailed( database, run.id, - staleDispatch ? "deployment_dispatch_expired" : "deployment_dispatch_missing", - new Error( - staleDispatch - ? "Deployment dispatch claim expired before completion." - : "Deployment dispatch command is missing.", - ), + "deployment_dispatch_missing", + new Error("Deployment dispatch command is missing."), nowMs, ); return null; } -async function markDeploymentDispatchCommandFailed( - database: D1Database, - commandId: ApiCommandId, - input: { errorCode: string; errorMessage: string; nowMs: number }, -): Promise { - await getAppDatabase(database) - .update(apiCommandsTable) - .set({ - claimExpiresAt: null, - claimOwner: null, - completedAt: input.nowMs, - lastErrorCode: input.errorCode, - lastErrorMessage: input.errorMessage, - status: "failed", - updatedAt: input.nowMs, - }) - .where( - and( - eq(apiCommandsTable.id, commandId), - inArray(apiCommandsTable.status, ["queued", "running"]), - ), - ) - .run(); -} - -async function recoverStaleActiveDeploymentRun(database: D1Database, appId: AppId): Promise { - await readActiveDeploymentRun(database, appId, { recoverMissingDispatch: true }); -} - async function markDeploymentRunFailed( database: D1Database, runId: AppDeploymentRunId, diff --git a/apps/api/src/modules/auth/application/personal-access-token.service.ts b/apps/api/src/modules/auth/application/personal-access-token.service.ts index fce8273b..fcaa6cef 100644 --- a/apps/api/src/modules/auth/application/personal-access-token.service.ts +++ b/apps/api/src/modules/auth/application/personal-access-token.service.ts @@ -62,7 +62,7 @@ export function isPersonalAccessTokenValue(tokenValue: string): boolean { export async function hashTokenValue(tokenValue: string): Promise { const encoded = new TextEncoder().encode(tokenValue); const digest = await crypto.subtle.digest("SHA-256", encoded); - return [...new Uint8Array(digest)].map((byte) => byte.toString(16).padStart(2, "0")).join(""); + return new Uint8Array(digest).toHex(); } function toTokenSummary(row: PersonalAccessTokenListRow): PersonalAccessTokenSummary { diff --git a/apps/api/src/modules/cost/application/cost-rollup.service.ts b/apps/api/src/modules/cost/application/cost-rollup.service.ts index b8b9b2c7..4b710ba0 100644 --- a/apps/api/src/modules/cost/application/cost-rollup.service.ts +++ b/apps/api/src/modules/cost/application/cost-rollup.service.ts @@ -1,4 +1,9 @@ -import { usageDailyRollupsTable, usageEventRollupReceiptsTable, usageEventsTable } from "@mosoo/db"; +import { + sessionModelCallsTable, + usageDailyRollupsTable, + usageEventRollupReceiptsTable, + usageEventsTable, +} from "@mosoo/db"; import { lt, sql } from "drizzle-orm"; import type { ApiBindings } from "../../../platform/cloudflare/worker-types"; @@ -55,6 +60,26 @@ export async function runUsageDailyRollup(env: ApiBindings, now = new Date()): P // Drizzle's D1 batch cannot prepare parameterized db.run(sql) queries. const cutoffSql = sql.raw(String(cutoffMs)); const rolledUpAtSql = sql.raw(String(rolledUpAtMs)); + const eligibleUsage = sql` + ${usageEventsTable.createdAt} < ${cutoffSql} + AND ( + ${usageEventsTable.source} <> 'runtime_driver' + OR EXISTS ( + SELECT 1 + FROM ${sessionModelCallsTable} AS rollup_model_call + WHERE rollup_model_call.session_id = ${usageEventsTable.sessionId} + AND rollup_model_call.session_run_id = ${usageEventsTable.sessionRunId} + AND ${usageEventsTable.sourceEventId} = + rollup_model_call.driver_instance_id || ':' || + CASE + WHEN trim(COALESCE(rollup_model_call.native_call_id, '')) <> '' + THEN trim(rollup_model_call.native_call_id) + ELSE rollup_model_call.session_run_id || ':' || rollup_model_call.call_key + END + AND rollup_model_call.source_event_seq >= ${usageEventsTable.sourceEventSeq} + ) + ) + `; await runAppDatabaseBatch(env.DB, (db) => [ db.run(sql` @@ -98,7 +123,7 @@ export async function runUsageDailyRollup(env: ApiBindings, now = new Date()): P SUM(CASE WHEN ${usageEventsTable.pricingStatus} = 'unknown' THEN 1 ELSE 0 END) AS unpriced_request_count FROM ${usageEventsTable} - WHERE ${usageEventsTable.createdAt} < ${cutoffSql} + WHERE ${eligibleUsage} GROUP BY ${usageEventsTable.organizationId}, ${usageEventsTable.appId}, @@ -140,10 +165,10 @@ export async function runUsageDailyRollup(env: ApiBindings, now = new Date()): P ${usageEventsTable.sourceEventId}, ${rolledUpAtSql} FROM ${usageEventsTable} - WHERE ${usageEventsTable.createdAt} < ${cutoffSql} + WHERE ${eligibleUsage} ON CONFLICT(source, source_event_id) DO NOTHING `), - db.delete(usageEventsTable).where(lt(usageEventsTable.createdAt, cutoffMs)), + db.delete(usageEventsTable).where(eligibleUsage), db.delete(usageDailyRollupsTable).where(createDailyRollupRetentionPredicate(now)), db .delete(usageEventRollupReceiptsTable) diff --git a/apps/api/src/modules/cost/application/cost-usage-event.service.ts b/apps/api/src/modules/cost/application/cost-usage-event.service.ts index 344397d6..96c1e0df 100644 --- a/apps/api/src/modules/cost/application/cost-usage-event.service.ts +++ b/apps/api/src/modules/cost/application/cost-usage-event.service.ts @@ -12,7 +12,8 @@ import type { SessionId, SessionRunId, } from "@mosoo/id"; -import { and, eq, sql } from "drizzle-orm"; +import { and, eq, exists, gte, notExists, or, sql } from "drizzle-orm"; +import type { SQL } from "drizzle-orm"; import { getAppDatabase } from "../../../platform/db/drizzle"; import type { AppDatabase } from "../../../platform/db/drizzle"; @@ -46,9 +47,26 @@ export interface RecordRuntimeUsageEventInput { driverInstanceId: DriverInstanceId; nativeCallId: string | null; run: RuntimeUsageRunContext; + sourceEventSeq?: number; usage: SessionUsageSummary; } +function selectedValue(value: Value, alias: string) { + return sql`${value}`.as(alias); +} + +function normalizeSourceEventSeq(value: number | undefined): number { + if (value === undefined) { + return 0; + } + + if (!Number.isSafeInteger(value) || value < 0) { + throw new Error("Runtime usage source event seq must be a non-negative safe integer."); + } + + return value; +} + function toTokenCount(value: number | null | undefined): number { if (typeof value !== "number" || !Number.isFinite(value) || value < 0) { return 0; @@ -57,6 +75,10 @@ function toTokenCount(value: number | null | undefined): number { return Math.round(value); } +function isProvidedTokenCount(value: number | null | undefined): boolean { + return typeof value === "number" && Number.isFinite(value) && value >= 0; +} + function isUsageContract(value: string | null | undefined): value is UsageContract { return ( value === "anthropic_bucketed" || @@ -127,6 +149,17 @@ export function hasRecordableRuntimeUsage(usage: SessionUsageSummary): boolean { ); } +function hasRuntimeUsageMutation(input: RecordRuntimeUsageEventInput): boolean { + return ( + hasRecordableRuntimeUsage(input.usage) || + (input.sourceEventSeq !== undefined && + (isProvidedTokenCount(input.usage.inputTokens) || + isProvidedTokenCount(input.usage.outputTokens) || + isProvidedTokenCount(input.usage.cachedReadTokens) || + isProvidedTokenCount(input.usage.cachedWriteTokens))) + ); +} + const RUNTIME_USAGE_SOURCE = "runtime_driver"; function resolveUsageEventIdentity(input: RecordRuntimeUsageEventInput): { @@ -158,14 +191,79 @@ async function isUsageEventAlreadyRolledUp( return existing.length > 0; } -function createRuntimeUsageEventInsert(database: AppDatabase, input: RecordRuntimeUsageEventInput) { +export async function hasRuntimeUsageEventRollupReceipt( + database: D1Database, + input: RecordRuntimeUsageEventInput, +): Promise { + return isUsageEventAlreadyRolledUp(getAppDatabase(database), resolveUsageEventIdentity(input)); +} + +export function createRuntimeUsageEventUnrolledPredicate( + database: AppDatabase, + input: RecordRuntimeUsageEventInput, +) { + const identity = resolveUsageEventIdentity(input); + + return notExists( + database + .select({ source: usageEventRollupReceiptsTable.source }) + .from(usageEventRollupReceiptsTable) + .where( + and( + eq(usageEventRollupReceiptsTable.source, identity.source), + eq(usageEventRollupReceiptsTable.sourceEventId, identity.sourceEventId), + ), + ), + ); +} + +export function createRuntimeUsageEventConvergencePredicate( + database: AppDatabase, + input: RecordRuntimeUsageEventInput, +) { + const identity = resolveUsageEventIdentity(input); + const identityPredicate = and( + eq(usageEventsTable.source, identity.source), + eq(usageEventsTable.sourceEventId, identity.sourceEventId), + ); + const converged = exists( + database + .select({ source: usageEventsTable.source }) + .from(usageEventsTable) + .where( + and( + identityPredicate, + eq(usageEventsTable.sessionId, input.run.sessionId), + eq(usageEventsTable.sessionRunId, input.run.sessionRunId), + eq(usageEventsTable.model, input.run.model), + eq(usageEventsTable.provider, input.run.provider), + eq(usageEventsTable.usageContract, requireUsageContract(input.usage)), + gte(usageEventsTable.sourceEventSeq, normalizeSourceEventSeq(input.sourceEventSeq)), + ), + ), + ); + + return hasRecordableRuntimeUsage(input.usage) + ? converged + : or( + notExists( + database + .select({ source: usageEventsTable.source }) + .from(usageEventsTable) + .where(identityPredicate), + ), + converged, + ); +} + +function prepareRuntimeUsageEventValues(input: RecordRuntimeUsageEventInput) { const rawInputTokens = toTokenCount(input.usage.inputTokens); const rawOutputTokens = toTokenCount(input.usage.outputTokens); const rawCacheReadTokens = toTokenCount(input.usage.cachedReadTokens); const rawCacheCreationTokens = toTokenCount(input.usage.cachedWriteTokens); const providedCostUsd = toProvidedUsdCost(input.usage); - if (!hasRecordableRuntimeUsage(input.usage)) { + if (!hasRuntimeUsageMutation(input)) { return null; } @@ -190,8 +288,7 @@ function createRuntimeUsageEventInsert(database: AppDatabase, input: RecordRunti provider, }); const { source, sourceEventId } = resolveUsageEventIdentity(input); - - return database.insert(usageEventsTable).values({ + const values = { actorUserId: input.run.actorUserId, agentId: input.run.agentId, agentOwnerUserId: input.run.agentOwnerUserId, @@ -215,34 +312,189 @@ function createRuntimeUsageEventInsert(database: AppDatabase, input: RecordRunti sessionRunId: input.run.sessionRunId, source, sourceEventId, + sourceEventSeq: normalizeSourceEventSeq(input.sourceEventSeq), totalCostUsdMicros: toUsdMicros(cost.totalCostUsd), usageContract, - }); + } satisfies typeof usageEventsTable.$inferInsert; + + return { providedCostUsd, values }; +} + +function createRuntimeUsageEventInsert( + database: AppDatabase, + input: RecordRuntimeUsageEventInput, + writeFence?: SQL, +) { + const prepared = prepareRuntimeUsageEventValues(input); + + if (prepared === null) { + return null; + } + + const { values } = prepared; + const identity = resolveUsageEventIdentity(input); + const mayCreate = hasRecordableRuntimeUsage(input.usage) + ? undefined + : exists( + database + .select({ source: usageEventsTable.source }) + .from(usageEventsTable) + .where( + and( + eq(usageEventsTable.source, identity.source), + eq(usageEventsTable.sourceEventId, identity.sourceEventId), + ), + ), + ); + + return { + prepared, + query: database.insert(usageEventsTable).select( + database + .select({ + actorUserId: selectedValue(values.actorUserId, "actor_user_id"), + agentId: selectedValue(values.agentId, "agent_id"), + agentOwnerUserId: selectedValue(values.agentOwnerUserId, "agent_owner_user_id"), + agentPublicationStateAtRun: selectedValue( + values.agentPublicationStateAtRun, + "agent_publication_state_at_run", + ), + agentRevisionId: selectedValue(values.agentRevisionId, "agent_revision_id"), + cacheCreationTokens: selectedValue(values.cacheCreationTokens, "cache_creation_tokens"), + cacheReadTokens: selectedValue(values.cacheReadTokens, "cache_read_tokens"), + createdAt: selectedValue(values.createdAt, "created_at"), + id: selectedValue(values.id, "id"), + inputTokens: selectedValue(values.inputTokens, "input_tokens"), + model: selectedValue(values.model, "model"), + organizationId: selectedValue(values.organizationId, "organization_id"), + appId: selectedValue(values.appId, "app_id"), + outputTokens: selectedValue(values.outputTokens, "output_tokens"), + priceSnapshotJson: selectedValue(values.priceSnapshotJson, "price_snapshot_json"), + pricingStatus: selectedValue(values.pricingStatus, "pricing_status"), + provider: selectedValue(values.provider, "provider"), + runPurpose: selectedValue(values.runPurpose, "run_purpose"), + runtimeId: selectedValue(values.runtimeId, "runtime_id"), + sessionId: selectedValue(values.sessionId, "session_id"), + sessionRunId: selectedValue(values.sessionRunId, "session_run_id"), + source: selectedValue(values.source, "source"), + sourceEventId: selectedValue(values.sourceEventId, "source_event_id"), + sourceEventSeq: selectedValue(values.sourceEventSeq, "source_event_seq"), + totalCostUsdMicros: selectedValue(values.totalCostUsdMicros, "total_cost_usd_micros"), + usageContract: selectedValue(values.usageContract, "usage_contract"), + }) + .from(sql`(SELECT 1)`) + .where( + and(createRuntimeUsageEventUnrolledPredicate(database, input), writeFence, mayCreate), + ), + ), + }; } export function createRuntimeUsageEventUpsert( database: AppDatabase, input: RecordRuntimeUsageEventInput, + writeFence?: SQL, ) { - const query = createRuntimeUsageEventInsert(database, input); + const insert = createRuntimeUsageEventInsert(database, input, writeFence); - if (query === null) { + if (insert === null) { return null; } + const { prepared, query } = insert; + + const sourceEventSeqFence = + input.sourceEventSeq === undefined + ? eq(usageEventsTable.sourceEventSeq, 0) + : sql`${usageEventsTable.sourceEventSeq} < excluded.source_event_seq`; + const cacheCreationTokensProvided = isProvidedTokenCount(input.usage.cachedWriteTokens); + const cacheReadTokensProvided = isProvidedTokenCount(input.usage.cachedReadTokens); + const inputTokensProvided = isProvidedTokenCount(input.usage.inputTokens); + const outputTokensProvided = isProvidedTokenCount(input.usage.outputTokens); + const mergedCacheCreationTokens = cacheCreationTokensProvided + ? sql`excluded.cache_creation_tokens` + : sql`${usageEventsTable.cacheCreationTokens}`; + const mergedCacheReadTokens = cacheReadTokensProvided + ? sql`excluded.cache_read_tokens` + : sql`${usageEventsTable.cacheReadTokens}`; + const mergedInputTokens = + input.usage.usageContract !== "anthropic_bucketed" + ? inputTokensProvided + ? sql`excluded.input_tokens` + : sql`${usageEventsTable.inputTokens}` + : inputTokensProvided + ? cacheReadTokensProvided + ? sql`excluded.input_tokens` + : sql`excluded.input_tokens + ${usageEventsTable.cacheReadTokens}` + : cacheReadTokensProvided + ? sql`${usageEventsTable.inputTokens} - ${usageEventsTable.cacheReadTokens} + excluded.cache_read_tokens` + : sql`${usageEventsTable.inputTokens}`; + const mergedOutputTokens = outputTokensProvided + ? sql`excluded.output_tokens` + : sql`${usageEventsTable.outputTokens}`; + const mergedHasTokens = sql`( + ${mergedCacheCreationTokens} > 0 OR + ${mergedCacheReadTokens} > 0 OR + ${mergedInputTokens} > 0 OR + ${mergedOutputTokens} > 0 + )`; + const preserveRuntimeReportedCost = sql`( + ${prepared.providedCostUsd === null} AND + NOT ${mergedHasTokens} AND + json_extract(${usageEventsTable.priceSnapshotJson}, '$.source') = 'runtime_reported_usd' + )`; + const billableInputTokens = sql`MAX(0, ${mergedInputTokens} - ${mergedCacheReadTokens})`; + const pricingRateSnapshot = sql`CASE + WHEN ${inputTokensProvided} OR + json_extract(${usageEventsTable.priceSnapshotJson}, '$.source') IS NOT 'mosoo_seed_2026_07_10' + THEN excluded.price_snapshot_json + ELSE ${usageEventsTable.priceSnapshotJson} + END`; + const priceSnapshotJson = sql`CASE + WHEN excluded.pricing_status <> 'priced' THEN NULL + WHEN ${preserveRuntimeReportedCost} THEN ${usageEventsTable.priceSnapshotJson} + WHEN ${mergedHasTokens} THEN + json_set(${pricingRateSnapshot}, '$.billableInputTokens', ${billableInputTokens}) + ELSE excluded.price_snapshot_json + END`; + const totalCostUsdMicros = sql`CASE + WHEN excluded.pricing_status = 'priced' AND ${preserveRuntimeReportedCost} + THEN ${usageEventsTable.totalCostUsdMicros} + WHEN excluded.pricing_status = 'priced' AND ${mergedHasTokens} THEN + CAST(ROUND( + ${billableInputTokens} * json_extract(${pricingRateSnapshot}, '$.inputUsdPerMillion') + + ${mergedOutputTokens} * json_extract(${pricingRateSnapshot}, '$.outputUsdPerMillion') + + ${mergedCacheReadTokens} * json_extract(${pricingRateSnapshot}, '$.cacheReadUsdPerMillion') + + ${mergedCacheCreationTokens} * json_extract(${pricingRateSnapshot}, '$.cacheWriteUsdPerMillion') + ) AS INTEGER) + WHEN excluded.pricing_status = 'unknown' AND ${prepared.providedCostUsd === null} + THEN ${usageEventsTable.totalCostUsdMicros} + ELSE excluded.total_cost_usd_micros + END`; + const identityFence = sql` + ${usageEventsTable.sessionId} IS excluded.session_id AND + ${usageEventsTable.sessionRunId} IS excluded.session_run_id AND + ${usageEventsTable.model} = excluded.model AND + ${usageEventsTable.provider} = excluded.provider AND + ${usageEventsTable.usageContract} = excluded.usage_contract + `; + const updateFence = sql`${sourceEventSeqFence} AND ${identityFence}`; + return query.onConflictDoUpdate({ set: { - cacheCreationTokens: sql`excluded.cache_creation_tokens`, - cacheReadTokens: sql`excluded.cache_read_tokens`, - inputTokens: sql`excluded.input_tokens`, + cacheCreationTokens: mergedCacheCreationTokens, + cacheReadTokens: mergedCacheReadTokens, + inputTokens: mergedInputTokens, model: sql`excluded.model`, - outputTokens: sql`excluded.output_tokens`, - priceSnapshotJson: sql`excluded.price_snapshot_json`, + outputTokens: mergedOutputTokens, + priceSnapshotJson, pricingStatus: sql`excluded.pricing_status`, provider: sql`excluded.provider`, - totalCostUsdMicros: sql`excluded.total_cost_usd_micros`, + sourceEventSeq: sql`excluded.source_event_seq`, + totalCostUsdMicros, usageContract: sql`excluded.usage_contract`, }, + setWhere: writeFence === undefined ? updateFence : sql`${updateFence} AND ${writeFence}`, target: [usageEventsTable.source, usageEventsTable.sourceEventId], }); } @@ -251,28 +503,59 @@ export function createRuntimeUsageEventInsertIfMissing( database: AppDatabase, input: RecordRuntimeUsageEventInput, ) { - const query = createRuntimeUsageEventInsert(database, input); + const insert = createRuntimeUsageEventInsert(database, input); - if (query === null) { + if (insert === null) { return null; } - return query.onConflictDoNothing({ + return insert.query.onConflictDoNothing({ target: [usageEventsTable.source, usageEventsTable.sourceEventId], }); } +function readRuntimeReportedUsd(priceSnapshotJson: string | null): number | null { + if (priceSnapshotJson === null) { + return null; + } + + try { + const snapshot = JSON.parse(priceSnapshotJson) as unknown; + + if ( + typeof snapshot !== "object" || + snapshot === null || + !("source" in snapshot) || + snapshot.source !== "runtime_reported_usd" || + !("reportedCostUsd" in snapshot) || + typeof snapshot.reportedCostUsd !== "number" || + !Number.isFinite(snapshot.reportedCostUsd) || + snapshot.reportedCostUsd < 0 + ) { + return null; + } + + return snapshot.reportedCostUsd; + } catch { + return null; + } +} + export async function recordRuntimeUsageEvent( database: D1Database, input: RecordRuntimeUsageEventInput, ): Promise { - if (!hasRecordableRuntimeUsage(input.usage)) { + if (!hasRuntimeUsageMutation(input)) { return; } const appDatabase = getAppDatabase(database); if (await isUsageEventAlreadyRolledUp(appDatabase, resolveUsageEventIdentity(input))) { + if (input.sourceEventSeq !== undefined) { + throw new Error("Runtime usage event was already rolled up and cannot be replaced safely."); + } + return; } @@ -283,4 +566,116 @@ export async function recordRuntimeUsageEvent( } await query.run(); + + if ( + input.sourceEventSeq !== undefined && + (await isUsageEventAlreadyRolledUp(appDatabase, resolveUsageEventIdentity(input))) + ) { + throw new Error("Runtime usage event was rolled up before its durable replay was verified."); + } + + if (input.sourceEventSeq !== undefined) { + const identity = resolveUsageEventIdentity(input); + const prepared = prepareRuntimeUsageEventValues(input); + + if (prepared === null) { + return; + } + const { values: expected } = prepared; + + const row = + (await appDatabase + .select({ + cacheCreationTokens: usageEventsTable.cacheCreationTokens, + cacheReadTokens: usageEventsTable.cacheReadTokens, + inputTokens: usageEventsTable.inputTokens, + model: usageEventsTable.model, + outputTokens: usageEventsTable.outputTokens, + priceSnapshotJson: usageEventsTable.priceSnapshotJson, + pricingStatus: usageEventsTable.pricingStatus, + provider: usageEventsTable.provider, + sessionId: usageEventsTable.sessionId, + sessionRunId: usageEventsTable.sessionRunId, + sourceEventSeq: usageEventsTable.sourceEventSeq, + totalCostUsdMicros: usageEventsTable.totalCostUsdMicros, + usageContract: usageEventsTable.usageContract, + }) + .from(usageEventsTable) + .where( + and( + eq(usageEventsTable.source, identity.source), + eq(usageEventsTable.sourceEventId, identity.sourceEventId), + ), + ) + .limit(1) + .get()) ?? null; + + if (row === null) { + if (!hasRecordableRuntimeUsage(input.usage)) { + return; + } + throw new Error("Runtime usage event CAS did not persist the durable event."); + } + + if (row.sourceEventSeq < input.sourceEventSeq) { + throw new Error("Runtime usage event CAS did not persist the durable event."); + } + + if ( + row.model !== expected.model || + row.provider !== expected.provider || + row.sessionId !== expected.sessionId || + row.sessionRunId !== expected.sessionRunId || + row.usageContract !== expected.usageContract + ) { + throw new Error("Runtime usage event identity conflicts with its durable event."); + } + + if (row.sourceEventSeq > input.sourceEventSeq) { + return; + } + + const tokensUnavailable = + row.cacheCreationTokens === 0 && + row.cacheReadTokens === 0 && + row.inputTokens === 0 && + row.outputTokens === 0; + const providedCostUsd = + prepared.providedCostUsd ?? + (tokensUnavailable ? readRuntimeReportedUsd(row.priceSnapshotJson) : null); + const mergedCost = calculateUsageCost({ + cacheCreationTokens: row.cacheCreationTokens, + cacheReadTokens: row.cacheReadTokens, + inputTokens: row.inputTokens, + model: row.model, + outputTokens: row.outputTokens, + pricedAtMs: input.run.createdAtMs, + providedCostUsd, + provider: row.provider, + }); + const costMustMatch = + (mergedCost.pricing !== null || providedCostUsd !== null) && + (tokensUnavailable || + isProvidedTokenCount(input.usage.inputTokens) || + mergedCost.pricing === null); + + if ( + (isProvidedTokenCount(input.usage.cachedWriteTokens) && + row.cacheCreationTokens !== expected.cacheCreationTokens) || + (isProvidedTokenCount(input.usage.cachedReadTokens) && + row.cacheReadTokens !== expected.cacheReadTokens) || + (isProvidedTokenCount(input.usage.inputTokens) && + (input.usage.usageContract === "anthropic_bucketed" && + !isProvidedTokenCount(input.usage.cachedReadTokens) + ? row.inputTokens - row.cacheReadTokens !== expected.inputTokens + : row.inputTokens !== expected.inputTokens)) || + (isProvidedTokenCount(input.usage.outputTokens) && + row.outputTokens !== expected.outputTokens) || + (costMustMatch && row.priceSnapshotJson !== mergedCost.priceSnapshotJson) || + (costMustMatch && row.pricingStatus !== mergedCost.pricingStatus) || + (costMustMatch && row.totalCostUsdMicros !== toUsdMicros(mergedCost.totalCostUsd)) + ) { + throw new Error("Runtime usage event seq was replayed with conflicting content."); + } + } } diff --git a/apps/api/src/modules/environments/application/environment-package-artifact-backup-store.ts b/apps/api/src/modules/environments/application/environment-package-artifact-backup-store.ts new file mode 100644 index 00000000..8a2f05eb --- /dev/null +++ b/apps/api/src/modules/environments/application/environment-package-artifact-backup-store.ts @@ -0,0 +1,826 @@ +import { + environmentPackageArtifactBackupsTable, + environmentPackageArtifactBackupStagingTable, +} from "@mosoo/db"; +import type { ApiCommandId } from "@mosoo/db"; +import type { AppId, SandboxBackupId } from "@mosoo/id"; +import { parsePlatformId } from "@mosoo/id"; +import { and, eq } from "drizzle-orm"; + +import { getAppDatabase } from "../../../platform/db/drizzle"; +import type { + EnvironmentPackageArtifactKey, + EnvironmentPackageArtifactPaths, +} from "../domain/environment-package-artifact"; +import { + environmentPackageArtifactDir, + ENVIRONMENT_PACKAGE_ARTIFACT_BACKUP_REFRESH_WINDOW_MS, + ENVIRONMENT_PACKAGE_ARTIFACT_BACKUP_TTL_SECONDS, + parseEnvironmentPackageArtifactPaths, +} from "../domain/environment-package-artifact"; + +const DATABASE_NOW_MS_SQL = "CAST(unixepoch('subsec') * 1000 AS INTEGER)"; + +export interface EnvironmentPackageArtifactBackupStage { + readonly actualBackupId: SandboxBackupId | null; + readonly appId: AppId; + readonly attemptCount: number; + readonly claimOwner: string; + readonly commandId: ApiCommandId; + readonly createdAt: number; + readonly deliveryGeneration: number; + readonly dir: string; + readonly inputDigest: string; + readonly paths: EnvironmentPackageArtifactPaths; + readonly updatedAt: number; +} + +export interface EnvironmentPackageArtifactBackupManifest extends EnvironmentPackageArtifactKey { + readonly attemptCount: number; + readonly backupId: SandboxBackupId; + readonly commandId: ApiCommandId; + readonly committedAt: number; + readonly deliveryGeneration: number; + readonly expiresAt: number; + readonly manifestGeneration: number; + readonly paths: EnvironmentPackageArtifactPaths; +} + +export interface EnvironmentPackageArtifactCommandAuthority extends EnvironmentPackageArtifactKey { + readonly attemptCount: number; + readonly commandId: ApiCommandId; + readonly deliveryGeneration: number; +} + +export type EnvironmentPackageArtifactBackupStageAuthority = Pick< + EnvironmentPackageArtifactBackupStage, + "attemptCount" | "claimOwner" | "commandId" | "deliveryGeneration" +>; + +function mapStage( + row: typeof environmentPackageArtifactBackupStagingTable.$inferSelect, +): EnvironmentPackageArtifactBackupStage { + const artifactDir = environmentPackageArtifactDir(row); + const paths = parseEnvironmentPackageArtifactPaths(JSON.parse(row.pathsJson), artifactDir); + if (row.dir !== artifactDir || paths === null) { + throw new Error("Environment package artifact backup stage paths are invalid."); + } + return { ...row, paths }; +} + +function mapManifest( + row: typeof environmentPackageArtifactBackupsTable.$inferSelect, +): EnvironmentPackageArtifactBackupManifest { + const paths = parseEnvironmentPackageArtifactPaths( + JSON.parse(row.pathsJson), + environmentPackageArtifactDir(row), + ); + if (paths === null) { + throw new Error("Environment package artifact backup manifest paths are invalid."); + } + return { ...row, paths }; +} + +export async function getEnvironmentPackageArtifactBackupManifest( + database: D1Database, + key: EnvironmentPackageArtifactKey, +): Promise { + const row = await getAppDatabase(database) + .select() + .from(environmentPackageArtifactBackupsTable) + .where( + and( + eq(environmentPackageArtifactBackupsTable.appId, key.appId), + eq(environmentPackageArtifactBackupsTable.inputDigest, key.inputDigest), + ), + ) + .limit(1) + .get(); + return row === undefined ? null : mapManifest(row); +} + +export async function retireExpiredEnvironmentPackageArtifactBackups( + database: D1Database, + limit: number, +): Promise { + if (!Number.isSafeInteger(limit) || limit <= 0) { + throw new TypeError("Environment package artifact retirement limit must be positive."); + } + const rows = await database + .prepare( + `DELETE FROM environment_package_artifact_backup + WHERE backup_id IN ( + SELECT artifact.backup_id + FROM environment_package_artifact_backup AS artifact + WHERE artifact.expires_at <= ${DATABASE_NOW_MS_SQL} + AND NOT EXISTS ( + SELECT 1 FROM sandbox_backup_delete_intent AS deletion + WHERE deletion.backup_id = artifact.backup_id + ) + AND NOT EXISTS ( + SELECT 1 FROM sandbox_backup AS backup + WHERE backup.id = artifact.backup_id AND backup.status = 'ready' + ) + AND NOT EXISTS ( + SELECT 1 FROM sandbox_backup_staging AS stage + WHERE stage.actual_backup_id = artifact.backup_id + ) + AND NOT EXISTS ( + SELECT 1 FROM environment_package_artifact_backup_staging AS stage + WHERE stage.actual_backup_id = artifact.backup_id + ) + ORDER BY artifact.expires_at, artifact.backup_id + LIMIT ? + ) + AND expires_at <= ${DATABASE_NOW_MS_SQL} + RETURNING backup_id`, + ) + .bind(limit) + .all<{ backup_id: SandboxBackupId }>(); + return rows.results.map((row) => row.backup_id); +} + +function commandMatchesStage(alias: string): string { + return `EXISTS ( + SELECT 1 FROM api_command AS command + WHERE command.id = ${alias}.command_id + AND command.kind = 'environment_package_artifact_build' + AND json_valid(command.payload_json) = 1 + AND json_extract(command.payload_json, '$.appId') = ${alias}.app_id + AND json_extract(command.payload_json, '$.inputDigest') = ${alias}.input_digest + AND command.delivery_generation = ${alias}.delivery_generation + AND command.attempt_count = ${alias}.attempt_count + AND command.claim_owner = ${alias}.claim_owner + )`; +} + +export async function getEnvironmentPackageArtifactBackupStage( + database: D1Database, + commandId: ApiCommandId, +): Promise { + const row = await getAppDatabase(database) + .select() + .from(environmentPackageArtifactBackupStagingTable) + .where(eq(environmentPackageArtifactBackupStagingTable.commandId, commandId)) + .limit(1) + .get(); + return row === undefined ? null : mapStage(row); +} + +function mapCommandAuthority(row: { + readonly attempt_count: number; + readonly delivery_generation: number; + readonly id: string; + readonly payload_json: string; +}): EnvironmentPackageArtifactCommandAuthority | null { + try { + const payload = JSON.parse(row.payload_json) as unknown; + if (typeof payload !== "object" || payload === null) { + return null; + } + const appId = Reflect.get(payload, "appId"); + const inputDigest = Reflect.get(payload, "inputDigest"); + if ( + typeof appId !== "string" || + typeof inputDigest !== "string" || + !Number.isSafeInteger(row.attempt_count) || + row.attempt_count <= 0 || + !Number.isSafeInteger(row.delivery_generation) || + row.delivery_generation <= 0 + ) { + return null; + } + return { + appId: parsePlatformId(appId, "environment artifact command app ID"), + attemptCount: row.attempt_count, + commandId: parsePlatformId(row.id, "environment artifact command ID"), + deliveryGeneration: row.delivery_generation, + inputDigest, + }; + } catch { + return null; + } +} + +export async function getEnvironmentPackageArtifactCommandIntent( + database: D1Database, + commandId: ApiCommandId, +): Promise { + const row = await database + .prepare( + `SELECT attempt_count, delivery_generation, id, payload_json + FROM api_command + WHERE id = ? AND kind = 'environment_package_artifact_build'`, + ) + .bind(commandId) + .first<{ + attempt_count: number; + delivery_generation: number; + id: string; + payload_json: string; + }>(); + return row === null ? null : mapCommandAuthority(row); +} + +export async function findEnvironmentPackageArtifactCommandAuthority( + database: D1Database, + key: EnvironmentPackageArtifactKey, +): Promise { + const rows = ( + await database + .prepare( + `SELECT attempt_count, delivery_generation, id, payload_json + FROM api_command + WHERE kind = 'environment_package_artifact_build' + AND json_valid(payload_json) = 1 + AND json_extract(payload_json, '$.appId') = ? + AND json_extract(payload_json, '$.inputDigest') = ? + ORDER BY id + LIMIT 2`, + ) + .bind(key.appId, key.inputDigest) + .all<{ + attempt_count: number; + delivery_generation: number; + id: string; + payload_json: string; + }>() + ).results; + return rows.length === 1 ? mapCommandAuthority(rows[0]!) : null; +} + +function manifestMatches( + manifest: EnvironmentPackageArtifactBackupManifest, + input: { + readonly backupId: SandboxBackupId; + readonly commandId: ApiCommandId; + readonly attemptCount: number; + readonly deliveryGeneration: number; + readonly expiresAt: number; + readonly key: EnvironmentPackageArtifactKey; + readonly paths: EnvironmentPackageArtifactPaths; + }, +): boolean { + return ( + manifest.backupId === input.backupId && + manifest.commandId === input.commandId && + manifest.attemptCount === input.attemptCount && + manifest.deliveryGeneration === input.deliveryGeneration && + manifest.expiresAt === input.expiresAt && + manifest.appId === input.key.appId && + manifest.inputDigest === input.key.inputDigest && + JSON.stringify(manifest.paths) === JSON.stringify(input.paths) + ); +} + +export async function commitEnvironmentPackageArtifactBackup( + database: D1Database, + input: EnvironmentPackageArtifactBackupStageAuthority & { + readonly actualBackupId: SandboxBackupId; + readonly expiresAt: number; + readonly key: EnvironmentPackageArtifactKey; + readonly paths: EnvironmentPackageArtifactPaths; + }, +): Promise { + const paths = parseEnvironmentPackageArtifactPaths( + input.paths, + environmentPackageArtifactDir(input.key), + ); + if (paths === null) { + throw new Error("Environment package artifact backup paths are invalid."); + } + const pathsJson = JSON.stringify(paths); + const previous = await getEnvironmentPackageArtifactBackupManifest(database, input.key); + if ( + previous !== null && + manifestMatches(previous, { ...input, backupId: input.actualBackupId }) + ) { + return true; + } + if (previous?.backupId === input.actualBackupId) { + return false; + } + const authoritySql = `EXISTS ( + SELECT 1 + FROM environment_package_artifact_backup_staging AS stage + JOIN api_command AS command ON command.id = stage.command_id + WHERE stage.command_id = ? AND stage.actual_backup_id = ? + AND stage.app_id = ? AND stage.input_digest = ? AND stage.dir = ? AND stage.paths_json = ? + AND stage.delivery_generation = ? AND stage.attempt_count = ? + AND stage.claim_owner = ? AND command.status = 'running' + AND command.claim_expires_at > ${DATABASE_NOW_MS_SQL} + AND ${commandMatchesStage("stage")} + AND NOT EXISTS ( + SELECT 1 FROM sandbox_backup_delete_intent AS deletion + WHERE deletion.backup_id = stage.actual_backup_id + ) + AND NOT EXISTS ( + SELECT 1 FROM sandbox_backup + WHERE id = stage.actual_backup_id + UNION ALL + SELECT 1 FROM sandbox_backup_staging + WHERE actual_backup_id = stage.actual_backup_id + ) + )`; + const bindings = [ + input.commandId, + input.actualBackupId, + input.key.appId, + input.key.inputDigest, + environmentPackageArtifactDir(input.key), + pathsJson, + input.deliveryGeneration, + input.attemptCount, + input.claimOwner, + ] as const; + const insert = async (): Promise => { + await database + .prepare( + `INSERT INTO environment_package_artifact_backup ( + app_id, attempt_count, backup_id, command_id, committed_at, + delivery_generation, expires_at, input_digest, manifest_generation, paths_json + ) + SELECT ?, ?, ?, ?, ${DATABASE_NOW_MS_SQL}, ?, ?, ?, 1, ? + WHERE ${authoritySql} + AND NOT EXISTS ( + SELECT 1 FROM environment_package_artifact_backup + WHERE backup_id = ? OR (app_id = ? AND input_digest = ?) + )`, + ) + .bind( + input.key.appId, + input.attemptCount, + input.actualBackupId, + input.commandId, + input.deliveryGeneration, + input.expiresAt, + input.key.inputDigest, + pathsJson, + ...bindings, + input.actualBackupId, + input.key.appId, + input.key.inputDigest, + ) + .run(); + }; + if (previous === null) { + await insert(); + } else { + await database + .prepare( + `UPDATE environment_package_artifact_backup + SET attempt_count = ?, backup_id = ?, command_id = ?, committed_at = ${DATABASE_NOW_MS_SQL}, + delivery_generation = ?, expires_at = ?, manifest_generation = manifest_generation + 1, + paths_json = ? + WHERE app_id = ? AND input_digest = ? AND manifest_generation = ? + AND backup_id = ? AND ${authoritySql} + AND NOT EXISTS ( + SELECT 1 FROM environment_package_artifact_backup AS other + WHERE other.backup_id = ? + AND (other.app_id <> ? OR other.input_digest <> ?) + )`, + ) + .bind( + input.attemptCount, + input.actualBackupId, + input.commandId, + input.deliveryGeneration, + input.expiresAt, + pathsJson, + input.key.appId, + input.key.inputDigest, + previous.manifestGeneration, + previous.backupId, + ...bindings, + input.actualBackupId, + input.key.appId, + input.key.inputDigest, + ) + .run(); + } + let manifest = await getEnvironmentPackageArtifactBackupManifest(database, input.key); + if (previous !== null && manifest === null) { + await insert(); + manifest = await getEnvironmentPackageArtifactBackupManifest(database, input.key); + } + return ( + manifest !== null && manifestMatches(manifest, { ...input, backupId: input.actualBackupId }) + ); +} + +export async function adoptLegacyEnvironmentPackageArtifactBackup( + database: D1Database, + input: EnvironmentPackageArtifactCommandAuthority & { + readonly actualBackupId: SandboxBackupId; + readonly expiresAt: number; + readonly key: EnvironmentPackageArtifactKey; + readonly paths: EnvironmentPackageArtifactPaths; + }, +): Promise { + const paths = parseEnvironmentPackageArtifactPaths( + input.paths, + environmentPackageArtifactDir(input.key), + ); + if (paths === null) { + throw new Error("Environment package artifact backup paths are invalid."); + } + const pathsJson = JSON.stringify(paths); + await database + .prepare( + `INSERT INTO environment_package_artifact_backup ( + app_id, attempt_count, backup_id, command_id, committed_at, + delivery_generation, expires_at, input_digest, manifest_generation, paths_json + ) + SELECT ?, command.attempt_count, ?, command.id, ${DATABASE_NOW_MS_SQL}, + command.delivery_generation, ?, ?, 1, ? + FROM api_command AS command + WHERE command.id = ? AND command.kind = 'environment_package_artifact_build' + AND command.status = 'succeeded' + AND command.completed_at IS NOT NULL + AND command.claim_owner IS NULL AND command.claim_expires_at IS NULL + AND command.attempt_count = ? AND command.delivery_generation = ? + AND ? > ${DATABASE_NOW_MS_SQL} + ? + AND ? <= ${DATABASE_NOW_MS_SQL} + ? + AND json_valid(command.payload_json) = 1 + AND json_extract(command.payload_json, '$.appId') = ? + AND json_extract(command.payload_json, '$.inputDigest') = ? + AND NOT EXISTS ( + SELECT 1 FROM sandbox_backup_delete_intent AS deletion + WHERE deletion.backup_id = ? + ) + AND NOT EXISTS ( + SELECT 1 FROM environment_package_artifact_backup AS artifact + WHERE artifact.backup_id = ? + OR (artifact.app_id = ? AND artifact.input_digest = ?) + ) + AND NOT EXISTS ( + SELECT 1 FROM environment_package_artifact_backup_staging AS stage + WHERE stage.actual_backup_id = ? + ) + AND NOT EXISTS ( + SELECT 1 FROM sandbox_backup + WHERE id = ? + UNION ALL + SELECT 1 FROM sandbox_backup_staging + WHERE actual_backup_id = ? + )`, + ) + .bind( + input.key.appId, + input.actualBackupId, + input.expiresAt, + input.key.inputDigest, + pathsJson, + input.commandId, + input.attemptCount, + input.deliveryGeneration, + input.expiresAt, + ENVIRONMENT_PACKAGE_ARTIFACT_BACKUP_REFRESH_WINDOW_MS, + input.expiresAt, + ENVIRONMENT_PACKAGE_ARTIFACT_BACKUP_TTL_SECONDS * 1_000, + input.key.appId, + input.key.inputDigest, + input.actualBackupId, + input.actualBackupId, + input.actualBackupId, + input.key.appId, + input.key.inputDigest, + input.actualBackupId, + input.actualBackupId, + ) + .run(); + const manifest = await getEnvironmentPackageArtifactBackupManifest(database, input.key); + return ( + manifest !== null && manifestMatches(manifest, { ...input, backupId: input.actualBackupId }) + ); +} + +export async function stageEnvironmentPackageArtifactBackup( + database: D1Database, + input: { + readonly attemptCount: number; + readonly claimOwner: string; + readonly commandId: ApiCommandId; + readonly deliveryGeneration: number; + readonly dir: string; + readonly key: EnvironmentPackageArtifactKey; + readonly paths: EnvironmentPackageArtifactPaths; + }, +): Promise { + if ( + !Number.isSafeInteger(input.deliveryGeneration) || + input.deliveryGeneration <= 0 || + !Number.isSafeInteger(input.attemptCount) || + input.attemptCount <= 0 + ) { + throw new Error("Environment package artifact attempt must be a positive safe integer."); + } + const artifactDir = environmentPackageArtifactDir(input.key); + const paths = parseEnvironmentPackageArtifactPaths(input.paths, artifactDir); + if (input.dir !== artifactDir || paths === null) { + throw new Error("Environment package artifact backup paths are invalid."); + } + const pathsJson = JSON.stringify(paths); + await database + .prepare( + `DELETE FROM environment_package_artifact_backup_staging AS stage + WHERE stage.command_id = ? + AND ( + stage.delivery_generation <> ? OR stage.attempt_count <> ? OR stage.claim_owner <> ? + ) + AND EXISTS ( + SELECT 1 FROM api_command AS command + WHERE command.id = stage.command_id + AND command.kind = 'environment_package_artifact_build' + AND command.status = 'running' AND command.claim_owner = ? + AND command.delivery_generation = ? AND command.attempt_count = ? + AND command.claim_expires_at > ${DATABASE_NOW_MS_SQL} + AND json_valid(command.payload_json) = 1 + AND json_extract(command.payload_json, '$.appId') = ? + AND json_extract(command.payload_json, '$.inputDigest') = ? + ) + `, + ) + .bind( + input.commandId, + input.deliveryGeneration, + input.attemptCount, + input.claimOwner, + input.claimOwner, + input.deliveryGeneration, + input.attemptCount, + input.key.appId, + input.key.inputDigest, + ) + .run(); + await database + .prepare( + `INSERT INTO environment_package_artifact_backup_staging ( + actual_backup_id, app_id, attempt_count, claim_owner, command_id, created_at, + delivery_generation, dir, input_digest, paths_json, updated_at + ) + SELECT NULL, ?, ?, ?, ?, ${DATABASE_NOW_MS_SQL}, ?, ?, ?, ?, ${DATABASE_NOW_MS_SQL} + WHERE EXISTS ( + SELECT 1 FROM api_command AS command + WHERE command.id = ? AND command.kind = 'environment_package_artifact_build' + AND command.status = 'running' AND command.claim_owner = ? + AND command.delivery_generation = ? AND command.attempt_count = ? + AND command.claim_expires_at > ${DATABASE_NOW_MS_SQL} + AND json_valid(command.payload_json) = 1 + AND json_extract(command.payload_json, '$.appId') = ? + AND json_extract(command.payload_json, '$.inputDigest') = ? + ) + AND NOT EXISTS ( + SELECT 1 FROM environment_package_artifact_backup_staging AS existing + WHERE existing.command_id = ? + OR (existing.app_id = ? AND existing.input_digest = ?) + ) + ON CONFLICT DO NOTHING`, + ) + .bind( + input.key.appId, + input.attemptCount, + input.claimOwner, + input.commandId, + input.deliveryGeneration, + input.dir, + input.key.inputDigest, + pathsJson, + input.commandId, + input.claimOwner, + input.deliveryGeneration, + input.attemptCount, + input.key.appId, + input.key.inputDigest, + input.commandId, + input.key.appId, + input.key.inputDigest, + ) + .run(); + const stage = await getEnvironmentPackageArtifactBackupStage(database, input.commandId); + if ( + stage === null || + stage.appId !== input.key.appId || + stage.attemptCount !== input.attemptCount || + stage.claimOwner !== input.claimOwner || + stage.deliveryGeneration !== input.deliveryGeneration || + stage.inputDigest !== input.key.inputDigest || + stage.dir !== input.dir || + JSON.stringify(stage.paths) !== pathsJson + ) { + throw new Error("Environment package artifact backup lost its immutable stage."); + } + const owned = await database + .prepare( + `SELECT 1 + FROM environment_package_artifact_backup_staging AS stage + JOIN api_command AS command ON command.id = stage.command_id + WHERE stage.command_id = ? AND command.status = 'running' + AND command.claim_owner = ? AND command.delivery_generation = ? + AND command.attempt_count = ? + AND command.claim_expires_at > ${DATABASE_NOW_MS_SQL} + AND ${commandMatchesStage("stage")}`, + ) + .bind(input.commandId, input.claimOwner, input.deliveryGeneration, input.attemptCount) + .first(); + if (owned === null) { + throw new Error("Environment package artifact build lost its API command lease."); + } + return stage; +} + +export async function claimEnvironmentPackageArtifactBackupActual( + database: D1Database, + input: { + readonly actualBackupId: SandboxBackupId; + readonly authority: { + readonly attemptCount: number; + readonly claimOwner: string; + readonly deliveryGeneration: number; + }; + readonly commandId: ApiCommandId; + readonly dir: string; + }, +): Promise<{ readonly actualBackupId: SandboxBackupId } | null> { + const authorityBindings = [ + input.authority.claimOwner, + input.authority.deliveryGeneration, + input.authority.attemptCount, + ] as const; + const authorityPredicate = `command.status = 'running' AND command.claim_owner = ? AND command.delivery_generation = ? AND command.attempt_count = ? AND command.claim_expires_at > ${DATABASE_NOW_MS_SQL}`; + const claimed = await database + .prepare( + `UPDATE environment_package_artifact_backup_staging AS stage + SET actual_backup_id = ?, updated_at = ${DATABASE_NOW_MS_SQL} + WHERE stage.command_id = ? AND stage.dir = ? AND stage.actual_backup_id IS NULL + AND EXISTS ( + SELECT 1 FROM api_command AS command + WHERE command.id = stage.command_id AND ${authorityPredicate} + ) + AND ${commandMatchesStage("stage")} + AND NOT EXISTS ( + SELECT 1 FROM sandbox_backup_delete_intent AS deletion + WHERE deletion.backup_id = ? + ) + AND NOT EXISTS ( + SELECT 1 FROM environment_package_artifact_backup AS artifact + WHERE artifact.backup_id = ? + ) + AND NOT EXISTS ( + SELECT 1 FROM sandbox_backup + WHERE id = ? + UNION ALL + SELECT 1 FROM sandbox_backup_staging + WHERE actual_backup_id = ? + ) + RETURNING actual_backup_id`, + ) + .bind( + input.actualBackupId, + input.commandId, + input.dir, + ...authorityBindings, + input.actualBackupId, + input.actualBackupId, + input.actualBackupId, + input.actualBackupId, + ) + .first<{ actual_backup_id: SandboxBackupId }>(); + if (claimed !== null) { + return { actualBackupId: claimed.actual_backup_id }; + } + const winner = await database + .prepare( + `SELECT stage.actual_backup_id + FROM environment_package_artifact_backup_staging AS stage + JOIN api_command AS command ON command.id = stage.command_id + WHERE stage.command_id = ? AND stage.dir = ? AND stage.actual_backup_id IS NOT NULL + AND ${authorityPredicate} AND ${commandMatchesStage("stage")} + AND NOT EXISTS ( + SELECT 1 FROM sandbox_backup_delete_intent AS deletion + WHERE deletion.backup_id = stage.actual_backup_id + ) + AND NOT EXISTS ( + SELECT 1 FROM environment_package_artifact_backup AS artifact + WHERE artifact.backup_id = stage.actual_backup_id + ) + AND NOT EXISTS ( + SELECT 1 FROM sandbox_backup + WHERE id = stage.actual_backup_id + UNION ALL + SELECT 1 FROM sandbox_backup_staging + WHERE actual_backup_id = stage.actual_backup_id + )`, + ) + .bind(input.commandId, input.dir, ...authorityBindings) + .first<{ actual_backup_id: SandboxBackupId }>(); + return winner === null ? null : { actualBackupId: winner.actual_backup_id }; +} + +export async function clearMissingEnvironmentPackageArtifactBackupActual( + database: D1Database, + input: EnvironmentPackageArtifactBackupStageAuthority & { + readonly actualBackupId: SandboxBackupId; + }, +): Promise { + const result = await database + .prepare( + `UPDATE environment_package_artifact_backup_staging + SET actual_backup_id = NULL, updated_at = ${DATABASE_NOW_MS_SQL} + WHERE command_id = ? AND delivery_generation = ? AND attempt_count = ? + AND claim_owner = ? AND actual_backup_id = ?`, + ) + .bind( + input.commandId, + input.deliveryGeneration, + input.attemptCount, + input.claimOwner, + input.actualBackupId, + ) + .run(); + return (result.meta.changes ?? 0) === 1; +} + +export async function completeEnvironmentPackageArtifactBackupStage( + database: D1Database, + input: EnvironmentPackageArtifactBackupStageAuthority & { + readonly actualBackupId: SandboxBackupId | null; + }, +): Promise { + const result = await database + .prepare( + `DELETE FROM environment_package_artifact_backup_staging + WHERE command_id = ? AND delivery_generation = ? AND attempt_count = ? + AND claim_owner = ? AND actual_backup_id IS ? + AND EXISTS ( + SELECT 1 FROM environment_package_artifact_backup AS artifact + WHERE artifact.app_id = environment_package_artifact_backup_staging.app_id + AND artifact.input_digest = environment_package_artifact_backup_staging.input_digest + AND artifact.backup_id = environment_package_artifact_backup_staging.actual_backup_id + AND artifact.command_id = environment_package_artifact_backup_staging.command_id + AND artifact.delivery_generation = environment_package_artifact_backup_staging.delivery_generation + AND artifact.attempt_count = environment_package_artifact_backup_staging.attempt_count + AND artifact.paths_json = environment_package_artifact_backup_staging.paths_json + )`, + ) + .bind( + input.commandId, + input.deliveryGeneration, + input.attemptCount, + input.claimOwner, + input.actualBackupId, + ) + .run(); + return (result.meta.changes ?? 0) === 1; +} + +export async function revokeTerminalEnvironmentPackageArtifactBackupStage( + database: D1Database, + commandId: ApiCommandId, +): Promise { + const row = await database + .prepare( + `DELETE FROM environment_package_artifact_backup_staging AS stage + WHERE stage.command_id = ? + AND NOT EXISTS ( + SELECT 1 FROM api_command AS command + WHERE command.id = stage.command_id AND command.status = 'running' + AND command.claim_expires_at > ${DATABASE_NOW_MS_SQL} + AND ${commandMatchesStage("stage")} + ) + RETURNING actual_backup_id`, + ) + .bind(commandId) + .first<{ actual_backup_id: SandboxBackupId | null }>(); + return row?.actual_backup_id ?? null; +} + +export async function revokeTerminalEnvironmentPackageArtifactBackupStages( + database: D1Database, +): Promise { + const result = await database + .prepare( + `DELETE FROM environment_package_artifact_backup_staging AS stage + WHERE stage.command_id IN ( + SELECT candidate.command_id + FROM environment_package_artifact_backup_staging AS candidate + WHERE NOT EXISTS ( + SELECT 1 FROM api_command AS command + WHERE command.id = candidate.command_id + AND command.status = 'running' + AND command.claim_expires_at > ${DATABASE_NOW_MS_SQL} + AND command.kind = 'environment_package_artifact_build' + AND json_valid(command.payload_json) = 1 + AND json_extract(command.payload_json, '$.appId') = candidate.app_id + AND json_extract(command.payload_json, '$.inputDigest') = candidate.input_digest + AND command.delivery_generation = candidate.delivery_generation + AND command.attempt_count = candidate.attempt_count + AND command.claim_owner = candidate.claim_owner + ) + ORDER BY candidate.updated_at, candidate.command_id + LIMIT 64 + )`, + ) + .run(); + return result.meta.changes ?? 0; +} diff --git a/apps/api/src/modules/environments/application/environment-package-artifact-backup.ts b/apps/api/src/modules/environments/application/environment-package-artifact-backup.ts new file mode 100644 index 00000000..dcefc1c3 --- /dev/null +++ b/apps/api/src/modules/environments/application/environment-package-artifact-backup.ts @@ -0,0 +1,304 @@ +import type { ApiCommandId } from "@mosoo/db"; +import type { SandboxBackupId } from "@mosoo/id"; +import { parsePlatformId } from "@mosoo/id"; + +import { logWarn } from "../../../platform/cloudflare/logger"; +import type { ApiBindings } from "../../../platform/cloudflare/worker-types"; +import { + decodeSandboxBackupIdForPlatform, + encodeSandboxBackupIdForStorage, +} from "../../runtime/infrastructure/sandbox-backup-id"; +import { + getSandboxBackupObjectKeys, + parseSandboxBackupMetadata, +} from "../../runtime/infrastructure/sandbox-backup-platform"; +import { authorizeSandboxBackupDeletion } from "../../runtime/infrastructure/sandbox-backup-store"; +import { + createEnvironmentPackageArtifactBackupName, + environmentPackageArtifactDir, + environmentPackageArtifactMetadataKey, + ENVIRONMENT_PACKAGE_ARTIFACT_BACKUP_REFRESH_WINDOW_MS, + ENVIRONMENT_PACKAGE_ARTIFACT_BACKUP_TTL_SECONDS, + parseEnvironmentPackageArtifactBackupName, + parseEnvironmentPackageArtifactMetadata, +} from "../domain/environment-package-artifact"; +import type { + EnvironmentPackageArtifactKey, + EnvironmentPackageArtifactMetadata, + EnvironmentPackageArtifactPaths, +} from "../domain/environment-package-artifact"; +import { + adoptLegacyEnvironmentPackageArtifactBackup, + commitEnvironmentPackageArtifactBackup, + findEnvironmentPackageArtifactCommandAuthority, + getEnvironmentPackageArtifactBackupManifest, + getEnvironmentPackageArtifactCommandIntent, +} from "./environment-package-artifact-backup-store"; + +export function environmentPackageArtifactMetadataBackupId( + metadata: EnvironmentPackageArtifactMetadata, +): SandboxBackupId | null { + try { + return encodeSandboxBackupIdForStorage(metadata.backupId); + } catch { + return null; + } +} + +interface EnvironmentPackageArtifactBackupVerification { + readonly authority: { + readonly attemptCount: number; + readonly commandId: ApiCommandId; + readonly deliveryGeneration: number; + } | null; + readonly expiresAt: number; +} + +async function readEnvironmentPackageArtifactBackupVerification( + bindings: Pick, + input: { readonly backupId: SandboxBackupId; readonly dir: string }, +): Promise { + const [dataKey, metadataKey] = getSandboxBackupObjectKeys(input.backupId); + const [data, storedMetadata] = await Promise.all([ + bindings.SANDBOX_STATE_BUCKET.head(dataKey), + bindings.SANDBOX_STATE_BUCKET.get(metadataKey), + ]); + if (data === null || storedMetadata === null) { + return null; + } + let metadata = null; + let createdAt: unknown; + let ttl: unknown; + try { + const value = JSON.parse(await storedMetadata.text()) as unknown; + metadata = parseSandboxBackupMetadata(value); + createdAt = + typeof value === "object" && value !== null ? Reflect.get(value, "createdAt") : null; + ttl = typeof value === "object" && value !== null ? Reflect.get(value, "ttl") : null; + } catch { + return null; + } + if ( + metadata?.id !== decodeSandboxBackupIdForPlatform(input.backupId) || + metadata.dir !== input.dir + ) { + return null; + } + const createdAtMs = typeof createdAt === "string" ? Date.parse(createdAt) : Number.NaN; + const expiresAt = createdAtMs + ENVIRONMENT_PACKAGE_ARTIFACT_BACKUP_TTL_SECONDS * 1_000; + if ( + !Number.isSafeInteger(createdAtMs) || + new Date(createdAtMs).toISOString() !== createdAt || + ttl !== ENVIRONMENT_PACKAGE_ARTIFACT_BACKUP_TTL_SECONDS || + !Number.isSafeInteger(expiresAt) + ) { + return null; + } + if (metadata.name === null) { + return { authority: null, expiresAt }; + } + const authority = parseEnvironmentPackageArtifactBackupName(metadata.name); + if (authority === null) { + return null; + } + try { + return { + authority: { + attemptCount: authority.attemptCount, + commandId: parsePlatformId( + authority.commandId, + "environment artifact command ID", + ), + deliveryGeneration: authority.deliveryGeneration, + }, + expiresAt, + }; + } catch { + return null; + } +} + +export async function isEnvironmentPackageArtifactBackupReady( + bindings: Pick, + input: { + readonly attemptCount: number; + readonly backupId: SandboxBackupId; + readonly commandId: ApiCommandId; + readonly deliveryGeneration: number; + readonly dir: string; + }, +): Promise { + const verified = await readEnvironmentPackageArtifactBackupVerification(bindings, input); + return ( + verified?.authority?.commandId === input.commandId && + verified.authority.deliveryGeneration === input.deliveryGeneration && + verified.authority.attemptCount === input.attemptCount + ); +} + +async function readLegacyEnvironmentPackageArtifactMetadata( + bindings: Pick, + key: EnvironmentPackageArtifactKey, +): Promise { + const object = await bindings.SANDBOX_STATE_BUCKET.get( + environmentPackageArtifactMetadataKey(key), + ); + if (object === null) { + return null; + } + try { + return parseEnvironmentPackageArtifactMetadata( + JSON.parse(await object.text()), + environmentPackageArtifactDir(key), + ); + } catch { + return null; + } +} + +export async function resolveEnvironmentPackageArtifactBackup( + bindings: Pick, + key: EnvironmentPackageArtifactKey, +): Promise { + const manifest = await getEnvironmentPackageArtifactBackupManifest(bindings.DB, key); + const clock = await bindings.DB.prepare( + "SELECT CAST(unixepoch('subsec') * 1000 AS INTEGER) AS now_ms", + ).first<{ now_ms: number }>(); + if (clock === null) { + return null; + } + if (manifest !== null) { + if ( + manifest.expiresAt <= + clock.now_ms + ENVIRONMENT_PACKAGE_ARTIFACT_BACKUP_REFRESH_WINDOW_MS + ) { + return null; + } + const verified = await readEnvironmentPackageArtifactBackupVerification(bindings, { + backupId: manifest.backupId, + dir: environmentPackageArtifactDir(key), + }); + const command = await getEnvironmentPackageArtifactCommandIntent( + bindings.DB, + manifest.commandId, + ); + if ( + verified === null || + command === null || + command.appId !== key.appId || + command.inputDigest !== key.inputDigest || + command.commandId !== manifest.commandId || + verified.expiresAt !== manifest.expiresAt || + (verified.authority !== null && + (verified.authority.commandId !== manifest.commandId || + verified.authority.deliveryGeneration !== manifest.deliveryGeneration || + verified.authority.attemptCount !== manifest.attemptCount)) + ) { + return null; + } + return { + backupId: decodeSandboxBackupIdForPlatform(manifest.backupId), + paths: manifest.paths, + }; + } + + const projection = await readLegacyEnvironmentPackageArtifactMetadata(bindings, key); + if (projection === null) { + return null; + } + const backupId = environmentPackageArtifactMetadataBackupId(projection); + if (backupId === null) { + return null; + } + const verified = await readEnvironmentPackageArtifactBackupVerification(bindings, { + backupId, + dir: environmentPackageArtifactDir(key), + }); + if ( + verified === null || + verified.expiresAt <= clock.now_ms + ENVIRONMENT_PACKAGE_ARTIFACT_BACKUP_REFRESH_WINDOW_MS || + verified.expiresAt > clock.now_ms + ENVIRONMENT_PACKAGE_ARTIFACT_BACKUP_TTL_SECONDS * 1_000 + ) { + return null; + } + const command = + verified.authority === null + ? await findEnvironmentPackageArtifactCommandAuthority(bindings.DB, key) + : await getEnvironmentPackageArtifactCommandIntent(bindings.DB, verified.authority.commandId); + if ( + command === null || + command.appId !== key.appId || + command.inputDigest !== key.inputDigest || + (verified.authority !== null && + (verified.authority.commandId !== command.commandId || + verified.authority.deliveryGeneration !== command.deliveryGeneration || + verified.authority.attemptCount !== command.attemptCount)) || + !(await adoptLegacyEnvironmentPackageArtifactBackup(bindings.DB, { + actualBackupId: backupId, + expiresAt: verified.expiresAt, + ...command, + key, + paths: projection.paths, + })) + ) { + logWarn("runtime.environment_artifact.legacy_projection_adoption_failed", { + backupId, + inputDigest: key.inputDigest, + }); + return null; + } + return projection; +} + +export async function publishEnvironmentPackageArtifactBackup( + bindings: Pick, + input: { + readonly attemptCount: number; + readonly backupId: SandboxBackupId; + readonly claimOwner: string; + readonly commandId: ApiCommandId; + readonly deliveryGeneration: number; + readonly key: EnvironmentPackageArtifactKey; + readonly paths: EnvironmentPackageArtifactPaths; + }, +): Promise { + const verified = await readEnvironmentPackageArtifactBackupVerification(bindings, { + backupId: input.backupId, + dir: environmentPackageArtifactDir(input.key), + }); + if ( + verified?.authority?.commandId !== input.commandId || + verified.authority.deliveryGeneration !== input.deliveryGeneration || + verified.authority.attemptCount !== input.attemptCount + ) { + throw new Error("Environment package artifact backup objects are incomplete."); + } + const committed = await commitEnvironmentPackageArtifactBackup(bindings.DB, { + actualBackupId: input.backupId, + attemptCount: input.attemptCount, + claimOwner: input.claimOwner, + commandId: input.commandId, + deliveryGeneration: input.deliveryGeneration, + expiresAt: verified.expiresAt, + key: input.key, + paths: input.paths, + }); + const manifest = await getEnvironmentPackageArtifactBackupManifest(bindings.DB, input.key); + if (!committed || manifest?.backupId !== input.backupId) { + if (manifest !== null && manifest.backupId !== input.backupId) { + await authorizeSandboxBackupDeletion(bindings.DB, { + authority: { + attemptCount: input.attemptCount, + commandId: input.commandId, + deliveryGeneration: input.deliveryGeneration, + kind: "environment_invalid", + }, + backupId: input.backupId, + }); + return; + } + throw new Error("Environment package artifact backup lost D1 commit authority."); + } +} + +export { createEnvironmentPackageArtifactBackupName }; diff --git a/apps/api/src/modules/environments/application/environment-package-artifact-build.service.ts b/apps/api/src/modules/environments/application/environment-package-artifact-build.service.ts index ad9a30f8..a9afdecd 100644 --- a/apps/api/src/modules/environments/application/environment-package-artifact-build.service.ts +++ b/apps/api/src/modules/environments/application/environment-package-artifact-build.service.ts @@ -1,29 +1,44 @@ +import type { ApiCommandId } from "@mosoo/db"; +import type { SandboxBackupId } from "@mosoo/id"; + import { disposeRpcResource, withDisposedRpcResult, } from "../../../platform/cloudflare/rpc-disposal"; -import { requireCloudflareSandboxBinding } from "../../../platform/cloudflare/sandbox-binding"; import type { ApiBindings } from "../../../platform/cloudflare/worker-types"; +import { quoteShellArg } from "../../../shared/shell"; import type { EnvironmentPackageArtifactBuildCommandPayload } from "../../api-command/application/api-command-payload"; import { isRuntimeSandboxLocalBucketEnabled } from "../../runtime/infrastructure/runtime-sandbox-bucket-mount"; -import { deleteSandboxBackupObjects } from "../../runtime/infrastructure/sandbox-backup-platform"; +import { getEphemeralUnversionedSandboxHandle } from "../../runtime/infrastructure/runtime-subject-lifecycle/runtime-subject-platform"; +import { encodeSandboxBackupIdForStorage } from "../../runtime/infrastructure/sandbox-backup-id"; +import { authorizeSandboxBackupDeletion } from "../../runtime/infrastructure/sandbox-backup-store"; import type { SandboxHandle } from "../../runtime/infrastructure/sandbox-handles"; -import { toSandboxHandle } from "../../runtime/infrastructure/sandbox-handles"; import { createEnvironmentPackageArtifactKey, + environmentPackageArtifactBuildSandboxId, environmentPackageArtifactDir, - environmentPackageArtifactMetadataKey, - environmentPackageArtifactSandboxId, ENVIRONMENT_PACKAGE_ARTIFACT_BACKUP_TTL_SECONDS, ENVIRONMENT_PACKAGE_ARTIFACT_MAX_BUILD_MS, } from "../domain/environment-package-artifact"; import type { EnvironmentPackageArtifactPaths } from "../domain/environment-package-artifact"; import { normalizePackages } from "./environment-config"; -import { readEnvironmentPackageArtifactMetadata } from "./environment-package-artifact.service"; +import { + createEnvironmentPackageArtifactBackupName, + environmentPackageArtifactMetadataBackupId, + isEnvironmentPackageArtifactBackupReady, + publishEnvironmentPackageArtifactBackup, + resolveEnvironmentPackageArtifactBackup, +} from "./environment-package-artifact-backup"; +import { + claimEnvironmentPackageArtifactBackupActual, + clearMissingEnvironmentPackageArtifactBackupActual, + completeEnvironmentPackageArtifactBackupStage, + getEnvironmentPackageArtifactBackupStage, + stageEnvironmentPackageArtifactBackup, +} from "./environment-package-artifact-backup-store"; -function quoteShellArg(value: string): string { - return `'${value.replaceAll("'", `'"'"'`)}'`; -} +const ENVIRONMENT_ARTIFACT_BUILD_SANDBOX_SLEEP_AFTER_SECONDS = + ENVIRONMENT_PACKAGE_ARTIFACT_MAX_BUILD_MS / 1_000 + 5 * 60; export function createEnvironmentPackageArtifactBuildScript(input: { npmRoot: string; @@ -62,9 +77,112 @@ async function closeBuildSandbox(sandbox: SandboxHandle): Promise { } } +export interface EnvironmentPackageArtifactBuildAuthority { + readonly attemptCount: number; + readonly claimOwner: string; + readonly commandId: ApiCommandId; + readonly deliveryGeneration: number; + requireOwnership(): Promise; +} + +async function withBuildOwnership( + authority: EnvironmentPackageArtifactBuildAuthority, + effect: () => Promise, +): Promise { + await authority.requireOwnership(); + try { + return await effect(); + } finally { + await authority.requireOwnership(); + } +} + +async function settlePublishedArtifact( + bindings: ApiBindings, + input: { + readonly authority: EnvironmentPackageArtifactBuildAuthority; + readonly commandId: ApiCommandId; + readonly key: Awaited>; + }, +): Promise { + const metadata = await resolveEnvironmentPackageArtifactBackup(bindings, input.key); + if (metadata === null) { + return null; + } + const backupId = environmentPackageArtifactMetadataBackupId(metadata); + if (backupId === null) { + throw new Error("Environment package artifact metadata has an invalid backup ID."); + } + const stage = await getEnvironmentPackageArtifactBackupStage(bindings.DB, input.commandId); + if (stage === null) { + return backupId; + } + if (JSON.stringify(stage.paths) !== JSON.stringify(metadata.paths)) { + throw new Error("Environment package artifact manifest changed its immutable paths."); + } + await withBuildOwnership(input.authority, () => + completeEnvironmentPackageArtifactBackupStage(bindings.DB, { + actualBackupId: stage.actualBackupId, + attemptCount: stage.attemptCount, + claimOwner: stage.claimOwner, + commandId: stage.commandId, + deliveryGeneration: stage.deliveryGeneration, + }), + ); + return backupId; +} + +async function publishClaimedArtifact( + bindings: ApiBindings, + input: { + readonly attemptCount: number; + readonly authority: EnvironmentPackageArtifactBuildAuthority; + readonly backupId: SandboxBackupId; + readonly commandId: ApiCommandId; + readonly deliveryGeneration: number; + readonly dir: string; + readonly key: Awaited>; + readonly paths: EnvironmentPackageArtifactPaths; + }, +): Promise { + if ( + (await settlePublishedArtifact(bindings, { + authority: input.authority, + commandId: input.commandId, + key: input.key, + })) !== null + ) { + return; + } + if (!(await isEnvironmentPackageArtifactBackupReady(bindings, input))) { + throw new Error("Environment package artifact backup objects are incomplete."); + } + await withBuildOwnership(input.authority, () => + publishEnvironmentPackageArtifactBackup(bindings, { + attemptCount: input.attemptCount, + backupId: input.backupId, + claimOwner: input.authority.claimOwner, + commandId: input.commandId, + deliveryGeneration: input.deliveryGeneration, + key: input.key, + paths: input.paths, + }), + ); + if ( + (await settlePublishedArtifact(bindings, { + authority: input.authority, + commandId: input.commandId, + key: input.key, + })) === null + ) { + throw new Error("Environment package artifact manifest was not committed."); + } +} + export async function buildEnvironmentPackageArtifact( bindings: ApiBindings, payload: EnvironmentPackageArtifactBuildCommandPayload, + authority: EnvironmentPackageArtifactBuildAuthority, ): Promise { const packages = normalizePackages(payload.packages); const key = await createEnvironmentPackageArtifactKey({ @@ -75,7 +193,13 @@ export async function buildEnvironmentPackageArtifact( if (key.inputDigest !== payload.inputDigest) { throw new Error("Environment package artifact digest does not match its payload."); } - if ((await readEnvironmentPackageArtifactMetadata(bindings, key)) !== null) { + if ( + (await settlePublishedArtifact(bindings, { + authority, + commandId: authority.commandId, + key, + })) !== null + ) { return; } @@ -85,32 +209,36 @@ export async function buildEnvironmentPackageArtifact( const tempRoot = `/tmp/mosoo-environment-artifact-${key.inputDigest}`; const npmRoot = `${dir}/npm`; const pipRoot = `${dir}/python`; - let backupId: string | null = null; - const { getSandbox } = await import("@cloudflare/sandbox"); - const sandbox = toSandboxHandle( - getSandbox( - requireCloudflareSandboxBinding(bindings), - environmentPackageArtifactSandboxId(key), - { keepAlive: true, normalizeId: true }, + const sandbox = await getEphemeralUnversionedSandboxHandle( + bindings, + environmentPackageArtifactBuildSandboxId( + authority.commandId, + authority.deliveryGeneration, + authority.attemptCount, ), + ENVIRONMENT_ARTIFACT_BUILD_SANDBOX_SLEEP_AFTER_SECONDS, ); try { - const reset = await sandbox.exec( - `rm -rf ${quoteShellArg(dir)} ${quoteShellArg(tempRoot)} && mkdir -p ${quoteShellArg(dir)} ${quoteShellArg(tempRoot)}`, + const reset = await withBuildOwnership(authority, () => + sandbox.exec( + `rm -rf ${quoteShellArg(dir)} ${quoteShellArg(tempRoot)} && mkdir -p ${quoteShellArg(dir)} ${quoteShellArg(tempRoot)}`, + ), ); if (!reset.success) { throw new Error("Environment package build directory could not be prepared."); } - const result = await sandbox.exec( - createEnvironmentPackageArtifactBuildScript({ - npmRoot, - npmSpecs, - pipRoot, - pipSpecs, - tempRoot, - }), - { timeout: ENVIRONMENT_PACKAGE_ARTIFACT_MAX_BUILD_MS }, + const result = await withBuildOwnership(authority, () => + sandbox.exec( + createEnvironmentPackageArtifactBuildScript({ + npmRoot, + npmSpecs, + pipRoot, + pipSpecs, + tempRoot, + }), + { timeout: ENVIRONMENT_PACKAGE_ARTIFACT_MAX_BUILD_MS }, + ), ); if (!result.success) { const tail = `${result.stdout}\n${result.stderr}`.trim().slice(-4096); @@ -129,29 +257,154 @@ export async function buildEnvironmentPackageArtifact( node: npmSpecs.length === 0 ? [] : [`${npmRoot}/node_modules`], python: pipSpecs.length === 0 ? [] : [pipSite], }; - const backup = await withDisposedRpcResult( - sandbox.createBackup({ + let stage = await withBuildOwnership(authority, () => + stageEnvironmentPackageArtifactBackup(bindings.DB, { + attemptCount: authority.attemptCount, + claimOwner: authority.claimOwner, + commandId: authority.commandId, + deliveryGeneration: authority.deliveryGeneration, dir, - localBucket: isRuntimeSandboxLocalBucketEnabled(bindings), - ttl: ENVIRONMENT_PACKAGE_ARTIFACT_BACKUP_TTL_SECONDS, + key, + paths, }), - (backupResult) => ({ dir: backupResult.dir, id: backupResult.id }), ); - backupId = backup.id; - if (!backupId || backup.dir !== dir) { + if ( + stage.actualBackupId !== null && + !(await isEnvironmentPackageArtifactBackupReady(bindings, { + attemptCount: authority.attemptCount, + backupId: stage.actualBackupId, + commandId: authority.commandId, + deliveryGeneration: authority.deliveryGeneration, + dir, + })) + ) { + const cleared = await withBuildOwnership(authority, () => + clearMissingEnvironmentPackageArtifactBackupActual(bindings.DB, { + actualBackupId: stage.actualBackupId as SandboxBackupId, + attemptCount: stage.attemptCount, + claimOwner: stage.claimOwner, + commandId: stage.commandId, + deliveryGeneration: stage.deliveryGeneration, + }), + ); + if (!cleared) { + if ( + (await settlePublishedArtifact(bindings, { + authority, + commandId: authority.commandId, + key, + })) !== null + ) { + return; + } + throw new Error("Environment package artifact build lost its immutable stage."); + } + stage = { ...stage, actualBackupId: null }; + } + if (stage.actualBackupId !== null) { + const claimed = await withBuildOwnership(authority, () => + claimEnvironmentPackageArtifactBackupActual(bindings.DB, { + actualBackupId: stage.actualBackupId as SandboxBackupId, + authority, + commandId: authority.commandId, + dir, + }), + ); + if (claimed?.actualBackupId !== stage.actualBackupId) { + if ( + (await settlePublishedArtifact(bindings, { + authority, + commandId: authority.commandId, + key, + })) !== null + ) { + return; + } + throw new Error("Environment package artifact build lost its API command lease."); + } + await publishClaimedArtifact(bindings, { + attemptCount: authority.attemptCount, + authority, + backupId: stage.actualBackupId, + commandId: authority.commandId, + deliveryGeneration: authority.deliveryGeneration, + dir, + key, + paths, + }); + return; + } + + const backup = await withBuildOwnership(authority, () => + withDisposedRpcResult( + sandbox.createBackup({ + dir, + localBucket: isRuntimeSandboxLocalBucketEnabled(bindings), + name: createEnvironmentPackageArtifactBackupName(authority), + ttl: ENVIRONMENT_PACKAGE_ARTIFACT_BACKUP_TTL_SECONDS, + }), + (backupResult) => ({ dir: backupResult.dir, id: backupResult.id }), + ), + ); + if (!backup.id || backup.dir !== dir) { throw new Error("Environment package artifact backup is invalid."); } - await bindings.SANDBOX_STATE_BUCKET.put( - environmentPackageArtifactMetadataKey(key), - JSON.stringify({ backupId, paths }), - { httpMetadata: { contentType: "application/json" } }, + const candidateId = encodeSandboxBackupIdForStorage(backup.id); + const claimed = await withBuildOwnership(authority, () => + claimEnvironmentPackageArtifactBackupActual(bindings.DB, { + actualBackupId: candidateId, + authority, + commandId: authority.commandId, + dir, + }), ); - backupId = null; - } catch (error) { - if (backupId !== null) { - await deleteSandboxBackupObjects(bindings, [backupId]).catch(() => undefined); + if (claimed?.actualBackupId !== candidateId) { + const published = await settlePublishedArtifact(bindings, { + authority, + commandId: authority.commandId, + key, + }); + if (claimed !== null && (published === null || published !== candidateId)) { + await withBuildOwnership(authority, () => + authorizeSandboxBackupDeletion(bindings.DB, { + authority: { + attemptCount: authority.attemptCount, + commandId: authority.commandId, + deliveryGeneration: authority.deliveryGeneration, + kind: "environment_candidate", + }, + backupId: candidateId, + }), + ); + } + if (published !== null) { + return; + } + if (claimed === null) { + throw new Error("Environment package artifact build lost its API command lease."); + } + await publishClaimedArtifact(bindings, { + attemptCount: authority.attemptCount, + authority, + backupId: claimed.actualBackupId, + commandId: authority.commandId, + deliveryGeneration: authority.deliveryGeneration, + dir, + key, + paths, + }); + return; } - throw error; + await publishClaimedArtifact(bindings, { + attemptCount: authority.attemptCount, + authority, + backupId: candidateId, + commandId: authority.commandId, + deliveryGeneration: authority.deliveryGeneration, + dir, + key, + paths, + }); } finally { await closeBuildSandbox(sandbox); } diff --git a/apps/api/src/modules/environments/application/environment-package-artifact.service.ts b/apps/api/src/modules/environments/application/environment-package-artifact.service.ts index a750ef23..31b9fbc1 100644 --- a/apps/api/src/modules/environments/application/environment-package-artifact.service.ts +++ b/apps/api/src/modules/environments/application/environment-package-artifact.service.ts @@ -10,7 +10,6 @@ import { import { createEnvironmentPackageArtifactKey, environmentPackageArtifactDir, - environmentPackageArtifactMetadataKey, ENVIRONMENT_PACKAGE_ARTIFACT_ABI, } from "../domain/environment-package-artifact"; import type { @@ -18,34 +17,14 @@ import type { EnvironmentPackageArtifactMetadata, } from "../domain/environment-package-artifact"; import { normalizePackages, parsePackagesJson } from "./environment-config"; +import { resolveEnvironmentPackageArtifactBackup } from "./environment-package-artifact-backup"; +import { getEnvironmentPackageArtifactBackupManifest } from "./environment-package-artifact-backup-store"; type ArtifactBindings = Pick< ApiBindings, "API_COMMAND_QUEUE" | "DB" | "ENVIRONMENT_ARTIFACT_BUILD_QUEUE" | "SANDBOX_STATE_BUCKET" >; -export async function readEnvironmentPackageArtifactMetadata( - bindings: Pick, - key: EnvironmentPackageArtifactKey, -): Promise { - const object = await bindings.SANDBOX_STATE_BUCKET.get( - environmentPackageArtifactMetadataKey(key), - ); - if (object === null) { - return null; - } - const metadata = JSON.parse(await object.text()) as Partial; - const paths = metadata.paths; - if ( - typeof metadata.backupId !== "string" || - paths === undefined || - ![paths.executable, paths.node, paths.python].every(Array.isArray) - ) { - throw new Error("Environment package artifact metadata is invalid."); - } - return { backupId: metadata.backupId, paths }; -} - export async function resolveEnvironmentPackageArtifact( bindings: ArtifactBindings, appId: AppId, @@ -64,13 +43,19 @@ export async function resolveEnvironmentPackageArtifact( artifactAbi: ENVIRONMENT_PACKAGE_ARTIFACT_ABI, packages: normalized, }); - const metadata = await readEnvironmentPackageArtifactMetadata(bindings, key); + const metadata = await resolveEnvironmentPackageArtifactBackup(bindings, key); if (metadata === null) { + const refreshCurrentManifest = + (await getEnvironmentPackageArtifactBackupManifest(bindings.DB, key)) !== null; const dedupeKey = `environment_package_artifact_build:${key.appId}:${key.inputDigest}`; + const existingCommand = await findApiCommandByDedupeKey(bindings.DB, dedupeKey); await enqueueApiCommand(bindings, { dedupeKey, kind: "environment_package_artifact_build", - retryTerminal: options.retryFailed === true, + retryTerminal: + refreshCurrentManifest || + existingCommand?.status === "succeeded" || + options.retryFailed === true, payload: { ...key, artifactAbi: ENVIRONMENT_PACKAGE_ARTIFACT_ABI, @@ -80,7 +65,7 @@ export async function resolveEnvironmentPackageArtifact( if (options.retryFailed !== true) { const command = await findApiCommandByDedupeKey(bindings.DB, dedupeKey); if (command !== null && command.status !== "queued" && command.status !== "running") { - const completedMetadata = await readEnvironmentPackageArtifactMetadata(bindings, key); + const completedMetadata = await resolveEnvironmentPackageArtifactBackup(bindings, key); if (completedMetadata !== null) { return { key, metadata: completedMetadata }; } diff --git a/apps/api/src/modules/environments/domain/environment-package-artifact.ts b/apps/api/src/modules/environments/domain/environment-package-artifact.ts index 6e6bebbf..4ff67976 100644 --- a/apps/api/src/modules/environments/domain/environment-package-artifact.ts +++ b/apps/api/src/modules/environments/domain/environment-package-artifact.ts @@ -1,10 +1,11 @@ import type { EnvironmentPackageSpec } from "@mosoo/contracts/environment"; import type { AppId } from "@mosoo/id"; -export const ENVIRONMENT_PACKAGE_ARTIFACT_ROOT = "/workspace/.mosoo/environment-artifacts"; export const ENVIRONMENT_PACKAGE_ARTIFACT_ABI = "environment-artifact-v1"; export const ENVIRONMENT_PACKAGE_ARTIFACT_MAX_BUILD_MS = 10 * 60 * 1000; export const ENVIRONMENT_PACKAGE_ARTIFACT_BACKUP_TTL_SECONDS = 10 * 365 * 24 * 60 * 60; +export const ENVIRONMENT_PACKAGE_ARTIFACT_BACKUP_REFRESH_WINDOW_MS = 24 * 60 * 60 * 1_000; +const ENVIRONMENT_PACKAGE_ARTIFACT_BACKUP_NAME_PREFIX = "mosoo:environment-artifact:v1:"; export interface EnvironmentPackageArtifactPaths { executable: string[]; @@ -22,8 +23,114 @@ export interface EnvironmentPackageArtifactMetadata { paths: EnvironmentPackageArtifactPaths; } -function bytesToHex(bytes: Uint8Array): string { - return [...bytes].map((byte) => byte.toString(16).padStart(2, "0")).join(""); +export interface EnvironmentPackageArtifactBackupAuthorityRef { + readonly attemptCount: number; + readonly commandId: string; + readonly deliveryGeneration: number; +} + +function isPositiveSafeInteger(value: number): boolean { + return Number.isSafeInteger(value) && value > 0; +} + +function parseArtifactPathArray( + value: unknown, + artifactDir: string, + seen: Set, +): string[] | null { + if (!Array.isArray(value)) { + return null; + } + for (const path of value) { + if ( + typeof path !== "string" || + path.includes("\0") || + path.includes(":") || + !path.startsWith(`${artifactDir}/`) || + path + .slice(artifactDir.length + 1) + .split("/") + .some((segment) => segment.length === 0 || segment === "." || segment === "..") || + seen.has(path) + ) { + return null; + } + seen.add(path); + } + return value; +} + +export function parseEnvironmentPackageArtifactPaths( + value: unknown, + artifactDir: string, +): EnvironmentPackageArtifactPaths | null { + if ( + typeof value !== "object" || + value === null || + Object.keys(value).toSorted().join(",") !== "executable,node,python" + ) { + return null; + } + const seen = new Set(); + const executable = parseArtifactPathArray(Reflect.get(value, "executable"), artifactDir, seen); + const node = parseArtifactPathArray(Reflect.get(value, "node"), artifactDir, seen); + const python = parseArtifactPathArray(Reflect.get(value, "python"), artifactDir, seen); + return executable === null || node === null || python === null + ? null + : { executable, node, python }; +} + +export function parseEnvironmentPackageArtifactMetadata( + value: unknown, + artifactDir: string, +): EnvironmentPackageArtifactMetadata | null { + if ( + typeof value !== "object" || + value === null || + Object.keys(value).toSorted().join(",") !== "backupId,paths" + ) { + return null; + } + const backupId = Reflect.get(value, "backupId"); + const paths = parseEnvironmentPackageArtifactPaths(Reflect.get(value, "paths"), artifactDir); + return typeof backupId === "string" && backupId.length > 0 && paths !== null + ? { backupId, paths } + : null; +} + +export function createEnvironmentPackageArtifactBackupName( + authority: EnvironmentPackageArtifactBackupAuthorityRef, +): string { + if ( + authority.commandId.length === 0 || + !isPositiveSafeInteger(authority.deliveryGeneration) || + !isPositiveSafeInteger(authority.attemptCount) + ) { + throw new Error("Environment package artifact command ID is required."); + } + return `${ENVIRONMENT_PACKAGE_ARTIFACT_BACKUP_NAME_PREFIX}${authority.commandId}:${authority.deliveryGeneration}:${authority.attemptCount}`; +} + +export function parseEnvironmentPackageArtifactBackupName( + value: string | null, +): EnvironmentPackageArtifactBackupAuthorityRef | null { + if (value === null || !value.startsWith(ENVIRONMENT_PACKAGE_ARTIFACT_BACKUP_NAME_PREFIX)) { + return null; + } + const [commandId, deliveryGenerationValue, attemptCountValue, extra] = value + .slice(ENVIRONMENT_PACKAGE_ARTIFACT_BACKUP_NAME_PREFIX.length) + .split(":"); + const deliveryGeneration = Number(deliveryGenerationValue); + const attemptCount = Number(attemptCountValue); + return commandId !== undefined && + commandId.length > 0 && + extra === undefined && + /^\d+$/u.test(deliveryGenerationValue ?? "") && + /^\d+$/u.test(attemptCountValue ?? "") && + isPositiveSafeInteger(deliveryGeneration) && + isPositiveSafeInteger(attemptCount) + ? { attemptCount, commandId, deliveryGeneration } + : null; } export async function createEnvironmentPackageArtifactKey(input: { @@ -39,17 +146,28 @@ export async function createEnvironmentPackageArtifactKey(input: { "SHA-256", new TextEncoder().encode(JSON.stringify({ artifactAbi, packages: input.packages })), ); - return { appId: input.appId, inputDigest: bytesToHex(new Uint8Array(digest)) }; + return { appId: input.appId, inputDigest: new Uint8Array(digest).toHex() }; } export function environmentPackageArtifactDir(key: EnvironmentPackageArtifactKey): string { - return `${ENVIRONMENT_PACKAGE_ARTIFACT_ROOT}/${key.inputDigest}`; + return `/workspace/.mosoo/environment-artifacts/${key.inputDigest}`; } export function environmentPackageArtifactMetadataKey(key: EnvironmentPackageArtifactKey): string { return `environment-artifacts/${key.appId}/${key.inputDigest}.json`; } -export function environmentPackageArtifactSandboxId(key: EnvironmentPackageArtifactKey): string { - return `envpkg-${key.appId}-${key.inputDigest}`.toLowerCase().slice(0, 63); +export function environmentPackageArtifactBuildSandboxId( + commandId: string, + deliveryGeneration: number, + attemptCount: number, +): string { + if ( + commandId.length === 0 || + !isPositiveSafeInteger(deliveryGeneration) || + !isPositiveSafeInteger(attemptCount) + ) { + throw new Error("Environment package artifact attempt must be a positive safe integer."); + } + return `envpkg-${commandId}-${deliveryGeneration.toString(36)}-${attemptCount.toString(36)}`.toLowerCase(); } diff --git a/apps/api/src/modules/files/application/file-store.ts b/apps/api/src/modules/files/application/file-store.ts index 4747294a..cec71eca 100644 --- a/apps/api/src/modules/files/application/file-store.ts +++ b/apps/api/src/modules/files/application/file-store.ts @@ -20,13 +20,13 @@ import type { SessionResource, } from "@mosoo/contracts/session"; import { fileRecordsTable, sessionsTable } from "@mosoo/db"; -import { createPlatformId, parsePlatformId } from "@mosoo/id"; +import { parsePlatformId } from "@mosoo/id"; import type { AccountId, AppId, FileId, SessionId } from "@mosoo/id"; -import { and, asc, desc, eq, inArray, or } from "drizzle-orm"; +import { and, asc, desc, eq, inArray, or, sql } from "drizzle-orm"; import type { SQL } from "drizzle-orm"; import type { ApiBindings } from "../../../platform/cloudflare/worker-types"; -import { getAppDatabase } from "../../../platform/db/drizzle"; +import { getAppDatabase, parameterizedSql } from "../../../platform/db/drizzle"; import { toArrayBuffer } from "../../../shared/bytes"; import { currentTimestampMs } from "../../../time"; import { ensureAppOwnership } from "../../apps/application/app.service"; @@ -45,23 +45,19 @@ import { createFileNotFoundError, createUnexpectedFileError, FileControlError, + isRetryableFileControlError, } from "../infrastructure/file-errors"; -import { - createFinalObjectKey, - createSessionArtifactPath, - normalizeContentType, - normalizeFileName, -} from "../infrastructure/file-paths"; +import { normalizeFileName } from "../infrastructure/file-paths"; import { ensureFileAccess, fileRecordRowColumns, listFileRecords, listFileRecordsById, - parseRuntimeOutputSourcePath, toFileEntry, toFileRecord, toSessionFile, } from "../infrastructure/file-record-store"; +import type { FileRecordRow } from "../infrastructure/file-record-store"; import { updateFile } from "../infrastructure/file-update"; import { completeFileUpload as completeFileUploadRecord } from "../infrastructure/file-upload-complete"; import { createFileUpload, getFileUpload } from "../infrastructure/file-upload-create"; @@ -70,7 +66,7 @@ import { uploadFileContent, uploadFilePart, } from "../infrastructure/file-upload-transfer"; -import { getObjectBody, putObject } from "../infrastructure/r2-s3-client"; +import { deleteObject, getObjectBody, putObject } from "../infrastructure/r2-s3-client"; import { normalizeR2Etag } from "../infrastructure/r2-s3-client"; import { ensureAppSessionFileAccess, @@ -88,16 +84,6 @@ export interface CompleteFileUploadCommand { viewer: AuthenticatedViewer; } -export interface RuntimeOutputFileInput { - bindings: ApiBindings; - body: Uint8Array; - contentSha256?: string; - contentType?: string | null; - createdBy: AccountId; - path: string; - sessionId: SessionId; -} - export interface AgentPackageFileAdmissionInput { appId: AppId; fileId: FileId; @@ -193,7 +179,6 @@ export interface FileStore { database: D1Database, sessionId: SessionId, ): Promise; - listReadySessionArtifactKeys(database: D1Database, sessionId: SessionId): Promise; listReadySessionFiles(database: D1Database, sessionId: SessionId): Promise; listSessionResourcePathEntries( database: D1Database, @@ -215,7 +200,6 @@ export interface FileStore { body: ContentBody, ): Promise; readSessionArtifactBytes(bindings: ApiBindings, objectKey: string): Promise; - recordRuntimeOutput(input: RuntimeOutputFileInput): Promise; streamContent( bindings: ApiBindings, viewer: AuthenticatedViewer, @@ -263,13 +247,50 @@ export function getRuntimeOutputName(path: string): string { export async function createRuntimeOutputContentSha256(body: Uint8Array): Promise { const digest = await crypto.subtle.digest("SHA-256", toArrayBuffer(body)); - return [...new Uint8Array(digest)].map((byte) => byte.toString(16).padStart(2, "0")).join(""); + return new Uint8Array(digest).toHex(); } export function createRuntimeOutputParentPath(path: string, contentSha256: string): string { return ["runtime-output", ...readRuntimeOutputPathSegments(path), contentSha256].join("/"); } +export async function putRuntimeArtifactObject(input: { + bindings: ApiBindings; + body: Uint8Array; + contentSha256: string; + contentType: string; + objectKey: string; + sourcePath: string; + attemptId: string; +}): Promise<{ contentLength: number; contentType: string | null; etag: string } | null> { + try { + return await putObject({ + bindings: input.bindings, + body: input.body, + contentType: input.contentType, + customMetadata: { + attemptId: input.attemptId, + contentSha256: input.contentSha256, + sourcePath: input.sourcePath, + }, + objectKey: input.objectKey, + options: { ifNoneMatch: "*" }, + }); + } catch (error) { + if (isRetryableFileControlError(error)) { + return null; + } + throw error; + } +} + +export async function deleteRuntimeArtifactObject( + bindings: ApiBindings, + objectKey: string, +): Promise { + await deleteObject(bindings, objectKey); +} + function toSessionResource(file: FileRecord): SessionResource { return { createdAt: file.createdAt, @@ -609,93 +630,116 @@ async function listVisibleFileRecords( .all(); } -async function listReadySessionFiles( +type CurrentSessionArtifactFileRow = Pick< + FileRecordRow, + "committed" | "created_at" | "id" | "mime_type" | "name" | "session_kind" | "size" +>; + +interface CurrentSessionArtifactSourceRow { + readonly authority_source_path: string; + readonly object_key: string; + readonly size: number; +} + +const CURRENT_SESSION_ARTIFACT_AUTHORITY_CTE = ` +WITH input(session_id) AS ( + VALUES (?) +), +artifact_authority AS ( + SELECT artifact_head.source_path, artifact_head.file_id + FROM session_artifact_head AS artifact_head + INNER JOIN input ON input.session_id = artifact_head.session_id +)`; + +const CURRENT_SESSION_ARTIFACT_FILES_SQL = `${CURRENT_SESSION_ARTIFACT_AUTHORITY_CTE} +SELECT + file_record.committed, + file_record.created_at, + file_record.id, + file_record.mime_type, + file_record.name, + file_record.session_kind, + file_record.size, + artifact_authority.source_path +FROM artifact_authority +INNER JOIN file_record ON file_record.id = artifact_authority.file_id +INNER JOIN input ON input.session_id = file_record.scope_id +WHERE file_record.scope_kind = 'session' + AND file_record.session_kind = 'artifact' + AND file_record.status = 'ready' +ORDER BY file_record.created_at DESC, file_record.id DESC`; + +const CURRENT_SESSION_ARTIFACT_SOURCES_SQL = `${CURRENT_SESSION_ARTIFACT_AUTHORITY_CTE} +SELECT + file_record.object_key, + file_record.size, + artifact_authority.source_path AS authority_source_path +FROM artifact_authority +INNER JOIN file_record ON file_record.id = artifact_authority.file_id +INNER JOIN input ON input.session_id = file_record.scope_id +WHERE file_record.scope_kind = 'session' + AND file_record.session_kind = 'artifact' + AND file_record.status = 'ready' +ORDER BY artifact_authority.source_path`; + +async function listCurrentSessionArtifactFiles( database: D1Database, sessionId: SessionId, -): Promise { - const rows = await getAppDatabase(database) - .select(fileRecordRowColumns) - .from(fileRecordsTable) - .where( - and( - eq(fileRecordsTable.scopeKind, "session"), - eq(fileRecordsTable.scopeId, sessionId), - eq(fileRecordsTable.status, "ready"), - ), - ) - .orderBy(desc(fileRecordsTable.createdAt)) - .all(); +): Promise { + return getAppDatabase(database).all( + parameterizedSql(CURRENT_SESSION_ARTIFACT_FILES_SQL, [sessionId]), + ); +} - return rows.map(toSessionFile); +async function listCurrentSessionArtifactSources( + database: D1Database, + sessionId: SessionId, +): Promise { + return getAppDatabase(database).all( + parameterizedSql(CURRENT_SESSION_ARTIFACT_SOURCES_SQL, [sessionId]), + ); } -async function listReadySessionArtifactKeys( +async function listReadySessionFiles( database: D1Database, sessionId: SessionId, -): Promise { - const rows = await getAppDatabase(database) - .select({ - parentPath: fileRecordsTable.parentPath, - }) - .from(fileRecordsTable) - .where( - and( - eq(fileRecordsTable.scopeKind, "session"), - eq(fileRecordsTable.scopeId, sessionId), - eq(fileRecordsTable.status, "ready"), - eq(fileRecordsTable.sessionKind, "artifact"), - ), - ) - .all(); +): Promise { + const db = getAppDatabase(database); + const [rows, artifacts] = await Promise.all([ + db + .select(fileRecordRowColumns) + .from(fileRecordsTable) + .where( + and( + eq(fileRecordsTable.scopeKind, "session"), + eq(fileRecordsTable.scopeId, sessionId), + eq(fileRecordsTable.status, "ready"), + sql`${fileRecordsTable.sessionKind} IS NOT 'artifact'`, + ), + ) + .all(), + listCurrentSessionArtifactFiles(database, sessionId), + ]); - return rows.map((row) => row.parentPath); + return [...rows, ...artifacts] + .toSorted( + (left, right) => right.created_at - left.created_at || right.id.localeCompare(left.id), + ) + .map(toSessionFile); } async function listLatestReadySessionArtifactSources( database: D1Database, sessionId: SessionId, ): Promise { - const rows = await getAppDatabase(database) - .select({ - id: fileRecordsTable.id, - createdAt: fileRecordsTable.createdAt, - objectKey: fileRecordsTable.objectKey, - parentPath: fileRecordsTable.parentPath, - size: fileRecordsTable.size, - }) - .from(fileRecordsTable) - .where( - and( - eq(fileRecordsTable.scopeKind, "session"), - eq(fileRecordsTable.scopeId, sessionId), - eq(fileRecordsTable.status, "ready"), - eq(fileRecordsTable.sessionKind, "artifact"), - ), - ) - .orderBy(asc(fileRecordsTable.createdAt), asc(fileRecordsTable.id)) - .all(); - - // Ascending scan + map overwrite keeps the newest record per source path - // (ULID ids break created-at ties in creation order). - const latestBySourcePath = new Map(); - - for (const row of rows) { - const sourcePath = parseRuntimeOutputSourcePath(row.parentPath); - - if (sourcePath === null) { - continue; - } - - latestBySourcePath.set(sourcePath, { - objectKey: row.objectKey, + const rows = await listCurrentSessionArtifactSources(database, sessionId); + return rows + .map((row) => ({ + objectKey: row.object_key, size: row.size, - sourcePath, - }); - } - - return [...latestBySourcePath.values()].toSorted((left, right) => - left.sourcePath.localeCompare(right.sourcePath), - ); + sourcePath: row.authority_source_path, + })) + .toSorted((left, right) => left.sourcePath.localeCompare(right.sourcePath)); } async function listSessionResources( @@ -899,68 +943,6 @@ async function readSessionArtifactBytes( return body === null ? null : new Uint8Array(await body.arrayBuffer()); } -async function recordRuntimeOutput(input: RuntimeOutputFileInput): Promise { - const fileId = createPlatformId(); - const name = getRuntimeOutputName(input.path); - const contentType = normalizeContentType(input.contentType ?? "application/octet-stream"); - const contentSha256 = input.contentSha256 ?? (await createRuntimeOutputContentSha256(input.body)); - const path = createSessionArtifactPath(fileId, name); - const timestampMs = currentTimestampMs(); - const objectKey = createFinalObjectKey({ - created_by_account_id: input.createdBy, - id: fileId, - name, - path, - scope_id: input.sessionId, - scope_kind: "session", - session_kind: "artifact", - }); - const object = await putObject({ - bindings: input.bindings, - body: input.body, - contentType, - objectKey, - }); - - await getAppDatabase(input.bindings.DB) - .insert(fileRecordsTable) - .values({ - committed: true, - createdAt: timestampMs, - createdByAccountId: input.createdBy, - etag: object.etag, - expiresAt: null, - id: fileId, - mimeType: object.contentType ?? contentType, - name, - objectKey, - ownerId: input.sessionId, - ownerKind: "session", - parentPath: createRuntimeOutputParentPath(input.path, contentSha256), - path, - purpose: "session_artifact", - scopeId: input.sessionId, - scopeKind: "session", - sessionKind: "artifact", - size: object.contentLength, - status: "ready", - updatedAt: timestampMs, - version: 1, - }) - .run(); - - const createdRows = await listFileRecordsById(input.bindings.DB, [fileId]); - const createdRow = createdRows[0]; - - if (createdRow === undefined) { - throw createFileNotFoundError("Runtime output file was not created."); - } - - const file = toFileRecord(createdRow); - await publishSessionResourceUpsert(input.bindings, file); - return file; -} - export { createFileErrorResponse, createUnexpectedFileError, @@ -986,14 +968,12 @@ export const fileStore: FileStore = { getUpload, list, listLatestReadySessionArtifactSources, - listReadySessionArtifactKeys, listReadySessionFiles, listSessionResourcePathEntries, listSessionResources, putContent, putPart, readSessionArtifactBytes, - recordRuntimeOutput, streamContent, update, }; diff --git a/apps/api/src/modules/files/infrastructure/file-record-model.ts b/apps/api/src/modules/files/infrastructure/file-record-model.ts index f9011cc0..bae51647 100644 --- a/apps/api/src/modules/files/infrastructure/file-record-model.ts +++ b/apps/api/src/modules/files/infrastructure/file-record-model.ts @@ -97,7 +97,7 @@ const SHA256_PATTERN = /^[a-f0-9]{64}$/; // path an artifact was recorded from. Path segments the writer could never // produce (empty, ".", "..") mark the record malformed, which keeps the value // safe to use as a workspace-relative write target. -export function parseRuntimeOutputSourcePath(parentPath: string): string | null { +function parseRuntimeOutputSourcePath(parentPath: string): string | null { const segments = parentPath.split("/"); const contentSha256 = segments.at(-1); @@ -114,8 +114,9 @@ export function parseRuntimeOutputSourcePath(parentPath: string): string | null const hasUnsafeSegment = pathSegments.some( (segment) => segment.length === 0 || segment === "." || segment === "..", ); + const sourcePath = pathSegments.join("/"); - return hasUnsafeSegment ? null : pathSegments.join("/"); + return hasUnsafeSegment || !sourcePath.startsWith("outputs/") ? null : sourcePath; } function toRuntimeOutputSourcePath(row: FileRecordRow): string | null { @@ -255,7 +256,12 @@ export function toUploadSummary(upload: FileUploadRow, file: FileRecordRow): Fil }; } -export function toSessionFile(row: FileRecordRow): SessionFile { +type SessionFileRow = Pick< + FileRecordRow, + "committed" | "created_at" | "id" | "mime_type" | "name" | "session_kind" | "size" +>; + +export function toSessionFile(row: SessionFileRow): SessionFile { return { committed: row.committed === 1, createdAt: toIsoString(row.created_at), diff --git a/apps/api/src/modules/files/infrastructure/file-record-store.ts b/apps/api/src/modules/files/infrastructure/file-record-store.ts index 84eccc4e..b4970340 100644 --- a/apps/api/src/modules/files/infrastructure/file-record-store.ts +++ b/apps/api/src/modules/files/infrastructure/file-record-store.ts @@ -11,7 +11,6 @@ export { } from "./file-record-mutations"; export { fileRecordRowColumns, - parseRuntimeOutputSourcePath, toFileEntry, toFileRecord, toSessionFile, diff --git a/apps/api/src/modules/files/infrastructure/r2-s3-client-types.ts b/apps/api/src/modules/files/infrastructure/r2-s3-client-types.ts index fd39430d..3fc20a19 100644 --- a/apps/api/src/modules/files/infrastructure/r2-s3-client-types.ts +++ b/apps/api/src/modules/files/infrastructure/r2-s3-client-types.ts @@ -55,6 +55,7 @@ export interface PutObjectInput { bindings: ApiBindings; body: ReadableStream | ArrayBuffer | ArrayBufferView | string | Blob | null; contentType: string; + customMetadata?: Record | undefined; objectKey: string; options?: PutObjectOptions | undefined; } diff --git a/apps/api/src/modules/files/infrastructure/r2-s3-object-client.ts b/apps/api/src/modules/files/infrastructure/r2-s3-object-client.ts index 3ba78a6c..2259430f 100644 --- a/apps/api/src/modules/files/infrastructure/r2-s3-object-client.ts +++ b/apps/api/src/modules/files/infrastructure/r2-s3-object-client.ts @@ -233,6 +233,7 @@ export async function copyObject(input: CopyObjectInput): Promise { const options = input.options ?? {}; const putOptions: R2PutOptions = { + ...(input.customMetadata === undefined ? {} : { customMetadata: input.customMetadata }), httpMetadata: { contentType: input.contentType, }, diff --git a/apps/api/src/modules/mcp/application/mcp-mappers.ts b/apps/api/src/modules/mcp/application/mcp-mappers.ts index dc269ecc..356d5cdf 100644 --- a/apps/api/src/modules/mcp/application/mcp-mappers.ts +++ b/apps/api/src/modules/mcp/application/mcp-mappers.ts @@ -120,7 +120,7 @@ function toCredentialSummary(row: CredentialRow): McpCredentialSummary { id: row.id, scope: row.scope, scopeValues: decodeJsonArray(row.scopeValuesJson), - status: getCredentialStatus(row) as Exclude, + status: getCredentialStatus(row), subjectLabel: row.subjectLabel, updatedAt: toIsoString(row.updatedAt), }; @@ -192,9 +192,7 @@ export function toAgentBinding( export function toOAuthFlowState(flow: OAuthFlowRow, server: ServerRow): McpOAuthFlowState { return { authorizationState: - flow.status === "succeeded" - ? ((server.enabled === 1 ? "active" : "disabled") as McpAuthorizationState) - : null, + flow.status === "succeeded" ? (server.enabled === 1 ? "active" : "disabled") : null, errorMessage: flow.errorMessage, flowId: flow.id, serverId: flow.serverId, diff --git a/apps/api/src/modules/public-api/app-agent-bound-ask.service.ts b/apps/api/src/modules/public-api/app-agent-bound-ask.service.ts index 218b769f..d3625d51 100644 --- a/apps/api/src/modules/public-api/app-agent-bound-ask.service.ts +++ b/apps/api/src/modules/public-api/app-agent-bound-ask.service.ts @@ -350,7 +350,6 @@ async function startBoundAgentRun(input: { if (createdSessionId !== null && input.recoverableSessionId === undefined) { await cleanupFailedThreadCreation({ bindings: input.bindings, - fileIds: [], sessionId: createdSessionId, }).catch((cleanupError: unknown) => { logError("public-api.bound_agent_call.cleanup_failed", { diff --git a/apps/api/src/modules/public-api/app-agent-capability.ts b/apps/api/src/modules/public-api/app-agent-capability.ts index 35d93d99..c8ed0535 100644 --- a/apps/api/src/modules/public-api/app-agent-capability.ts +++ b/apps/api/src/modules/public-api/app-agent-capability.ts @@ -15,6 +15,8 @@ import type { AgentId, AppDeploymentId, AppDeploymentRunId, AppId } from "@mosoo/id"; import { isPlatformId } from "@mosoo/id"; +import { fromBase64Url, toBase64Url } from "../../shared/bytes"; + export type AppAgentCapabilityExpose = "public_thread"; export interface AppAgentCapabilityBinding { @@ -56,24 +58,6 @@ export function boundAgentUrl(apiOrigin: string, token: string): string { return `${stripTrailingSlashes(apiOrigin)}${APP_AGENT_BOUND_PATH_PREFIX}/${token}`; } -function bytesToBase64Url(bytes: Uint8Array): string { - let binary = ""; - for (const byte of bytes) { - binary += String.fromCharCode(byte); - } - return btoa(binary).replaceAll("+", "-").replaceAll("/", "_").replaceAll("=", ""); -} - -function base64UrlToBytes(value: string): Uint8Array { - const normalized = value.replaceAll("-", "+").replaceAll("_", "/"); - const binary = atob(normalized); - const bytes = new Uint8Array(binary.length); - for (let index = 0; index < binary.length; index += 1) { - bytes[index] = binary.charCodeAt(index); - } - return bytes; -} - async function importSigningKey(secret: string): Promise { return crypto.subtle.importKey("raw", new TextEncoder().encode(secret), HMAC_PARAMS, false, [ "sign", @@ -112,14 +96,14 @@ export async function mintAppAgentCapabilityToken( secret: string, claims: AppAgentCapabilityClaims, ): Promise { - const payload = bytesToBase64Url(new TextEncoder().encode(JSON.stringify(claims))); + const payload = toBase64Url(new TextEncoder().encode(JSON.stringify(claims))); const key = await importSigningKey(secret); const signature = await crypto.subtle.sign( HMAC_PARAMS.name, key, new TextEncoder().encode(payload), ); - return `${payload}.${bytesToBase64Url(new Uint8Array(signature))}`; + return `${payload}.${toBase64Url(new Uint8Array(signature))}`; } /** @@ -159,7 +143,7 @@ export async function inspectAppAgentCapabilityToken( signatureValid = await crypto.subtle.verify( HMAC_PARAMS.name, key, - base64UrlToBytes(signaturePart), + fromBase64Url(signaturePart), new TextEncoder().encode(payload), ); } catch { @@ -171,7 +155,7 @@ export async function inspectAppAgentCapabilityToken( let parsed: unknown; try { - parsed = JSON.parse(new TextDecoder().decode(base64UrlToBytes(payload))); + parsed = JSON.parse(new TextDecoder().decode(fromBase64Url(payload))); } catch { return { status: "invalid" }; } diff --git a/apps/api/src/modules/public-api/public-api-idempotency.service.ts b/apps/api/src/modules/public-api/public-api-idempotency.service.ts index ae7cf869..5c63ec93 100644 --- a/apps/api/src/modules/public-api/public-api-idempotency.service.ts +++ b/apps/api/src/modules/public-api/public-api-idempotency.service.ts @@ -78,7 +78,7 @@ export async function hashPublicApiIdempotencyBody(value: unknown): Promise byte.toString(16).padStart(2, "0")).join(""); + return new Uint8Array(digest).toHex(); } function enforceSameIdempotentRequest( diff --git a/apps/api/src/modules/public-api/public-thread-create.ts b/apps/api/src/modules/public-api/public-thread-create.ts index c43024a1..93d40d64 100644 --- a/apps/api/src/modules/public-api/public-thread-create.ts +++ b/apps/api/src/modules/public-api/public-thread-create.ts @@ -162,7 +162,6 @@ export async function createPublicThread( if (createdSessionId !== null) { await cleanupFailedThreadCreation({ bindings: request.bindings, - fileIds: request.input.fileIds, sessionId: createdSessionId, }).catch((cleanupError: unknown) => { logError("public-api.thread.cleanup_failed", { diff --git a/apps/api/src/modules/public-api/public-thread-events.ts b/apps/api/src/modules/public-api/public-thread-events.ts index 0e250ba4..7af58ebe 100644 --- a/apps/api/src/modules/public-api/public-thread-events.ts +++ b/apps/api/src/modules/public-api/public-thread-events.ts @@ -12,16 +12,30 @@ import { import type { SessionProcessEvent } from "@mosoo/contracts/session"; import { parseJsonObject } from "@mosoo/contracts/validation"; import type { JsonObject } from "@mosoo/contracts/validation"; -import { sessionEventsTable, sessionMessagesTable } from "@mosoo/db"; +import { sessionEventsTable, sessionMessagesTable, sessionRunsTable } from "@mosoo/db"; import { parsePlatformId } from "@mosoo/id"; -import type { RuntimeEventId, SessionId, SessionRunId } from "@mosoo/id"; -import { and, asc, desc, eq, gt, lt } from "drizzle-orm"; +import type { RuntimeEventId, SessionId, SessionMessageId, SessionRunId } from "@mosoo/id"; +import { createSessionRunTerminalSourceId } from "@mosoo/runtime-events"; +import { and, asc, desc, eq, gt, inArray, lt } from "drizzle-orm"; import type { SQL } from "drizzle-orm"; import { createErrorLogContext, logWarn } from "../../platform/cloudflare/logger"; import { getAppDatabase } from "../../platform/db/drizzle"; import { createSessionProcessEventsFromSessionEventRows } from "../sessions/application/session-process-events.service"; import type { SessionEventProcessRow } from "../sessions/application/session-process-events.service"; +import { + excludeSessionEventStreams, + findLeftIncompleteSessionEventStreamKeys, + foldStreamedSessionEventRows, + getMessageStreamTextFragment, + getSessionEventStreamKey, +} from "../sessions/domain/session-event-stream-fold"; +import { readTerminalEventSemanticAuthority } from "../sessions/domain/session-terminal-event-authority"; +import type { SessionMessageEventStreamCursor } from "../sessions/infrastructure/session-message-event-stream.repository"; +import { + iteratePublicSessionMessageEventRows, + readSealedPublicSessionMessage, +} from "../sessions/infrastructure/session-message-event-stream.repository"; import { connectSessionPublicEventWebSocket } from "../sessions/infrastructure/session/client"; import { publicInternalError, publicInvalidRequest, toPublicApiError } from "./public-api-errors"; import { sanitizePublicOutput } from "./public-output-sanitization"; @@ -44,8 +58,10 @@ const SSE_TEXT_ENCODER = new TextEncoder(); interface PublicThreadEventWindow { events: PublicThreadEventLogEntry[]; + leftIncompleteStreamKeys: Set; latestSeq: number | null; rows: PublicThreadEventProcessRow[]; + trustUnknownMessageDeltas: boolean; truncated: boolean; } @@ -54,130 +70,276 @@ interface PublicThreadEventProcessRow extends SessionEventProcessRow { tool_call_id: string | null; tool_input_json: string | null; tool_name: string | null; + tool_status: "cancelled" | "completed" | "failed" | "running" | null; +} + +interface LiveMessageSnapshot { + endSeq: number; + row: PublicThreadEventProcessRow; + sealed: boolean; + startSeq: number; } interface LiveMessageState { - filter: OpenAiPrivateCitationStreamFilter; - text: string; + draftEndSeq: number; + draftFilter: OpenAiPrivateCitationStreamFilter; + draftProjectedChars: number; + draftStartSeq: number; + emittedCursor: SessionMessageEventStreamCursor | null; + firstOccurredAt: number; + firstSeq: number; + messageTerminal: boolean; + runId: SessionRunId | null; + runTerminal: boolean; + snapshot: LiveMessageSnapshot | null; + trusted: boolean; } -class PublicLiveEventRowProjector { - readonly #messages = new Map(); - readonly #messageKeysByRun = new Map>(); +interface PublicMessageReconciliationRequest { + candidateCursor: SessionMessageEventStreamCursor; + emittedCursor: SessionMessageEventStreamCursor | null; + key: string; + outputRow: PublicThreadEventProcessRow; + sourceRow: PublicThreadEventProcessRow; +} - #getMessageRunKey(row: PublicThreadEventProcessRow): string { - return row.run_id ?? ""; - } +interface PublicMessageReconciliationResult { + compatible: boolean; + emitted: boolean; +} - #getMessageKey(row: PublicThreadEventProcessRow): string { - return `${this.#getMessageRunKey(row)}:${row.process_type}`; - } +function projectPublicMessageText(text: string, finish: boolean): string { + const filter = new OpenAiPrivateCitationStreamFilter(); + const projected = filter.push(text).text; + return finish ? projected + filter.finish().text : projected; +} - #setMessage(row: PublicThreadEventProcessRow, message: LiveMessageState): void { - const key = this.#getMessageKey(row); - const runKey = this.#getMessageRunKey(row); - const runKeys = this.#messageKeysByRun.get(runKey); +class PublicLiveEventRowProjector { + #isSeeding = false; + readonly #messages = new Map(); + readonly #trustedUnknownDeltaRuns = new Set(); + #trustUnknownMessageDeltas = true; + + seedFromCanonicalEvents( + rows: readonly PublicThreadEventProcessRow[], + events: readonly PublicThreadEventLogEntry[], + leftIncompleteStreamKeys: ReadonlySet = new Set(), + trustUnknownMessageDeltas = true, + ): PublicThreadEventLogEntry[] { + this.#trustUnknownMessageDeltas = trustUnknownMessageDeltas; + for (const row of rows) { + if (row.process_type !== "agent.message.delta" || !row.event_type.startsWith("message.")) { + continue; + } + const key = this.#getMessageKey(row); + if (leftIncompleteStreamKeys.has(key) && !this.#messages.has(key)) { + this.#messages.set(key, this.#createMessage(row, false)); + } + } - this.#messages.set(key, message); - if (runKeys === undefined) { - this.#messageKeysByRun.set(runKey, new Set([key])); - } else { - runKeys.add(key); + this.#isSeeding = true; + try { + this.project(rows); + } finally { + this.#isSeeding = false; } - } - #deleteMessage(row: PublicThreadEventProcessRow): void { - const key = this.#getMessageKey(row); - const runKey = this.#getMessageRunKey(row); - const runKeys = this.#messageKeysByRun.get(runKey); + const rowsByEventId = new Map(rows.map((row) => [row.id, row])); + return events.map((event) => { + const row = rowsByEventId.get(event.id); + if (row?.event_type.startsWith("message.") !== true) { + return event; + } - this.#messages.delete(key); - runKeys?.delete(key); - if (runKeys?.size === 0) { - this.#messageKeysByRun.delete(runKey); - } - } + const message = this.#messages.get(this.#getMessageKey(row)); + if (message === undefined) { + return event; + } - #deleteRunMessages(runId: SessionRunId | null): void { - const runKey = runId ?? ""; - const messageKeys = this.#messageKeysByRun.get(runKey); + const cursor = this.#currentCursor(message); + message.emittedCursor = cursor; + const content = projectPublicMessageText(event.content, cursor.finish); + return content === event.content ? event : { ...event, content }; + }); + } - if (messageKeys === undefined) { - return; + getReconciliationRequests( + row: PublicThreadEventProcessRow, + ): PublicMessageReconciliationRequest[] { + if (row.event_type === "message.completed") { + const key = this.#getMessageKey(row); + const message = this.#messages.get(key); + if ( + message?.snapshot === null || + message?.snapshot === undefined || + message.runTerminal || + message.snapshot.sealed || + !message.trusted + ) { + return []; + } + return [this.#createReconciliationRequest(key, message, row, true)]; } - for (const messageKey of messageKeys) { - this.#messages.delete(messageKey); + if ( + row.event_type !== "run.cancelled" && + row.event_type !== "run.completed" && + row.event_type !== "run.failed" + ) { + return []; } - this.#messageKeysByRun.delete(runKey); + const requests: PublicMessageReconciliationRequest[] = []; + for (const [key, message] of this.#messages) { + if ( + message.runId === row.run_id && + message.snapshot !== null && + !message.snapshot.sealed && + !message.runTerminal && + message.trusted + ) { + requests.push(this.#createReconciliationRequest(key, message, row, false)); + } + } + return requests; } - project(rows: readonly PublicThreadEventProcessRow[]): PublicThreadEventProcessRow[] { + project( + rows: readonly PublicThreadEventProcessRow[], + reconciled: ReadonlyMap = new Map(), + ): PublicThreadEventProcessRow[] { const output: PublicThreadEventProcessRow[] = []; for (const row of rows) { - const key = this.#getMessageKey(row); - if (row.event_type === "message.started") { - this.#setMessage(row, { - filter: new OpenAiPrivateCitationStreamFilter(), - text: "", - }); + const key = this.#getMessageKey(row); + if (!this.#messages.has(key)) { + this.#messages.set(key, this.#createMessage(row)); + } continue; } if (row.event_type === "message.delta") { + const key = this.#getMessageKey(row); let message = this.#messages.get(key); - if (message === undefined) { - message = { filter: new OpenAiPrivateCitationStreamFilter(), text: "" }; - this.#setMessage(row, message); + const runBoundaryIsTrusted = + row.run_id !== null && this.#trustedUnknownDeltaRuns.has(row.run_id); + if ( + !this.#trustUnknownMessageDeltas && + !runBoundaryIsTrusted && + row.stream_id !== row.id + ) { + continue; + } + message = this.#createMessage(row); + this.#messages.set(key, message); + } + if (!message.trusted) { + continue; + } + if (message.snapshot !== null) { + message.snapshot.endSeq = row.seq; + message.snapshot.sealed = false; + message.messageTerminal = false; + continue; } - const contentText = message.filter.push(row.content_text).text; - message.text += contentText; - + const projectedBefore = message.draftProjectedChars; + const contentText = message.draftFilter.push(row.content_text).text; + message.draftEndSeq = row.seq; + message.draftProjectedChars += contentText.length; + message.messageTerminal = false; if (contentText.length > 0) { + this.#recordDraftOutput(message, row.seq, projectedBefore); output.push({ ...row, content_text: contentText }); } continue; } - if (row.event_type === "message.completed") { - const message = this.#messages.get(key); - const contentText = message?.filter.finish().text ?? ""; - - if (message !== undefined) { - message.text += contentText; + if (row.event_type === "message.added") { + const key = this.#getMessageKey(row); + let message = this.#messages.get(key); + if (message === undefined && row.process_type === "agent.message.delta") { + message = this.#createMessage(row); + this.#messages.set(key, message); } - if (contentText.length > 0) { - output.push({ ...row, content_text: contentText }); + if (message === undefined) { + output.push({ ...row, content_text: sanitizePublicOutput(row.content_text).text }); + continue; } + if (message.runTerminal && !this.#isSeeding) { + message.trusted = false; + continue; + } + message.trusted = true; + message.messageTerminal = false; + message.snapshot = { + endSeq: row.seq, + row: { ...row, content_text: "" }, + sealed: false, + startSeq: row.seq, + }; continue; } - if (row.event_type === "message.added") { + if ( + row.event_type === "message.cancelled" || + row.event_type === "message.completed" || + row.event_type === "message.failed" + ) { + const key = this.#getMessageKey(row); const message = this.#messages.get(key); - this.#deleteMessage(row); - - if (message !== undefined) { - const snapshotText = sanitizePublicOutput(row.content_text).text; - - // SSE is append-only; a divergent snapshot stays canonical through - // run.finalOutput because emitted text cannot be retracted. - if (snapshotText.startsWith(message.text)) { - const suffix = snapshotText.slice(message.text.length); - - if (suffix.length > 0) { - output.push({ ...row, content_text: suffix }); - } + if (message === undefined || message.runTerminal) { + if (message === undefined) { + output.push({ ...row, content_text: "" }); } continue; } + if (!message.trusted && message.snapshot === null) { + message.messageTerminal = true; + continue; + } + const wasTerminal = message.messageTerminal; + let contentText = ""; + if (row.event_type === "message.completed" && message.snapshot !== null) { + message.snapshot.endSeq = row.seq; + message.snapshot.sealed = true; + if (!this.#isSeeding) { + const compatible = reconciled.get(key)?.compatible === true; + message.trusted = compatible; + if (compatible) { + message.emittedCursor = this.#snapshotCursor(message.snapshot, true); + } + } + } else { + message.snapshot = null; + if (!this.#isSeeding) { + contentText = message.draftFilter.finish().text; + const projectedBefore = message.draftProjectedChars; + message.draftProjectedChars += contentText.length; + message.draftEndSeq = row.seq; + if (contentText.length > 0) { + this.#recordDraftOutput(message, row.seq, projectedBefore); + } + if (message.emittedCursor?.startSeq === message.draftStartSeq) { + message.emittedCursor = { ...message.emittedCursor, endSeq: row.seq, finish: true }; + } + } + message.trusted = row.event_type === "message.completed"; + } + message.messageTerminal = true; + if ((!wasTerminal || contentText.length > 0) && reconciled.get(key)?.emitted !== true) { + output.push({ ...row, content_text: contentText }); + } + continue; } - if (row.event_type === "thought.started" || row.event_type === "thought.completed") { + if ( + row.event_type === "thought.cancelled" || + row.event_type === "thought.completed" || + row.event_type === "thought.started" + ) { continue; } @@ -186,7 +348,41 @@ class PublicLiveEventRowProjector { row.event_type === "run.completed" || row.event_type === "run.failed" ) { - this.#deleteRunMessages(row.run_id); + for (const [key, message] of this.#messages) { + if (message.runId !== row.run_id) { + continue; + } + if (message.snapshot !== null && !message.snapshot.sealed && !this.#isSeeding) { + const compatible = reconciled.get(key)?.compatible === true; + message.trusted = compatible; + if (compatible) { + message.emittedCursor = this.#snapshotCursor(message.snapshot, false); + } + } + message.runTerminal = true; + } + if (row.run_id !== null) { + this.#trustedUnknownDeltaRuns.delete(row.run_id); + } + output.push(row); + continue; + } + + if (row.event_type === "run.started") { + let released = false; + for (const [key, message] of this.#messages) { + if (message.runId !== row.run_id) { + this.#messages.delete(key); + released = true; + } + } + if (released) { + this.#trustUnknownMessageDeltas = false; + } + if (row.run_id !== null) { + this.#trustedUnknownDeltaRuns.clear(); + this.#trustedUnknownDeltaRuns.add(row.run_id); + } } output.push(row); @@ -194,6 +390,261 @@ class PublicLiveEventRowProjector { return output; } + + #createMessage(row: PublicThreadEventProcessRow, trusted = true): LiveMessageState { + return { + draftEndSeq: row.seq, + draftFilter: new OpenAiPrivateCitationStreamFilter(), + draftProjectedChars: 0, + draftStartSeq: row.seq, + emittedCursor: null, + firstOccurredAt: row.occurred_at, + firstSeq: row.seq, + messageTerminal: false, + runId: row.run_id, + runTerminal: false, + snapshot: null, + trusted, + }; + } + + #getMessageKey(row: PublicThreadEventProcessRow): string { + const key = getSessionEventStreamKey(row); + if (key === null) { + throw new Error(`Persisted ${row.event_type} event is missing its stream ID.`); + } + return key; + } + + #currentCursor(message: LiveMessageState): SessionMessageEventStreamCursor { + return message.snapshot === null + ? { + endSeq: message.draftEndSeq, + finish: message.messageTerminal, + outputOffset: 0, + startSeq: message.draftStartSeq, + } + : this.#snapshotCursor(message.snapshot, message.snapshot.sealed); + } + + #snapshotCursor(snapshot: LiveMessageSnapshot, finish: boolean): SessionMessageEventStreamCursor { + return { + endSeq: snapshot.endSeq, + finish, + outputOffset: 0, + startSeq: snapshot.startSeq, + }; + } + + #createReconciliationRequest( + key: string, + message: LiveMessageState, + boundaryRow: PublicThreadEventProcessRow, + finish: boolean, + ): PublicMessageReconciliationRequest { + const snapshot = message.snapshot; + if (snapshot === null) { + throw new Error("Cannot reconcile a message without an authoritative snapshot."); + } + return { + candidateCursor: { + ...this.#snapshotCursor(snapshot, finish), + endSeq: finish ? boundaryRow.seq : snapshot.endSeq, + }, + emittedCursor: message.emittedCursor, + key, + outputRow: { + ...(finish ? boundaryRow : snapshot.row), + event_type: "message.delta", + occurred_at: message.firstOccurredAt, + seq: message.firstSeq, + }, + sourceRow: snapshot.row, + }; + } + + #recordDraftOutput(message: LiveMessageState, endSeq: number, outputOffset: number): void { + if (message.emittedCursor === null) { + message.emittedCursor = { + endSeq, + finish: false, + outputOffset, + startSeq: message.draftStartSeq, + }; + } else if (message.emittedCursor.startSeq === message.draftStartSeq) { + message.emittedCursor = { ...message.emittedCursor, endSeq, finish: false }; + } + } +} + +interface ProjectedMessageChunk { + row: PublicThreadEventProcessRow; + text: string; +} + +async function* iterateProjectedMessageChunks(input: { + cursor: SessionMessageEventStreamCursor; + database: D1Database; + row: PublicThreadEventProcessRow; + sessionId: SessionId; +}): AsyncGenerator { + if (input.row.stream_id === null) { + throw new Error(`Persisted ${input.row.event_type} event is missing its stream ID.`); + } + + let filter = new OpenAiPrivateCitationStreamFilter(); + let lastContentRow: PublicThreadEventProcessRow | null = null; + let outputOffset = input.cursor.outputOffset; + let pending: ProjectedMessageChunk | null = null; + + for await (const row of iteratePublicSessionMessageEventRows(input.database, { + cursor: input.cursor, + processType: input.row.process_type, + runId: input.row.run_id, + sessionId: input.sessionId, + streamId: input.row.stream_id, + })) { + const fragment = getMessageStreamTextFragment(row); + if (fragment === null) { + continue; + } + if (fragment.kind === "reset") { + filter = new OpenAiPrivateCitationStreamFilter(); + } + + const projectedRow: PublicThreadEventProcessRow = { + ...row, + tool_call_id: null, + tool_input_json: null, + tool_name: null, + tool_status: null, + }; + lastContentRow = projectedRow; + let text = filter.push(fragment.text).text; + if (outputOffset >= text.length) { + outputOffset -= text.length; + text = ""; + } else if (outputOffset > 0) { + text = text.slice(outputOffset); + outputOffset = 0; + } + if (text.length === 0) { + continue; + } + if (pending !== null) { + yield pending; + } + pending = { row: projectedRow, text }; + } + + if (input.cursor.finish) { + let tail = filter.finish().text; + if (outputOffset >= tail.length) { + outputOffset -= tail.length; + tail = ""; + } else if (outputOffset > 0) { + tail = tail.slice(outputOffset); + outputOffset = 0; + } + if (tail.length > 0) { + if (pending === null) { + if (lastContentRow === null) { + throw new Error("Cannot finish a projected message without a content row."); + } + pending = { row: lastContentRow, text: tail }; + } else { + pending = { ...pending, text: pending.text + tail }; + } + } + } + + if (pending !== null) { + yield pending; + } +} + +async function reconcilePublicMessage(input: { + database: D1Database; + emit: (chunk: ProjectedMessageChunk) => void; + request: PublicMessageReconciliationRequest; + sessionId: SessionId; +}): Promise { + const suffix: string[] = []; + const emitSuffix = () => { + if (suffix.length === 0) { + return false; + } + input.emit({ row: input.request.outputRow, text: suffix.join("") }); + return true; + }; + const candidate = iterateProjectedMessageChunks({ + cursor: input.request.candidateCursor, + database: input.database, + row: input.request.sourceRow, + sessionId: input.sessionId, + })[Symbol.asyncIterator](); + + if (input.request.emittedCursor === null) { + let chunk = await candidate.next(); + while (!chunk.done) { + suffix.push(chunk.value.text); + chunk = await candidate.next(); + } + return { compatible: true, emitted: emitSuffix() }; + } + + const emitted = iterateProjectedMessageChunks({ + cursor: input.request.emittedCursor, + database: input.database, + row: input.request.sourceRow, + sessionId: input.sessionId, + })[Symbol.asyncIterator](); + let candidateChunk = await candidate.next(); + let candidateOffset = 0; + + for (;;) { + const emittedChunk = await emitted.next(); + if (emittedChunk.done) { + break; + } + + let emittedOffset = 0; + while (emittedOffset < emittedChunk.value.text.length) { + if (candidateChunk.done) { + await emitted.return?.(undefined); + return { compatible: false, emitted: false }; + } + const comparedLength = Math.min( + emittedChunk.value.text.length - emittedOffset, + candidateChunk.value.text.length - candidateOffset, + ); + if ( + emittedChunk.value.text.slice(emittedOffset, emittedOffset + comparedLength) !== + candidateChunk.value.text.slice(candidateOffset, candidateOffset + comparedLength) + ) { + await candidate.return?.(undefined); + await emitted.return?.(undefined); + return { compatible: false, emitted: false }; + } + emittedOffset += comparedLength; + candidateOffset += comparedLength; + if (candidateOffset === candidateChunk.value.text.length) { + candidateChunk = await candidate.next(); + candidateOffset = 0; + } + } + } + + if (!candidateChunk.done && candidateOffset < candidateChunk.value.text.length) { + suffix.push(candidateChunk.value.text.slice(candidateOffset)); + } + for (;;) { + candidateChunk = await candidate.next(); + if (candidateChunk.done) { + return { compatible: true, emitted: emitSuffix() }; + } + suffix.push(candidateChunk.value.text); + } } class PublicThreadEventWakeup { @@ -327,7 +778,11 @@ function toPublicThreadEventLogEntry(input: { let toolInput: JsonObject | undefined; if (input.row?.tool_input_json !== null && input.row?.tool_input_json !== undefined) { - toolInput = parseJsonObject(JSON.parse(input.row.tool_input_json), "Persisted tool input"); + try { + toolInput = parseJsonObject(JSON.parse(input.row.tool_input_json), "Persisted tool input"); + } catch { + toolInput = undefined; + } } return { @@ -351,13 +806,12 @@ function toPublicThreadEventLogEntry(input: { function toPublicThreadEventLogEntries( rows: PublicThreadEventProcessRow[], - options: { foldStreamedRows?: boolean } = {}, ): PublicThreadEventLogEntry[] { const rowsByEventId = new Map( rows.map((row) => [row.id, row]), ); - return createSessionProcessEventsFromSessionEventRows(rows, options).flatMap((event) => { + return createSessionProcessEventsFromSessionEventRows(rows).flatMap((event) => { const publicEvent = toPublicThreadEventLogEntry({ event, row: rowsByEventId.get(event.id), @@ -366,6 +820,15 @@ function toPublicThreadEventLogEntries( }); } +function toCanonicalPublicThreadEventLogEntries( + rows: PublicThreadEventProcessRow[], +): PublicThreadEventLogEntry[] { + return new PublicLiveEventRowProjector().seedFromCanonicalEvents( + rows, + toPublicThreadEventLogEntries(rows), + ); +} + function selectPublicThreadEventRows(input: { database: D1Database; filters: SQL[]; @@ -383,9 +846,11 @@ function selectPublicThreadEventRows(input: { process_type: sessionEventsTable.processType, run_id: sessionEventsTable.runId, seq: sessionEventsTable.seq, + stream_id: sessionEventsTable.streamId, tool_call_id: sessionEventsTable.toolCallId, tool_input_json: sessionEventsTable.toolInputJson, tool_name: sessionEventsTable.toolName, + tool_status: sessionEventsTable.toolStatus, tokens: sessionEventsTable.tokens, }) .from(sessionEventsTable) @@ -407,7 +872,8 @@ async function readPublicThreadEventWindow(input: { while (scannedRows.length < THREAD_EVENT_RAW_ROW_SCAN_LIMIT) { const remainingRows = THREAD_EVENT_RAW_ROW_SCAN_LIMIT - scannedRows.length; - const pageSize = Math.min(THREAD_EVENT_ROW_PAGE_SIZE, remainingRows); + const rowCapacity = Math.min(THREAD_EVENT_ROW_PAGE_SIZE, remainingRows); + const querySize = rowCapacity + 1; const filters = [ eq(sessionEventsTable.sessionId, input.sessionId), eq(sessionEventsTable.visibility, "all_consumers"), @@ -421,7 +887,7 @@ async function readPublicThreadEventWindow(input: { database: input.database, filters, order: desc(sessionEventsTable.seq), - pageSize, + pageSize: querySize, }); if (page.length === 0) { @@ -429,34 +895,60 @@ async function readPublicThreadEventWindow(input: { break; } - latestSeq ??= page[0]?.seq ?? null; - scannedRows.push(...page); - beforeSeq = page[page.length - 1]?.seq ?? beforeSeq; - - const events = toPublicThreadEventLogEntries(scannedRows.toReversed()); + const scannedPage = page.slice(0, rowCapacity); + latestSeq ??= scannedPage[0]?.seq ?? null; + scannedRows.push(...scannedPage); + beforeSeq = scannedPage[scannedPage.length - 1]?.seq ?? beforeSeq; + const chronologicalRows = scannedRows.toReversed(); + const foldedRows = foldStreamedSessionEventRows(chronologicalRows); + const events = toCanonicalPublicThreadEventLogEntries(chronologicalRows); if (events.length > input.limit) { + const reachedDatabaseStart = page.length <= rowCapacity; + const incompleteStreams = reachedDatabaseStart + ? new Set() + : findLeftIncompleteSessionEventStreamKeys(chronologicalRows); + const rowsByEventId = new Map(foldedRows.map((row) => [row.id, row])); + const retainedStreamsAreComplete = events.slice(-input.limit).every((event) => { + const row = rowsByEventId.get(event.id); + const key = row === undefined ? null : getSessionEventStreamKey(row); + return key === null || !incompleteStreams.has(key); + }); + + if (!retainedStreamsAreComplete) { + continue; + } + return { events: events.slice(-input.limit), + leftIncompleteStreamKeys: incompleteStreams, latestSeq, - rows: scannedRows.toReversed(), + rows: chronologicalRows, + trustUnknownMessageDeltas: reachedDatabaseStart, truncated: true, }; } - if (page.length < pageSize) { + if (page.length <= rowCapacity) { reachedStart = true; break; } } - const events = toPublicThreadEventLogEntries(scannedRows.toReversed()); + const chronologicalRows = scannedRows.toReversed(); + const incompleteStreams = reachedStart + ? new Set() + : findLeftIncompleteSessionEventStreamKeys(chronologicalRows); + const completeRows = excludeSessionEventStreams(chronologicalRows, incompleteStreams); + const events = toCanonicalPublicThreadEventLogEntries(completeRows); const truncated = !reachedStart || events.length > input.limit; return { events: truncated ? events.slice(-input.limit) : events, + leftIncompleteStreamKeys: incompleteStreams, latestSeq, - rows: scannedRows.toReversed(), + rows: chronologicalRows, + trustUnknownMessageDeltas: reachedStart, truncated, }; } @@ -483,10 +975,155 @@ export async function readPublicThreadRunFinalOutput(input: { runId: SessionRunId; sessionId: SessionId; }): Promise { + const database = getAppDatabase(input.database); + const snapshotRows = await database + .select({ + eventType: sessionEventsTable.eventType, + runStatus: sessionRunsTable.status, + semanticHash: sessionEventsTable.semanticHash, + seq: sessionEventsTable.seq, + sourceEventId: sessionEventsTable.sourceEventId, + streamId: sessionEventsTable.streamId, + terminalEventJson: sessionEventsTable.terminalEventJson, + }) + .from(sessionRunsTable) + .leftJoin( + sessionEventsTable, + and( + eq(sessionEventsTable.sessionId, sessionRunsTable.sessionId), + eq(sessionEventsTable.runId, sessionRunsTable.id), + inArray(sessionEventsTable.eventType, ["run.cancelled", "run.completed", "run.failed"]), + ), + ) + .where( + and(eq(sessionRunsTable.id, input.runId), eq(sessionRunsTable.sessionId, input.sessionId)), + ) + .limit(3) + .all(); + const [snapshot] = snapshotRows; + if (snapshot?.runStatus !== "completed") { + return null; + } + const terminalRows = snapshotRows.flatMap((row) => + row.eventType === null + ? [] + : [ + { + eventType: row.eventType, + semanticHash: row.semanticHash, + seq: row.seq, + sourceEventId: row.sourceEventId, + streamId: row.streamId, + terminalEventJson: row.terminalEventJson, + }, + ], + ); + if (terminalRows.length !== 1) { + throw new Error(`Final assistant reference for run ${input.runId} is not immutable.`); + } + + const terminal = terminalRows[0]; + if (terminal === undefined) { + throw new Error(`Final assistant reference for run ${input.runId} is not immutable.`); + } + if (terminal.semanticHash !== null) { + if ( + terminal.eventType !== "run.completed" || + terminal.seq === null || + terminal.sourceEventId !== createSessionRunTerminalSourceId(input.runId, "run.completed") + ) { + throw new Error(`Final assistant reference for run ${input.runId} is invalid.`); + } + const semanticAuthority = await readTerminalEventSemanticAuthority({ + eventJson: terminal.terminalEventJson, + eventType: terminal.eventType, + runId: input.runId, + semanticHash: terminal.semanticHash, + sessionId: input.sessionId, + sourceEventId: terminal.sourceEventId, + streamId: terminal.streamId, + }); + if (semanticAuthority.finalMessageId === null) { + return null; + } + const finalMessageId = parsePlatformId( + semanticAuthority.finalMessageId, + "Final assistant message ID", + ); + const message = + (await database + .select({ + content_text: sessionMessagesTable.contentText, + id: sessionMessagesTable.id, + plan_json: sessionMessagesTable.planJson, + projection_format: sessionMessagesTable.projectionFormat, + segments_json: sessionMessagesTable.segmentsJson, + }) + .from(sessionMessagesTable) + .where( + and( + eq(sessionMessagesTable.id, finalMessageId), + eq(sessionMessagesTable.sessionId, input.sessionId), + eq(sessionMessagesTable.sessionRunId, input.runId), + eq(sessionMessagesTable.role, "assistant"), + ), + ) + .limit(1) + .get()) ?? null; + if ( + message === null || + message.content_text !== "" || + message.plan_json !== null || + message.projection_format !== "event_stream_v3" || + message.segments_json !== null + ) { + throw new Error(`Final assistant reference ${finalMessageId} is not lightweight.`); + } + const sealed = await readSealedPublicSessionMessage(input.database, { + endSeq: terminal.seq, + processType: "agent.message.delta", + runId: input.runId, + sessionId: input.sessionId, + streamId: finalMessageId, + }); + if (sealed === null) { + throw new Error(`Final assistant reference ${finalMessageId} is not sealed.`); + } + const sanitizedOutput = sanitizePublicOutput(sealed.text); + return { + text: sanitizedOutput.text, + ...(sanitizedOutput.warnings.length === 0 ? {} : { warnings: sanitizedOutput.warnings }), + }; + } + + if ( + terminal.eventType !== "run.completed" || + terminal.sourceEventId !== createSessionRunTerminalSourceId(input.runId, "run.completed") + ) { + throw new Error(`Legacy final assistant projection for run ${input.runId} is invalid.`); + } + + const eventStreamReference = await database + .select({ id: sessionMessagesTable.id }) + .from(sessionMessagesTable) + .where( + and( + eq(sessionMessagesTable.sessionId, input.sessionId), + eq(sessionMessagesTable.sessionRunId, input.runId), + eq(sessionMessagesTable.role, "assistant"), + eq(sessionMessagesTable.projectionFormat, "event_stream_v3"), + ), + ) + .limit(1) + .get(); + if (eventStreamReference !== undefined) { + throw new Error(`Final assistant reference for run ${input.runId} has no terminal pointer.`); + } + const message = - (await getAppDatabase(input.database) + (await database .select({ - content: sessionMessagesTable.contentText, + content_text: sessionMessagesTable.contentText, }) .from(sessionMessagesTable) .where( @@ -494,6 +1131,7 @@ export async function readPublicThreadRunFinalOutput(input: { eq(sessionMessagesTable.sessionId, input.sessionId), eq(sessionMessagesTable.sessionRunId, input.runId), eq(sessionMessagesTable.role, "assistant"), + eq(sessionMessagesTable.projectionFormat, "materialized"), ), ) .orderBy(desc(sessionMessagesTable.seq)) @@ -503,8 +1141,7 @@ export async function readPublicThreadRunFinalOutput(input: { if (message === null) { return null; } - - const sanitizedOutput = sanitizePublicOutput(message.content); + const sanitizedOutput = sanitizePublicOutput(message.content_text); return { text: sanitizedOutput.text, @@ -553,21 +1190,62 @@ function enqueueSseText(controller: ReadableStreamDefaultController, function enqueueThreadEvents(input: { controller: ReadableStreamDefaultController; - emittedEventIds: Set; events: PublicThreadEventLogEntry[]; }): boolean { - let enqueued = false; - for (const event of input.events) { - if (input.emittedEventIds.has(event.id)) { - continue; + enqueueSseText(input.controller, encodeSseThreadEvent(event)); + } + + return input.events.length > 0; +} + +async function enqueueLiveThreadRows(input: { + controller: ReadableStreamDefaultController; + database: D1Database; + projector: PublicLiveEventRowProjector; + rows: readonly PublicThreadEventProcessRow[]; + sessionId: SessionId; +}): Promise { + let enqueued = false; + let pendingRows: PublicThreadEventProcessRow[] = []; + const flushPendingRows = () => { + if (pendingRows.length === 0) { + return; } + enqueued = + enqueueThreadEvents({ + controller: input.controller, + events: toPublicThreadEventLogEntries(pendingRows), + }) || enqueued; + pendingRows = []; + }; - input.emittedEventIds.add(event.id); - enqueueSseText(input.controller, encodeSseThreadEvent(event)); - enqueued = true; + for (const row of input.rows) { + const reconciled = new Map(); + const requests = input.projector.getReconciliationRequests(row); + if (requests.length > 0) { + flushPendingRows(); + } + for (const request of requests) { + const result = await reconcilePublicMessage({ + database: input.database, + emit: (chunk) => { + enqueued = + enqueueThreadEvents({ + controller: input.controller, + events: toPublicThreadEventLogEntries([{ ...chunk.row, content_text: chunk.text }]), + }) || enqueued; + }, + request, + sessionId: input.sessionId, + }); + reconciled.set(request.key, result); + } + pendingRows.push(...input.projector.project([row], reconciled)); } + flushPendingRows(); + return enqueued; } @@ -607,15 +1285,17 @@ export async function createPublicThreadEventStream( wakeup.close(); throw error; } - const emittedEventIds = new Set(); const state = { cancelled: false, }; let lastSeenSeq = initialWindow.latestSeq ?? 0; const liveProjector = new PublicLiveEventRowProjector(); - const initialEvents = toPublicThreadEventLogEntries( - liveProjector.project(initialWindow.rows), - ).slice(-limit); + const initialEvents = liveProjector.seedFromCanonicalEvents( + initialWindow.rows, + initialWindow.events, + initialWindow.leftIncompleteStreamKeys, + initialWindow.trustUnknownMessageDeltas, + ); return new ReadableStream({ async start(controller) { @@ -625,7 +1305,6 @@ export async function createPublicThreadEventStream( enqueueSseText(controller, encodeSseComment("connected")); enqueueThreadEvents({ controller, - emittedEventIds, events: initialEvents, }); @@ -651,11 +1330,13 @@ export async function createPublicThreadEventStream( lastSeenSeq = rows[rows.length - 1]?.seq ?? lastSeenSeq; enqueuedEvents = - enqueueThreadEvents({ + (await enqueueLiveThreadRows({ controller, - emittedEventIds, - events: toPublicThreadEventLogEntries(liveProjector.project(rows)), - }) || enqueuedEvents; + database: request.database, + projector: liveProjector, + rows, + sessionId, + })) || enqueuedEvents; if (rows.length < THREAD_EVENT_ROW_PAGE_SIZE) { break; diff --git a/apps/api/src/modules/public-api/public-thread-store.ts b/apps/api/src/modules/public-api/public-thread-store.ts index 93549cf2..a464f19c 100644 --- a/apps/api/src/modules/public-api/public-thread-store.ts +++ b/apps/api/src/modules/public-api/public-thread-store.ts @@ -1,19 +1,14 @@ import type { SessionSummary } from "@mosoo/contracts/session"; -import { - sessionEventsTable, - sessionMessagesTable, - sessionRunsTable, - sessionsTable, -} from "@mosoo/db"; +import { sessionRunsTable, sessionsTable } from "@mosoo/db"; import { parsePlatformId } from "@mosoo/id"; -import type { AccountId, AgentId, FileId, PublicThreadId, SessionId } from "@mosoo/id"; +import type { AccountId, AgentId, PublicThreadId, SessionId } from "@mosoo/id"; import { and, eq, sql } from "drizzle-orm"; import type { SQL } from "drizzle-orm"; import type { ApiBindings } from "../../platform/cloudflare/worker-types"; import { getAppDatabase } from "../../platform/db/drizzle"; import { currentTimestampMs, toIsoString } from "../../time"; -import { fileStore } from "../files/application/file-store"; +import { deleteSessionCascade } from "../sessions/application/session-cleanup.service"; import { buildSessionSummaryFromJoinedRow, sessionSummaryWithLastRunColumns, @@ -46,35 +41,9 @@ export interface ThreadSnapshot { export async function cleanupFailedThreadCreation(input: { bindings: ApiBindings; - fileIds: FileId[]; sessionId: SessionId; }): Promise { - if (input.fileIds.length > 0) { - await fileStore.deleteScope(input.bindings, { - id: input.sessionId, - kind: "session", - }); - } - - await getAppDatabase(input.bindings.DB) - .delete(sessionEventsTable) - .where(eq(sessionEventsTable.sessionId, input.sessionId)) - .run(); - - await getAppDatabase(input.bindings.DB) - .delete(sessionMessagesTable) - .where(eq(sessionMessagesTable.sessionId, input.sessionId)) - .run(); - - await getAppDatabase(input.bindings.DB) - .delete(sessionRunsTable) - .where(eq(sessionRunsTable.sessionId, input.sessionId)) - .run(); - - await getAppDatabase(input.bindings.DB) - .delete(sessionsTable) - .where(eq(sessionsTable.id, input.sessionId)) - .run(); + await deleteSessionCascade(input.bindings, input.sessionId); } export async function getThreadSnapshot( diff --git a/apps/api/src/modules/runtime/application/execution-plane/execution-plane-adapter.ts b/apps/api/src/modules/runtime/application/execution-plane/execution-plane-adapter.ts index 04a024ad..772b1247 100644 --- a/apps/api/src/modules/runtime/application/execution-plane/execution-plane-adapter.ts +++ b/apps/api/src/modules/runtime/application/execution-plane/execution-plane-adapter.ts @@ -1,4 +1,3 @@ -import type { RunError, SessionRunStatus } from "@mosoo/contracts/session-run"; import type { AgentId, DriverInstanceId, @@ -16,6 +15,7 @@ import type { RuntimeTimingSnapshot } from "../session-runs/session-runtime-timi import type { DriverBootPayloadPreparedHandler } from "./driver-boot-payload-prepared"; export interface RuntimeExecutionPlaneRunLease { + driverGeneration: number; driverInstanceId: DriverInstanceId; timing: RuntimeTimingSnapshot; readiness(): Promise; @@ -39,6 +39,7 @@ export interface PrepareRuntimeRunInput { export interface DispatchRuntimeTurnInput { attachmentIds: FileId[]; + driverGeneration: number; driverInstanceId: DriverInstanceId; prompt: string; sessionRunId: SessionRunId; @@ -47,13 +48,9 @@ export interface DispatchRuntimeTurnInput { export interface StopRuntimeSubjectDriversInput { operationId?: RuntimeOperationId; runtimeSubjectId: SandboxId; - preserveSessionLifecycle?: boolean; + sandboxIncarnation?: number; reason: string; targets?: readonly RuntimeSubjectOperationSessionTarget[]; - terminalRun?: { - error?: RunError | null; - status: Extract; - }; } export interface RuntimeSubjectOperationInput { @@ -61,10 +58,6 @@ export interface RuntimeSubjectOperationInput { runtimeSubjectId: SandboxId; reason: string; targets: readonly RuntimeSubjectOperationSessionTarget[]; - terminalRun: { - error?: RunError | null; - status: Extract; - }; } export interface RuntimeSubjectOperationSessionTarget { diff --git a/apps/api/src/modules/runtime/application/owner-debug-terminal.service.ts b/apps/api/src/modules/runtime/application/owner-debug-terminal.service.ts index 05cc9160..98c2c641 100644 --- a/apps/api/src/modules/runtime/application/owner-debug-terminal.service.ts +++ b/apps/api/src/modules/runtime/application/owner-debug-terminal.service.ts @@ -2,6 +2,7 @@ import type { PtyOptions } from "@cloudflare/sandbox"; import { parsePlatformId } from "@mosoo/id"; import type { AccountId, AgentId } from "@mosoo/id"; +import { withDisposedRpcResource } from "../../../platform/cloudflare/rpc-disposal"; import type { ApiBindings } from "../../../platform/cloudflare/worker-types"; import { API_ERROR_CODE, ApiError, createApiError } from "../../../platform/errors"; import { ensureAgentOwner } from "../../agents/application/agent-access.service"; @@ -88,9 +89,11 @@ export async function connectOwnerDebugTerminalWebSocket( networkConstraints: { allowedHosts: [], networkPolicy: "full" }, }); - return connectPreparedSandboxTerminal(activation.subject, { - options, - request: input.request, - terminalSessionId: target.terminalSessionId, - }); + return withDisposedRpcResource(activation.subject, (subject) => + connectPreparedSandboxTerminal(subject, { + options, + request: input.request, + terminalSessionId: target.terminalSessionId, + }), + ); } diff --git a/apps/api/src/modules/runtime/application/runtime-diagnostic-events.ts b/apps/api/src/modules/runtime/application/runtime-diagnostic-events.ts index 0451dde4..6afb350f 100644 --- a/apps/api/src/modules/runtime/application/runtime-diagnostic-events.ts +++ b/apps/api/src/modules/runtime/application/runtime-diagnostic-events.ts @@ -30,6 +30,7 @@ export interface RuntimeDiagnosticEventInput< TName extends RuntimeDiagnosticEventName = RuntimeDiagnosticEventName, > { eventName: TName; + sourceEventId?: string; value: RuntimeDiagnosticEventValue; } @@ -76,11 +77,18 @@ export async function appendRuntimeDiagnosticEvent; }, ): Promise { return appendRuntimeDiagnosticEvents(bindings, { - events: [{ eventName: input.eventName, value: input.value }], + events: [ + { + eventName: input.eventName, + ...(input.sourceEventId === undefined ? {} : { sourceEventId: input.sourceEventId }), + value: input.value, + }, + ], sessionId: input.sessionId, }); } @@ -104,6 +112,7 @@ export async function appendRuntimeDiagnosticEvents( createRuntimeDiagnosticSessionEvent({ eventName: event.eventName, sessionId: input.sessionId, + ...(event.sourceEventId === undefined ? {} : { sourceEventId: event.sourceEventId }), value: event.value, }), ), @@ -172,6 +181,7 @@ function createRuntimeDiagnosticSessionEvent([ + "recreateSandbox", + "resetAgentState", + "restartDriver", +]); + +function isRecord(value: unknown): value is Record { + return typeof value === "object" && value !== null && !Array.isArray(value); +} + +function canonicalError(input: { + readonly operationId: RuntimeOperationId; + readonly sessionId: SessionId; + readonly status: RuntimeOperationEventStatus; +}): Error { + return new Error( + `Runtime operation ${input.status} source runtime-operation:${input.operationId}:${input.sessionId}:${input.status} is not canonical.`, + ); +} + +export function readRuntimeOperationEventIdentity( + event: RuntimeEventEnvelope, + input: { + readonly agentId: AgentId; + readonly operationId: RuntimeOperationId; + readonly sessionId: SessionId; + readonly status: RuntimeOperationEventStatus; + }, +): RuntimeOperationEventIdentity { + const sourceEventId = `runtime-operation:${input.operationId}:${input.sessionId}:${input.status}`; + if ( + event.actor !== "api" || + event.delivery !== "lossless" || + event.kind !== "agent.task.updated" || + event.origin !== "api" || + event.sessionId !== input.sessionId || + event.sourceEventId !== sourceEventId || + event.visibility !== "participant" || + event.context !== undefined || + event.correlationId !== undefined || + event.driverInstanceId !== undefined || + event.native !== undefined || + event.receivedAt !== undefined || + event.runId !== undefined || + event.runtimeId !== undefined || + event.seq !== undefined || + event.traceId !== undefined || + !isRecord(event.payload) + ) { + throw canonicalError(input); + } + + const payload = event.payload; + const timingField = input.status === "ready" ? "readyAt" : "startedAt"; + const allowedKeys = new Set([ + "agentId", + "deploymentVersionId", + "deploymentVersionNumber", + "operation", + "operationId", + "status", + timingField, + ]); + if (Object.keys(payload).some((key) => !allowedKeys.has(key))) { + throw canonicalError(input); + } + + let agentId: AgentId; + let deploymentVersionId: AgentDeploymentVersionId | null = null; + let operationId: RuntimeOperationId; + try { + agentId = parsePlatformId(payload["agentId"], "Runtime operation Agent ID"); + operationId = parsePlatformId( + payload["operationId"], + "Runtime operation ID", + ); + if (payload["deploymentVersionId"] !== undefined) { + deploymentVersionId = parsePlatformId( + payload["deploymentVersionId"], + "Runtime operation deployment version ID", + ); + } + } catch { + throw canonicalError(input); + } + + const operation = payload["operation"]; + const observedAt = payload[timingField]; + const hasDeploymentVersion = payload["deploymentVersionId"] !== undefined; + const deploymentVersionNumber = payload["deploymentVersionNumber"]; + if ( + agentId !== input.agentId || + operationId !== input.operationId || + typeof operation !== "string" || + !runtimeStateOperationNames.has(operation as RuntimeStateOperationName) || + payload["status"] !== input.status || + typeof observedAt !== "string" || + observedAt !== event.occurredAt || + !Number.isFinite(Date.parse(observedAt)) || + hasDeploymentVersion !== (deploymentVersionNumber !== undefined) || + (hasDeploymentVersion && + (!Number.isSafeInteger(deploymentVersionNumber) || (deploymentVersionNumber as number) <= 0)) + ) { + throw canonicalError(input); + } + + return { + agentId, + deploymentVersionId, + deploymentVersionNumber: hasDeploymentVersion ? (deploymentVersionNumber as number) : null, + operation: operation as RuntimeStateOperationName, + }; +} + +export function createRuntimeOperationEventAuthorityJson(input: { + readonly agentId: AgentId; + readonly event: RuntimeEventEnvelope; + readonly operationId: RuntimeOperationId; + readonly sessionId: SessionId; + readonly status: RuntimeOperationEventStatus; +}): string { + readRuntimeOperationEventIdentity(input.event, input); + return stringifyRuntimeEventSemanticValue(input.event); +} + +export async function readRuntimeOperationEventAuthority(input: { + readonly agentId: AgentId; + readonly eventId: string; + readonly eventJson: string | null; + readonly eventType: string; + readonly occurredAt: number; + readonly operationId: RuntimeOperationId; + readonly rowAgentId: AgentId; + readonly semanticHash: string | null; + readonly sessionId: SessionId; + readonly source: string; + readonly sourceEventId: string; + readonly status: RuntimeOperationEventStatus; + readonly visibility: string; +}): Promise<{ + readonly agentId: AgentId; + readonly deploymentVersionId: AgentDeploymentVersionId | null; + readonly deploymentVersionNumber: number | null; + readonly event: RuntimeEventEnvelope; + readonly occurredAt: number; + readonly operation: RuntimeStateOperationName; +}> { + const sourceEventId = `runtime-operation:${input.operationId}:${input.sessionId}:${input.status}`; + const event = await readSessionRuntimeEventSemanticAuthority({ + eventJson: input.eventJson, + invalidMessage: `Runtime operation ${input.status} source ${sourceEventId} is not canonical.`, + missingMessage: `Runtime operation ${input.status} source ${sourceEventId} has no semantic authority.`, + semanticHash: input.semanticHash, + }); + const identity = readRuntimeOperationEventIdentity(event, input); + const projection = createSessionRuntimeEventProjection(event); + + if ( + input.rowAgentId !== input.agentId || + event.id !== input.eventId || + event.kind !== input.eventType || + Date.parse(event.occurredAt) !== input.occurredAt || + event.sourceEventId !== input.sourceEventId || + input.sourceEventId !== sourceEventId || + projection.eventType !== input.eventType || + projection.runId !== null || + projection.source !== input.source || + projection.source !== "api" || + projection.traceId !== null || + projection.visibility !== input.visibility || + projection.visibility !== "all_consumers" + ) { + throw canonicalError(input); + } + + return { ...identity, event, occurredAt: input.occurredAt }; +} diff --git a/apps/api/src/modules/runtime/application/runtime-state-operation-events.ts b/apps/api/src/modules/runtime/application/runtime-state-operation-events.ts index dff85f1e..ec6b44a1 100644 --- a/apps/api/src/modules/runtime/application/runtime-state-operation-events.ts +++ b/apps/api/src/modules/runtime/application/runtime-state-operation-events.ts @@ -1,6 +1,7 @@ import type { RuntimeStateOperationName } from "@mosoo/contracts/agent"; -import type { AgentDeploymentVersionId, AgentId } from "@mosoo/id"; +import type { AgentDeploymentVersionId, AgentId, RuntimeOperationId, SessionId } from "@mosoo/id"; +import { createSessionRuntimeEvent } from "../../sessions/application/session-event-write.service"; import type { RuntimeOperationTargetVersion } from "./runtime-state-operation-version"; export interface RuntimeOperationEvent { @@ -12,6 +13,36 @@ export interface RuntimeOperationEvent { status: "ready" | "updating"; } +export function createRuntimeOperationSessionEvent(input: { + readonly event: RuntimeOperationEvent; + readonly operationId: RuntimeOperationId; + readonly sessionId: SessionId; +}) { + const occurredAtMs = Date.parse(input.event.observedAt); + + return createSessionRuntimeEvent({ + kind: "agent.task.updated", + ...(Number.isFinite(occurredAtMs) ? { occurredAtMs } : {}), + payload: { + agentId: input.event.agentId, + ...(input.event.deploymentVersionId + ? { + deploymentVersionId: input.event.deploymentVersionId, + deploymentVersionNumber: input.event.deploymentVersionNumber, + } + : {}), + operation: input.event.operation, + operationId: input.operationId, + ...(input.event.status === "ready" + ? { readyAt: input.event.observedAt } + : { startedAt: input.event.observedAt }), + status: input.event.status, + }, + sessionId: input.sessionId, + sourceEventId: `runtime-operation:${input.operationId}:${input.sessionId}:${input.event.status}`, + }); +} + export function buildRuntimeStateOperationEvents(input: { agentId: AgentId; operation: RuntimeStateOperationName; diff --git a/apps/api/src/modules/runtime/application/runtime-state-operation-execution.ts b/apps/api/src/modules/runtime/application/runtime-state-operation-execution.ts index c367874b..d3efd33c 100644 --- a/apps/api/src/modules/runtime/application/runtime-state-operation-execution.ts +++ b/apps/api/src/modules/runtime/application/runtime-state-operation-execution.ts @@ -3,7 +3,6 @@ import type { RuntimeOperationId } from "@mosoo/id"; import type { ApiBindings } from "../../../platform/cloudflare/worker-types"; import type { RuntimeExecutionPlaneAdapter } from "./execution-plane/execution-plane-adapter"; -import { RUNTIME_STATE_OPERATION_INTERRUPTED_ERROR } from "./runtime-state-operation-errors"; import type { RuntimeOperationSubject } from "./runtime-state-operation-subjects"; export type RuntimeStateOperationExecutionPlane = Pick< @@ -19,10 +18,6 @@ function operationInput(input: RuntimeOperationSubject & { operationId: RuntimeO runtimeSubjectId: input.runtimeSubjectId, reason: "agent.runtime_state_operation", targets: input.targets, - terminalRun: { - error: RUNTIME_STATE_OPERATION_INTERRUPTED_ERROR, - status: "cancelled" as const, - }, }; } @@ -36,10 +31,7 @@ async function executeRuntimeStateOperationSubject( ): Promise { switch (input.operation) { case "restartDriver": { - await executionPlane.stopSubjectDrivers(bindings, { - ...operationInput(input), - preserveSessionLifecycle: true, - }); + await executionPlane.stopSubjectDrivers(bindings, operationInput(input)); return; } case "recreateSandbox": { @@ -70,7 +62,7 @@ export async function executeRuntimeStateOperationSubjects( index < input.subjects.length; index += RUNTIME_OPERATION_SUBJECT_CONCURRENCY ) { - await Promise.all( + const outcomes = await Promise.allSettled( input.subjects.slice(index, index + RUNTIME_OPERATION_SUBJECT_CONCURRENCY).map((subject) => executeRuntimeStateOperationSubject(input.executionPlane, bindings, { operationId: input.operationId, @@ -80,5 +72,11 @@ export async function executeRuntimeStateOperationSubjects( }), ), ); + const failure = outcomes.find( + (outcome): outcome is PromiseRejectedResult => outcome.status === "rejected", + ); + if (failure !== undefined) { + throw failure.reason; + } } } diff --git a/apps/api/src/modules/runtime/application/runtime-state-operation-phases.ts b/apps/api/src/modules/runtime/application/runtime-state-operation-phases.ts index f6aa5ffb..bf6be9c2 100644 --- a/apps/api/src/modules/runtime/application/runtime-state-operation-phases.ts +++ b/apps/api/src/modules/runtime/application/runtime-state-operation-phases.ts @@ -4,16 +4,15 @@ import type { AgentId, RuntimeOperationId } from "@mosoo/id"; import type { ApiBindings } from "../../../platform/cloudflare/worker-types"; import { buildRuntimeStateOperationEvents } from "./runtime-state-operation-events"; -import type { RuntimeOperationEvent } from "./runtime-state-operation-events"; import { - broadcastRuntimeOperationEvent, - writeRuntimeOperationInterruptedSnapshots, + publishRuntimeOperationEvent, writeRuntimeOperationTimedOutSnapshots, } from "./runtime-state-operation-target-events"; import { restoreRuntimeOperationFailedTargets } from "./runtime-state-operation-target-recovery"; import { + claimRuntimeOperationTargets, expireStaleRuntimeOperationTargets, - transitionRuntimeTargetSessionStatus, + listRuntimeOperationTargets, } from "./runtime-state-operation-target-store"; import type { RuntimeSessionTarget, @@ -46,31 +45,6 @@ function listAdmissibleOperationTargets( ); } -async function broadcastOperationPhase( - bindings: ApiBindings, - input: { - readonly event: RuntimeOperationEvent; - readonly expectedStatus?: RuntimeSessionTarget["sessionStatus"]; - readonly operationId: RuntimeOperationId; - readonly status: RuntimeSessionTarget["sessionStatus"]; - readonly targets: RuntimeSessionTarget[]; - }, -): Promise { - const transitions = await transitionRuntimeTargetSessionStatus(bindings.DB, { - ...(input.expectedStatus ? { expectedStatus: input.expectedStatus } : {}), - expectedOperationId: input.expectedStatus === undefined ? null : input.operationId, - operationId: input.operationId, - status: input.status, - targets: input.targets, - }); - await broadcastRuntimeOperationEvent(bindings, { - event: input.event, - operationId: input.operationId, - targets: listCurrentTargets(transitions), - }); - return transitions; -} - export async function startRuntimeStateOperationPhase( bindings: ApiBindings, input: { @@ -89,11 +63,51 @@ export async function startRuntimeStateOperationPhase( startedAt, targetVersion: input.targetVersion, }); - const reschedulingTargets = await broadcastOperationPhase(bindings, { + const admissibleTargets = listAdmissibleOperationTargets(input.targets); + let reschedulingTargets: RuntimeSessionTargetTransition[]; + + try { + reschedulingTargets = await claimRuntimeOperationTargets(bindings.DB, { + event: updatingEvent, + operationId, + targets: admissibleTargets, + }); + } catch (error) { + const claimedTargets = await listRuntimeOperationTargets(bindings.DB, { + operationId, + targets: admissibleTargets, + }); + const partialTransitions = claimedTargets.map((current) => ({ current })); + const [, readyEvent] = buildRuntimeStateOperationEvents({ + agentId: input.agentId, + operation: input.operation, + readyAt: new Date().toISOString(), + startedAt, + targetVersion: input.targetVersion, + }); + + try { + await restoreRuntimeOperationFailedTargets(bindings, { + operationId, + readyEvent, + targets: partialTransitions, + terminalTimestampMs: Date.parse(startedAt), + }); + } catch (recoveryError) { + throw new AggregateError( + [error, recoveryError], + "Runtime operation admission and partial-target recovery both failed.", + { cause: recoveryError }, + ); + } + throw new Error("Runtime operation admission failed after partial-target recovery.", { + cause: error, + }); + } + await publishRuntimeOperationEvent(bindings, { event: updatingEvent, operationId, - status: "RESCHEDULING", - targets: listAdmissibleOperationTargets(input.targets), + targets: listCurrentTargets(reschedulingTargets), }); return { @@ -104,31 +118,7 @@ export async function startRuntimeStateOperationPhase( }; } -export async function failRuntimeStateOperationPhase( - bindings: ApiBindings, - input: { - readonly agentId: AgentId; - readonly operation: RuntimeStateOperationName; - readonly phase: RuntimeStateOperationPhase; - }, -): Promise { - const failedAt = new Date().toISOString(); - const [, failureReadyEvent] = buildRuntimeStateOperationEvents({ - agentId: input.agentId, - operation: input.operation, - readyAt: failedAt, - startedAt: input.phase.startedAt, - targetVersion: input.phase.targetVersion, - }); - - await restoreRuntimeOperationFailedTargets(bindings, { - operationId: input.phase.operationId, - readyEvent: failureReadyEvent, - targets: input.phase.reschedulingTargets, - }); -} - -export async function completeRuntimeStateOperationPhase( +async function finishRuntimeStateOperationPhase( bindings: ApiBindings, input: { readonly agentId: AgentId; @@ -144,28 +134,26 @@ export async function completeRuntimeStateOperationPhase( startedAt: input.phase.startedAt, targetVersion: input.phase.targetVersion, }); - + const phaseTargets = listRuntimeStateOperationPhaseTargets(input.phase); const timedOutTargets = await expireStaleRuntimeOperationTargets(bindings.DB, { operationId: input.phase.operationId, - targets: listRuntimeStateOperationPhaseTargets(input.phase), + targets: phaseTargets, }); await writeRuntimeOperationTimedOutSnapshots(bindings, { operationId: input.phase.operationId, targets: timedOutTargets, }); - const readyTransitions = await transitionRuntimeTargetSessionStatus(bindings.DB, { - expectedOperationId: input.phase.operationId, - expectedStatus: "RESCHEDULING", - status: "IDLE", - targets: listRuntimeStateOperationPhaseTargets(input.phase), - }); - await writeRuntimeOperationInterruptedSnapshots(bindings, { - operationId: input.phase.operationId, - targets: readyTransitions.map((transition) => transition.previous), - }); - await broadcastRuntimeOperationEvent(bindings, { - event: readyEvent, + const timedOutIds = new Set(timedOutTargets.map((target) => target.sessionId)); + + await restoreRuntimeOperationFailedTargets(bindings, { operationId: input.phase.operationId, - targets: listCurrentTargets(readyTransitions), + readyEvent, + targets: input.phase.reschedulingTargets.filter( + (target) => !timedOutIds.has(target.current.sessionId), + ), + terminalTimestampMs: Date.parse(input.phase.startedAt), }); } + +export const failRuntimeStateOperationPhase = finishRuntimeStateOperationPhase; +export const completeRuntimeStateOperationPhase = finishRuntimeStateOperationPhase; diff --git a/apps/api/src/modules/runtime/application/runtime-state-operation-subjects.ts b/apps/api/src/modules/runtime/application/runtime-state-operation-subjects.ts index 3cd2ec2e..a07c24b4 100644 --- a/apps/api/src/modules/runtime/application/runtime-state-operation-subjects.ts +++ b/apps/api/src/modules/runtime/application/runtime-state-operation-subjects.ts @@ -71,9 +71,11 @@ async function resolveLeaseScopedRuntimeOperationScope( runtimeSubjectId: sandboxesTable.id, sandboxId: sandboxSessionsTable.sandboxId, sessionId: sessionsTable.id, + sessionRuntimeEventSeqCursor: sessionsTable.runtimeEventSeqCursor, sessionStatusOperationId: sessionsTable.statusOperationId, sessionStatusSeq: sessionsTable.statusSeq, sessionStatus: sql`${sessionsTable.status}`, + sessionUpdatedAt: sessionsTable.updatedAt, }) .from(sessionsTable) .innerJoin(sandboxSessionsTable, eq(sandboxSessionsTable.sessionId, sessionsTable.id)) @@ -97,9 +99,11 @@ async function resolveLeaseScopedRuntimeOperationScope( lastRunId: row.lastRunId, sandboxId: row.sandboxId, sessionId: row.sessionId, + sessionRuntimeEventSeqCursor: row.sessionRuntimeEventSeqCursor, sessionStatusOperationId: row.sessionStatusOperationId, sessionStatusSeq: row.sessionStatusSeq, sessionStatus: row.sessionStatus, + sessionUpdatedAt: row.sessionUpdatedAt, })), }; } diff --git a/apps/api/src/modules/runtime/application/runtime-state-operation-target-events.ts b/apps/api/src/modules/runtime/application/runtime-state-operation-target-events.ts index d12b6920..cc076764 100644 --- a/apps/api/src/modules/runtime/application/runtime-state-operation-target-events.ts +++ b/apps/api/src/modules/runtime/application/runtime-state-operation-target-events.ts @@ -1,19 +1,20 @@ -import type { RunError, SessionRunSummary } from "@mosoo/contracts/session-run"; +import type { SessionRunSummary } from "@mosoo/contracts/session-run"; +import { sessionEventsTable } from "@mosoo/db"; import { createPlatformId } from "@mosoo/id"; import type { AgentId, RuntimeOperationId, SandboxId, SessionId, SessionRunId } from "@mosoo/id"; import { RUNTIME_DIAGNOSTIC_EVENT } from "@mosoo/runtime-events"; +import { and, inArray } from "drizzle-orm"; import type { ApiBindings } from "../../../platform/cloudflare/worker-types"; +import { getAppDatabase } from "../../../platform/db/drizzle"; import { isTruthy } from "../../../shared/truthiness"; -import { - appendOneSessionRuntimeEventPerSession, - createSessionRuntimeEvent, -} from "../../sessions/application/session-event-write.service"; +import { publishPersistedSessionRuntimeEvents } from "../../sessions/application/session-event-write.service"; +import { RESCHEDULING_RECONNECT_WINDOW_MS } from "../../sessions/domain/session-lifecycle"; +import { readTerminalEventSemanticAuthority } from "../../sessions/domain/session-terminal-event-authority"; +import type { TerminalEventSemanticAuthority } from "../../sessions/domain/session-terminal-event-authority"; +import { adoptTerminalRunProjection } from "../infrastructure/driver-instance/completed-run-commit.repository"; import { listLiveDriverInstanceRefsForSandboxSessions } from "../infrastructure/driver-instance/live-driver-instance.repository"; -import { - cancelActiveSessionRunsForRuntimeOperation, - getSessionRunSummariesByIds, -} from "../infrastructure/session-runs/session-run-store.repository"; +import { getSessionRunSummariesByIds } from "../infrastructure/session-runs/session-run-store.repository"; import { appendOneRuntimeDiagnosticEventPerSession, toRuntimeDiagnosticBaseValue, @@ -22,72 +23,180 @@ import { RUNTIME_STATE_OPERATION_INTERRUPTED_ERROR, RUNTIME_STATE_OPERATION_TIMEOUT_ERROR, } from "./runtime-state-operation-errors"; +import { createRuntimeOperationSessionEvent } from "./runtime-state-operation-events"; import type { RuntimeOperationEvent } from "./runtime-state-operation-events"; +import { + adoptRuntimeOperationReadyReceipt, + commitSessionLifecycleEventProjection, + listRuntimeOperationTargets, + transitionRuntimeTargetSessionStatus, +} from "./runtime-state-operation-target-store"; import type { RuntimeSessionTarget } from "./runtime-state-operation-target-store"; import type { RuntimeOperationTargetVersion } from "./runtime-state-operation-version"; -import { - createCancelledSessionRunRuntimeEvent, - createSessionLifecycleTerminatedEvent, -} from "./session-runs/session-run-view-events.service"; +import { recordCanonicalSessionRunTerminal } from "./session-runs/session-run-terminal-failure.service"; +import { createSessionLifecycleTerminatedEvent } from "./session-runs/session-run-view-events.service"; function listTargetRunIds(targets: readonly RuntimeSessionTarget[]): SessionRunId[] { - const runIds: SessionRunId[] = []; + return [...new Set(targets.flatMap((target) => (target.lastRunId ? [target.lastRunId] : [])))]; +} - for (const target of targets) { - if (target.sessionStatus !== "IDLE" && isTruthy(target.lastRunId)) { - runIds.push(target.lastRunId); - } +function runtimeOperationSessionEventId(input: { + readonly kind: "interrupted" | "timed_out"; + readonly operationId: RuntimeOperationId; + readonly sessionId: SessionId; +}): string { + return `runtime-operation:${input.operationId}:${input.sessionId}:${input.kind}`; +} + +async function getTargetRuns( + database: D1Database, + targets: readonly RuntimeSessionTarget[], +): Promise> { + const runIds = listTargetRunIds(targets); + + if (runIds.length === 0) { + return new Map(); } - return runIds; + return getSessionRunSummariesByIds(database, runIds); } -function toObservedAtMs(value: string): number | undefined { - const observedAtMs = Date.parse(value); - - return Number.isFinite(observedAtMs) ? observedAtMs : undefined; +function isTerminalRun(run: SessionRunSummary): boolean { + return ["cancelled", "completed", "expired", "failed"].includes(run.status); } -function runtimeOperationRunEventId(input: { - readonly kind: "interrupted" | "timed_out"; - readonly operationId: RuntimeOperationId; - readonly runId: SessionRunId; -}): string { - return `runtime-operation:${input.operationId}:${input.runId}:${input.kind}`; +async function throwFirstFailure( + outcomes: readonly PromiseSettledResult[], +): Promise { + const failed = outcomes.find( + (outcome): outcome is PromiseRejectedResult => outcome.status === "rejected", + ); + + if (failed !== undefined) { + throw failed.reason; + } } -function runtimeOperationSessionEventId(input: { - readonly kind: "timed_out"; - readonly operationId: RuntimeOperationId; - readonly sessionId: SessionId; -}): string { - return `runtime-operation:${input.operationId}:${input.sessionId}:${input.kind}`; +async function adoptCanonicalTerminalRuns( + database: D1Database, + targets: readonly { + readonly run: SessionRunSummary; + readonly sessionId: SessionId; + }[], +): Promise> { + const outcomes = await Promise.allSettled( + targets.map(async ({ run, sessionId }) => { + const outcome = await adoptTerminalRunProjection(database, { + runId: run.id, + sessionId, + }); + if (outcome.kind === "missing" || outcome.kind === "stale") { + throw new Error( + `Runtime operation found an incomplete terminal projection for run ${run.id}.`, + ); + } + }), + ); + await throwFirstFailure(outcomes); + + if (targets.length === 0) { + return new Map(); + } + const rows = await getAppDatabase(database) + .select({ + eventType: sessionEventsTable.eventType, + runId: sessionEventsTable.runId, + semanticHash: sessionEventsTable.semanticHash, + sessionId: sessionEventsTable.sessionId, + sourceEventId: sessionEventsTable.sourceEventId, + streamId: sessionEventsTable.streamId, + terminalEventJson: sessionEventsTable.terminalEventJson, + }) + .from(sessionEventsTable) + .where( + and( + inArray( + sessionEventsTable.runId, + targets.map(({ run }) => run.id), + ), + inArray(sessionEventsTable.eventType, ["run.cancelled", "run.completed", "run.failed"]), + ), + ) + .all(); + const rowsByRunId = Map.groupBy( + rows.filter((row) => row.runId !== null), + (row) => row.runId!, + ); + const authorities = new Map(); + for (const { run, sessionId } of targets) { + const receipts = rowsByRunId.get(run.id) ?? []; + const [receipt] = receipts; + if (receipts.length !== 1 || receipt === undefined || receipt.semanticHash === null) { + throw new Error(`Runtime operation found no v3 lifecycle authority for run ${run.id}.`); + } + authorities.set( + run.id, + await readTerminalEventSemanticAuthority({ + eventJson: receipt.terminalEventJson, + eventType: receipt.eventType, + runId: run.id, + semanticHash: receipt.semanticHash, + sessionId, + sourceEventId: receipt.sourceEventId, + streamId: receipt.streamId, + }), + ); + } + return authorities; } -async function cancelRuntimeOperationTargetRuns( +export async function commitRuntimeOperationReadySnapshots( bindings: ApiBindings, input: { - readonly error: RunError; + readonly event: RuntimeOperationEvent; readonly operationId: RuntimeOperationId; readonly targets: readonly RuntimeSessionTarget[]; }, -): Promise> { - const runIds = listTargetRunIds(input.targets); - - if (runIds.length === 0) { - return new Map(); +): Promise { + if (input.event.status !== "ready") { + throw new Error("Runtime operation release requires one ready event."); + } + const timestampMs = Date.parse(input.event.observedAt); + if (!Number.isFinite(timestampMs)) { + throw new Error("Runtime operation ready time must be a valid ISO timestamp."); } - const updated = await cancelActiveSessionRunsForRuntimeOperation(bindings.DB, { - error: input.error, - operationId: input.operationId, - runIds, - }); - - return getSessionRunSummariesByIds(bindings.DB, [...updated.runIds]); + const outcomes = await Promise.allSettled( + input.targets.map(async (target) => { + const event = createRuntimeOperationSessionEvent({ + event: input.event, + operationId: input.operationId, + sessionId: target.sessionId, + }); + const outcome = await commitSessionLifecycleEventProjection(bindings.DB, { + event, + runtimeOperation: { operationId: input.operationId, status: "ready" }, + status: "IDLE", + target, + timestampMs, + }); + + return outcome.kind === "stale" ? null : { event, sessionId: target.sessionId }; + }), + ); + await throwFirstFailure(outcomes); + const committed = outcomes.flatMap((outcome) => + outcome.status === "fulfilled" && outcome.value !== null ? [outcome.value] : [], + ); + const deliveries = await Promise.allSettled( + committed.map(({ event, sessionId }) => + publishPersistedSessionRuntimeEvents({ bindings, events: [event], sessionId }), + ), + ); + await throwFirstFailure(deliveries); } -export async function broadcastRuntimeOperationEvent( +export async function publishRuntimeOperationEvent( bindings: ApiBindings, input: { readonly event: RuntimeOperationEvent; @@ -95,84 +204,133 @@ export async function broadcastRuntimeOperationEvent( readonly targets: readonly RuntimeSessionTarget[]; }, ): Promise { - await appendOneSessionRuntimeEventPerSession({ - bindings, - records: input.targets.map((target) => { - const occurredAtMs = toObservedAtMs(input.event.observedAt); - - return { - event: createSessionRuntimeEvent({ - kind: "agent.task.updated", - ...(occurredAtMs === undefined ? {} : { occurredAtMs }), - payload: { - agentId: input.event.agentId, - ...(input.event.deploymentVersionId - ? { - deploymentVersionId: input.event.deploymentVersionId, - deploymentVersionNumber: input.event.deploymentVersionNumber, - } - : {}), - operation: input.event.operation, + const outcomes = await Promise.allSettled( + input.targets.map((target) => + publishPersistedSessionRuntimeEvents({ + bindings, + events: [ + createRuntimeOperationSessionEvent({ + event: input.event, operationId: input.operationId, - ...(input.event.status === "ready" - ? { readyAt: input.event.observedAt } - : { startedAt: input.event.observedAt }), - status: input.event.status, - }, - sessionId: target.sessionId, - }), + sessionId: target.sessionId, + }), + ], sessionId: target.sessionId, - }; - }), - }); + }), + ), + ); + await throwFirstFailure(outcomes); } export async function writeRuntimeOperationInterruptedSnapshots( bindings: ApiBindings, input: { readonly operationId: RuntimeOperationId; + readonly timestampMs: number; readonly targets: readonly RuntimeSessionTarget[]; }, ): Promise { - const runsById = await cancelRuntimeOperationTargetRuns(bindings, { - error: RUNTIME_STATE_OPERATION_INTERRUPTED_ERROR, + const runsById = await getTargetRuns(bindings.DB, input.targets); + const outcomes = await Promise.allSettled( + input.targets.flatMap((target) => { + const run = target.lastRunId === null ? null : (runsById.get(target.lastRunId) ?? null); + + return run === null || isTerminalRun(run) + ? [] + : [ + recordCanonicalSessionRunTerminal(bindings, { + assistantMessage: null, + error: RUNTIME_STATE_OPERATION_INTERRUPTED_ERROR, + expectedSessionOperationId: input.operationId, + lifecycle: "IDLE", + runId: run.id, + sessionId: target.sessionId, + source: "runtime_operation", + status: "cancelled", + timestampMs: input.timestampMs, + }), + ]; + }), + ); + await throwFirstFailure(outcomes); + + const latestRuns = await getTargetRuns(bindings.DB, input.targets); + const terminalRuns = input.targets.flatMap((target) => { + const run = target.lastRunId === null ? null : (latestRuns.get(target.lastRunId) ?? null); + + if (run === null) { + return []; + } + if (!isTerminalRun(run)) { + throw new Error(`Runtime operation did not terminalize active run ${run.id}.`); + } + return [{ run, sessionId: target.sessionId }]; + }); + const terminalAuthorities = await adoptCanonicalTerminalRuns(bindings.DB, terminalRuns); + const terminatedTargets = input.targets.filter((target) => { + const run = target.lastRunId === null ? null : (latestRuns.get(target.lastRunId) ?? null); + return run !== null && terminalAuthorities.get(run.id)?.lifecycle === "TERMINATED"; + }); + const currentTerminatedTargets = await listRuntimeOperationTargets(bindings.DB, { operationId: input.operationId, - targets: input.targets, + targets: terminatedTargets, }); - - const records = input.targets - .map((target) => { - if (!isTruthy(target.lastRunId) || target.sessionStatus === "IDLE") { + const lifecycleOutcomes = await Promise.allSettled( + currentTerminatedTargets.map(async (target) => { + const ready = await adoptRuntimeOperationReadyReceipt(bindings.DB, { + operationId: input.operationId, + target, + }); + if (ready !== "missing") { return null; } - - const run = runsById.get(target.lastRunId); - - if (!run) { - return null; + if (target.sessionUpdatedAt > input.timestampMs) { + throw new Error("Runtime operation target advanced beyond its interruption time."); } - - return { - event: createCancelledSessionRunRuntimeEvent({ - eventId: createPlatformId(), - sourceEventId: runtimeOperationRunEventId({ - kind: "interrupted", - operationId: input.operationId, - runId: target.lastRunId, - }), - run, - runError: RUNTIME_STATE_OPERATION_INTERRUPTED_ERROR, + const event = createSessionLifecycleTerminatedEvent({ + eventId: createPlatformId(), + sourceEventId: runtimeOperationSessionEventId({ + kind: "interrupted", + operationId: input.operationId, sessionId: target.sessionId, }), + lastSeen: new Date(input.timestampMs).toISOString(), + message: RUNTIME_STATE_OPERATION_INTERRUPTED_ERROR.message, + occurredAtMs: input.timestampMs, + reason: RUNTIME_STATE_OPERATION_INTERRUPTED_ERROR.code, sessionId: target.sessionId, - }; - }) - .filter(isTruthy); - - await appendOneSessionRuntimeEventPerSession({ - bindings, - records, + }); + const outcome = await commitSessionLifecycleEventProjection(bindings.DB, { + event, + status: "TERMINATED", + target, + timestampMs: input.timestampMs, + }); + return outcome.kind === "stale" ? null : { event, sessionId: target.sessionId }; + }), + ); + await throwFirstFailure(lifecycleOutcomes); + const lifecycleDeliveries = await Promise.allSettled( + lifecycleOutcomes.flatMap((outcome) => + outcome.status === "fulfilled" && outcome.value !== null + ? [ + publishPersistedSessionRuntimeEvents({ + bindings, + events: [outcome.value.event], + sessionId: outcome.value.sessionId, + }), + ] + : [], + ), + ); + await throwFirstFailure(lifecycleDeliveries); + const unreleased = await listRuntimeOperationTargets(bindings.DB, { + operationId: input.operationId, + targets: terminatedTargets, }); + if (unreleased.length > 0) { + throw new Error("Runtime operation interruption did not preserve terminal Session lifecycle."); + } } export async function appendRuntimeDriverRestartAttemptedEvents( @@ -271,72 +429,186 @@ export async function writeRuntimeOperationTimedOutSnapshots( readonly targets: readonly RuntimeSessionTarget[]; }, ): Promise { - const runsById = await cancelRuntimeOperationTargetRuns(bindings, { - error: RUNTIME_STATE_OPERATION_TIMEOUT_ERROR, - operationId: input.operationId, - targets: input.targets, - }); + await writeRuntimeOperationTimedOutSnapshotsOnce( + bindings, + input, + new Map( + input.targets.map((target) => [ + target.sessionId, + target.sessionUpdatedAt + RESCHEDULING_RECONNECT_WINDOW_MS, + ]), + ), + true, + ); +} - const records = input.targets - .map((target) => { - if (!isTruthy(target.lastRunId) || target.sessionStatus === "IDLE") { - return { - event: createSessionLifecycleTerminatedEvent({ - eventId: createPlatformId(), - sourceEventId: runtimeOperationSessionEventId({ - kind: "timed_out", - operationId: input.operationId, - sessionId: target.sessionId, - }), - lastSeen: new Date().toISOString(), - message: RUNTIME_STATE_OPERATION_TIMEOUT_ERROR.message, - reason: RUNTIME_STATE_OPERATION_TIMEOUT_ERROR.code, - sessionId: target.sessionId, - }), - sessionId: target.sessionId, - }; +async function writeRuntimeOperationTimedOutSnapshotsOnce( + bindings: ApiBindings, + input: { + readonly operationId: RuntimeOperationId; + readonly targets: readonly RuntimeSessionTarget[]; + }, + deadlines: ReadonlyMap, + retryStaleTargets: boolean, +): Promise { + const deadlineFor = (target: RuntimeSessionTarget): number => { + const deadline = deadlines.get(target.sessionId); + if (deadline === undefined) { + throw new Error("Runtime operation timeout lost its stable target deadline."); + } + return deadline; + }; + const runsById = await getTargetRuns(bindings.DB, input.targets); + const activeTargets = input.targets.filter((target) => { + const run = target.lastRunId === null ? null : (runsById.get(target.lastRunId) ?? null); + return run !== null && !isTerminalRun(run); + }); + const outcomes = await Promise.allSettled( + activeTargets.map((target) => { + const run = runsById.get(target.lastRunId!); + if (run === undefined) { + throw new Error("Runtime operation timeout lost its active run snapshot."); } - const run = runsById.get(target.lastRunId); + return recordCanonicalSessionRunTerminal(bindings, { + assistantMessage: null, + error: RUNTIME_STATE_OPERATION_TIMEOUT_ERROR, + expectedSessionOperationId: input.operationId, + lifecycle: "TERMINATED", + runId: run.id, + sessionId: target.sessionId, + source: "runtime_operation", + status: "expired", + timestampMs: deadlineFor(target), + }); + }), + ); + await throwFirstFailure(outcomes); + + const latestRuns = await getTargetRuns(bindings.DB, input.targets); + const terminalRuns = input.targets.flatMap((target) => { + const run = target.lastRunId === null ? null : (latestRuns.get(target.lastRunId) ?? null); + if (run === null) { + return []; + } + if (!isTerminalRun(run)) { + throw new Error(`Runtime operation timeout did not terminalize active run ${run.id}.`); + } + return [{ run, sessionId: target.sessionId }]; + }); + const terminalAuthorities = await adoptCanonicalTerminalRuns(bindings.DB, terminalRuns); - if (!run) { - return { - event: createSessionLifecycleTerminatedEvent({ - eventId: createPlatformId(), - sourceEventId: runtimeOperationSessionEventId({ - kind: "timed_out", - operationId: input.operationId, - sessionId: target.sessionId, - }), - lastSeen: new Date().toISOString(), - message: RUNTIME_STATE_OPERATION_TIMEOUT_ERROR.message, - reason: RUNTIME_STATE_OPERATION_TIMEOUT_ERROR.code, - sessionId: target.sessionId, - }), - sessionId: target.sessionId, - }; + const noRunLifecycleTargets = input.targets.filter((target) => { + const run = target.lastRunId === null ? null : (latestRuns.get(target.lastRunId) ?? null); + return run === null; + }); + const terminatedTargets = input.targets.filter((target) => { + const run = target.lastRunId === null ? null : (latestRuns.get(target.lastRunId) ?? null); + return run !== null && terminalAuthorities.get(run.id)?.lifecycle === "TERMINATED"; + }); + const currentTerminatedTargets = await listRuntimeOperationTargets(bindings.DB, { + operationId: input.operationId, + targets: terminatedTargets, + }); + const originalTargetsById = new Map(input.targets.map((target) => [target.sessionId, target])); + const lifecycleTargets = [ + ...noRunLifecycleTargets.map((target) => { + const timestampMs = deadlineFor(target); + if (target.sessionUpdatedAt > timestampMs) { + throw new Error("Runtime operation target advanced beyond its stable timeout deadline."); + } + return { target, timestampMs }; + }), + ...currentTerminatedTargets.map((target) => { + const original = originalTargetsById.get(target.sessionId); + if (original === undefined) { + throw new Error("Runtime operation timeout lost its original target."); + } + const timestampMs = deadlineFor(original); + if (target.sessionUpdatedAt > timestampMs) { + throw new Error("Runtime operation target advanced beyond its stable timeout deadline."); + } + return { target, timestampMs }; + }), + ]; + const lifecycleOutcomes = await Promise.allSettled( + lifecycleTargets.map(async ({ target, timestampMs }) => { + const ready = await adoptRuntimeOperationReadyReceipt(bindings.DB, { + operationId: input.operationId, + target, + }); + if (ready !== "missing") { + return null; } - return { - event: createCancelledSessionRunRuntimeEvent({ - eventId: createPlatformId(), - sourceEventId: runtimeOperationRunEventId({ - kind: "timed_out", - operationId: input.operationId, - runId: target.lastRunId, - }), - lifecycle: "TERMINATED", - run, - runError: RUNTIME_STATE_OPERATION_TIMEOUT_ERROR, + const event = createSessionLifecycleTerminatedEvent({ + eventId: createPlatformId(), + sourceEventId: runtimeOperationSessionEventId({ + kind: "timed_out", + operationId: input.operationId, sessionId: target.sessionId, }), + lastSeen: new Date(timestampMs).toISOString(), + message: RUNTIME_STATE_OPERATION_TIMEOUT_ERROR.message, + occurredAtMs: timestampMs, + reason: RUNTIME_STATE_OPERATION_TIMEOUT_ERROR.code, sessionId: target.sessionId, - }; - }) - .filter(isTruthy); + }); + const outcome = await commitSessionLifecycleEventProjection(bindings.DB, { + event, + status: "TERMINATED", + target, + timestampMs, + }); + + return outcome.kind === "stale" ? null : { event, sessionId: target.sessionId }; + }), + ); + await throwFirstFailure(lifecycleOutcomes); + const lifecycleDeliveries = await Promise.allSettled( + lifecycleOutcomes.flatMap((outcome) => + outcome.status === "fulfilled" && outcome.value !== null + ? [ + publishPersistedSessionRuntimeEvents({ + bindings, + events: [outcome.value.event], + sessionId: outcome.value.sessionId, + }), + ] + : [], + ), + ); + await throwFirstFailure(lifecycleDeliveries); - await appendOneSessionRuntimeEventPerSession({ - bindings, - records, + const adoptedTargets = input.targets.filter((target) => { + const run = target.lastRunId === null ? null : (latestRuns.get(target.lastRunId) ?? null); + return run !== null && terminalAuthorities.get(run.id)?.lifecycle === "IDLE"; }); + const currentAdoptedTargets = await listRuntimeOperationTargets(bindings.DB, { + operationId: input.operationId, + targets: adoptedTargets, + }); + await transitionRuntimeTargetSessionStatus(bindings.DB, { + expectedOperationId: input.operationId, + operationId: null, + status: "IDLE", + targets: currentAdoptedTargets, + }); + + const unreleased = await listRuntimeOperationTargets(bindings.DB, { + operationId: input.operationId, + targets: input.targets, + }); + if (unreleased.length > 0) { + if (retryStaleTargets) { + await writeRuntimeOperationTimedOutSnapshotsOnce( + bindings, + { operationId: input.operationId, targets: unreleased }, + deadlines, + false, + ); + return; + } + throw new Error("Runtime operation timeout did not release every owned Session."); + } } diff --git a/apps/api/src/modules/runtime/application/runtime-state-operation-target-recovery.ts b/apps/api/src/modules/runtime/application/runtime-state-operation-target-recovery.ts index c31d6d10..7f064848 100644 --- a/apps/api/src/modules/runtime/application/runtime-state-operation-target-recovery.ts +++ b/apps/api/src/modules/runtime/application/runtime-state-operation-target-recovery.ts @@ -1,79 +1,13 @@ -import type { RuntimeOperationId, SessionRunId } from "@mosoo/id"; +import type { RuntimeOperationId } from "@mosoo/id"; import type { ApiBindings } from "../../../platform/cloudflare/worker-types"; -import { isTruthy } from "../../../shared/truthiness"; -import { getSessionRunSummariesByIds } from "../infrastructure/session-runs/session-run-store.repository"; import type { RuntimeOperationEvent } from "./runtime-state-operation-events"; import { - broadcastRuntimeOperationEvent, + commitRuntimeOperationReadySnapshots, writeRuntimeOperationInterruptedSnapshots, } from "./runtime-state-operation-target-events"; -import { - isTerminalRunStatus, - transitionRuntimeTargetSessionStatus, -} from "./runtime-state-operation-target-store"; -import type { - RuntimeSessionTarget, - RuntimeSessionTargetTransition, -} from "./runtime-state-operation-target-store"; - -type RuntimeTargetGroups = Record; -type RuntimeTargetTransitionGroups = Record< - RuntimeSessionTarget["sessionStatus"], - RuntimeSessionTargetTransition[] ->; -const RUNTIME_TARGET_SESSION_STATUSES = ["IDLE", "RESCHEDULING", "RUNNING"] as const; - -function createRuntimeTargetGroups(): RuntimeTargetGroups { - return { - IDLE: [], - RESCHEDULING: [], - RUNNING: [], - }; -} - -function createRuntimeTargetTransitionGroups(): RuntimeTargetTransitionGroups { - return { - IDLE: [], - RESCHEDULING: [], - RUNNING: [], - }; -} - -async function groupFailureRestoreTargets( - bindings: ApiBindings, - targets: readonly RuntimeSessionTargetTransition[], -): Promise { - const runIds: SessionRunId[] = []; - - for (const { previous: target } of targets) { - if (target.sessionStatus === "RUNNING" && isTruthy(target.lastRunId)) { - runIds.push(target.lastRunId); - } - } - - const runsById = await getSessionRunSummariesByIds(bindings.DB, runIds); - const groups = createRuntimeTargetTransitionGroups(); - - for (const target of targets) { - const previous = target.previous; - - if (previous.sessionStatus === "IDLE" || previous.sessionStatus === "RESCHEDULING") { - groups[previous.sessionStatus].push(target); - continue; - } - - if (!isTruthy(previous.lastRunId)) { - groups.IDLE.push(target); - continue; - } - - const run = runsById.get(previous.lastRunId); - groups[run && !isTerminalRunStatus(run.status) ? "RUNNING" : "IDLE"].push(target); - } - - return groups; -} +import { listRuntimeOperationTargets } from "./runtime-state-operation-target-store"; +import type { RuntimeSessionTargetTransition } from "./runtime-state-operation-target-store"; export async function restoreRuntimeOperationFailedTargets( bindings: ApiBindings, @@ -81,52 +15,36 @@ export async function restoreRuntimeOperationFailedTargets( readonly operationId: RuntimeOperationId; readonly readyEvent: RuntimeOperationEvent; readonly targets: readonly RuntimeSessionTargetTransition[]; + readonly terminalTimestampMs: number; }, -): Promise { - const restoreGroups = await groupFailureRestoreTargets(bindings, input.targets); - const restoredTargets: RuntimeSessionTarget[] = []; - const restoredGroups = createRuntimeTargetGroups(); - - const restoredEntries = await Promise.all( - RUNTIME_TARGET_SESSION_STATUSES.map(async (status) => { - const transitions = await transitionRuntimeTargetSessionStatus(bindings.DB, { - expectedOperationId: input.operationId, - expectedStatus: "RESCHEDULING", - status, - targets: restoreGroups[status].map((target) => target.current), - }); - const updatedTargets = transitions.map((transition) => transition.current); - - return [status, updatedTargets] as const; - }), - ); - - for (const [status, updatedTargets] of restoredEntries) { - restoredGroups[status] = updatedTargets; - restoredTargets.push(...updatedTargets); - } +): Promise { + const phaseTargets = input.targets.map((target) => target.current); + const ownedTargets = await listRuntimeOperationTargets(bindings.DB, { + operationId: input.operationId, + targets: phaseTargets, + }); await writeRuntimeOperationInterruptedSnapshots(bindings, { operationId: input.operationId, - targets: input.targets.map((target) => target.previous), + targets: ownedTargets, + timestampMs: input.terminalTimestampMs, }); - await broadcastRuntimeOperationEvent(bindings, { + const readyTargets = await listRuntimeOperationTargets(bindings.DB, { + operationId: input.operationId, + targets: phaseTargets, + }); + await commitRuntimeOperationReadySnapshots(bindings, { event: input.readyEvent, operationId: input.operationId, - targets: restoredGroups.IDLE, + targets: readyTargets, }); - const runningEvent: RuntimeOperationEvent = { - ...input.readyEvent, - observedAt: new Date().toISOString(), - status: "ready", - }; - await broadcastRuntimeOperationEvent(bindings, { - event: runningEvent, + const unreleased = await listRuntimeOperationTargets(bindings.DB, { operationId: input.operationId, - targets: restoredGroups.RUNNING, + targets: phaseTargets, }); - - return restoredTargets; + if (unreleased.length > 0) { + throw new Error("Runtime operation ready projection did not release every owned Session."); + } } diff --git a/apps/api/src/modules/runtime/application/runtime-state-operation-target-store.ts b/apps/api/src/modules/runtime/application/runtime-state-operation-target-store.ts index 55bec015..b8908824 100644 --- a/apps/api/src/modules/runtime/application/runtime-state-operation-target-store.ts +++ b/apps/api/src/modules/runtime/application/runtime-state-operation-target-store.ts @@ -1,19 +1,31 @@ -import type { SessionRunStatus } from "@mosoo/contracts/session-run"; -import { sandboxSessionsTable, sessionsTable } from "@mosoo/db"; +import { sandboxSessionsTable, sessionEventsTable, sessionsTable } from "@mosoo/db"; +import { createPlatformId } from "@mosoo/id"; import type { AgentId, PlatformId, + RuntimeEventId, RuntimeOperationId, SandboxId, SessionId, SessionRunId, } from "@mosoo/id"; -import { and, eq, inArray, isNull, lte, or, sql } from "drizzle-orm"; +import { createRuntimeEventSemanticHash } from "@mosoo/runtime-events"; +import type { RuntimeEventEnvelope } from "@mosoo/runtime-events"; +import { and, asc, eq, inArray, isNotNull, isNull, lte, or, sql } from "drizzle-orm"; -import { getAppDatabase } from "../../../platform/db/drizzle"; +import { getAppDatabase, getD1ChangeCount } from "../../../platform/db/drizzle"; import { currentTimestampMs } from "../../../time"; import { RESCHEDULING_RECONNECT_WINDOW_MS } from "../../sessions/domain/session-lifecycle"; +import { createSessionRuntimeEventProjection } from "../../sessions/domain/session-runtime-event-projection"; import { createSessionStatusTransitionPatch } from "../infrastructure/session-runs/session-lifecycle-projection.repository"; +import { + createRuntimeOperationEventAuthorityJson, + readRuntimeOperationEventIdentity, + readRuntimeOperationEventAuthority, +} from "./runtime-state-operation-event-authority"; +import type { RuntimeOperationEventStatus } from "./runtime-state-operation-event-authority"; +import { createRuntimeOperationSessionEvent } from "./runtime-state-operation-events"; +import type { RuntimeOperationEvent } from "./runtime-state-operation-events"; export interface RuntimeSessionTarget { readonly agentId: AgentId | null; @@ -21,11 +33,28 @@ export interface RuntimeSessionTarget { readonly lastRunId: SessionRunId | null; readonly sandboxId: SandboxId; readonly sessionId: SessionId; + readonly sessionRuntimeEventSeqCursor: number; readonly sessionStatusOperationId: RuntimeOperationId | null; readonly sessionStatusSeq: number; readonly sessionStatus: "IDLE" | "RUNNING" | "RESCHEDULING"; + readonly sessionUpdatedAt: number; } +export type RuntimeSessionLifecycleTarget = Pick< + RuntimeSessionTarget, + | "lastRunId" + | "sessionId" + | "sessionRuntimeEventSeqCursor" + | "sessionStatus" + | "sessionStatusOperationId" + | "sessionStatusSeq" + | "sessionUpdatedAt" +> & { readonly agentId?: AgentId | null }; + +export type SessionLifecycleEventProjectionOutcome = + | { readonly kind: "applied" | "duplicate" } + | { readonly kind: "stale" }; + export const RUNTIME_TARGET_SESSION_STATUSES: RuntimeSessionTarget["sessionStatus"][] = [ "IDLE", "RUNNING", @@ -35,13 +64,10 @@ const RUNTIME_TARGET_STATUS_WRITE_BATCH_SIZE = 25; export interface RuntimeSessionTargetTransition { readonly current: RuntimeSessionTarget; - readonly previous: RuntimeSessionTarget; } -function isTerminalRunStatus(status: SessionRunStatus): boolean { - return ( - status === "completed" || status === "failed" || status === "cancelled" || status === "expired" - ); +export interface StaleRuntimeOperationTarget extends RuntimeSessionTarget { + readonly operationId: RuntimeOperationId; } function sessionStatusOperationCondition(operationId: RuntimeOperationId | null) { @@ -72,11 +98,223 @@ function sessionTargetFreshnessCondition( eq(sessionsTable.id, target.sessionId), eq(sessionsTable.status, input.expectedStatus ?? target.sessionStatus), eq(sessionsTable.statusSeq, target.sessionStatusSeq), + eq(sessionsTable.runtimeEventSeqCursor, target.sessionRuntimeEventSeqCursor), sessionLastRunCondition(target.lastRunId), ...sessionStatusOperationCondition(expectedOperationId), ); } +interface PreparedSessionLifecycleEvent { + readonly event: RuntimeEventEnvelope; + readonly eventType: string; + readonly occurredAtMs: number; + readonly projection: ReturnType; + readonly semanticHash: string; + readonly sourceEventId: string; + readonly runtimeOperationEventJson: string | null; +} + +async function prepareSessionLifecycleEvent( + event: RuntimeEventEnvelope, + runtimeOperation?: { + readonly agentId: AgentId; + readonly operationId: RuntimeOperationId; + readonly status: RuntimeOperationEventStatus; + }, +): Promise { + const occurredAtMs = Date.parse(event.occurredAt); + if (!Number.isFinite(occurredAtMs)) { + throw new Error("Session lifecycle event time must be a valid ISO timestamp."); + } + if (event.sourceEventId === undefined) { + throw new Error("Atomic Session lifecycle events require one stable source event ID."); + } + + const projection = createSessionRuntimeEventProjection(event); + return { + event, + eventType: projection.eventType, + occurredAtMs, + projection, + semanticHash: await createRuntimeEventSemanticHash(event), + sourceEventId: event.sourceEventId, + runtimeOperationEventJson: + runtimeOperation === undefined + ? null + : createRuntimeOperationEventAuthorityJson({ + event, + ...runtimeOperation, + sessionId: event.sessionId, + }), + }; +} + +function createSessionLifecycleEventInsertStatements( + database: D1Database, + input: { + readonly prepared: PreparedSessionLifecycleEvent; + readonly target: { + readonly lastRunId: SessionRunId | null; + readonly operationId: RuntimeOperationId | null; + readonly runtimeEventSeqCursor: number; + readonly sessionId: SessionId; + readonly status: "IDLE" | "RESCHEDULING" | "TERMINATED"; + readonly statusSeq: number; + readonly updatedAt: number; + }; + }, +): [D1PreparedStatement, D1PreparedStatement] { + const { event, projection } = input.prepared; + const target = input.target; + + return [ + database + .prepare( + `INSERT INTO session_event ( + agent_id, content_text, created_at, ended_at, event_type, family, id, + occurred_at, process_status, process_type, run_id, semantic_hash, + runtime_operation_event_json, seq, + session_id, source_event_id, source, stream_id, tool_call_id, + tool_input_delta_json, tool_input_json, tool_name, tool_output_delta_text, + tool_output_text, tool_parent_message_id, tool_result_message_id, tool_status, + tokens, trace_id, visibility + ) + SELECT + s.agent_id, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, s.id, ?, ?, ?, ?, ?, ?, ?, + ?, ?, ?, ?, ?, ?, ?, ? + FROM session AS s + WHERE s.id = ? + AND s.archived_at IS NULL + AND s.cleanup_operation_kind IS NULL + AND s.last_run_id IS ? + AND s.runtime_event_seq_cursor = ? + AND s.status = ? + AND s.status_operation_id IS ? + AND s.status_seq = ? + AND s.updated_at = ? + ON CONFLICT(session_id, source_event_id) DO NOTHING`, + ) + .bind( + projection.contentText, + input.prepared.occurredAtMs, + input.prepared.occurredAtMs, + projection.eventType, + projection.family, + event.id, + input.prepared.occurredAtMs, + projection.processStatus, + projection.processType, + projection.runId, + input.prepared.semanticHash, + input.prepared.runtimeOperationEventJson, + target.runtimeEventSeqCursor, + input.prepared.sourceEventId, + projection.source, + projection.streamId, + projection.toolCallId, + projection.toolInputDeltaJson, + projection.toolInputJson, + projection.toolName, + projection.toolOutputDeltaText, + projection.toolOutputText, + projection.toolParentMessageId, + projection.toolResultMessageId, + projection.toolStatus, + projection.tokens, + projection.traceId, + projection.visibility, + target.sessionId, + target.lastRunId, + target.runtimeEventSeqCursor, + target.status, + target.operationId, + target.statusSeq, + target.updatedAt, + ), + database + .prepare( + `INSERT INTO session_event (id) + SELECT ? + WHERE EXISTS ( + SELECT 1 + FROM session AS s + WHERE s.id = ? + AND s.archived_at IS NULL + AND s.cleanup_operation_kind IS NULL + AND s.last_run_id IS ? + AND s.runtime_event_seq_cursor = ? + AND s.status = ? + AND s.status_operation_id IS ? + AND s.status_seq = ? + AND s.updated_at = ? + ) + AND NOT EXISTS ( + SELECT 1 + FROM session_event AS event + WHERE event.session_id = ? + AND event.source_event_id = ? + AND event.event_type = ? + AND event.semantic_hash = ? + AND event.runtime_operation_event_json IS ? + AND event.seq = ? + )`, + ) + .bind( + createPlatformId(), + target.sessionId, + target.lastRunId, + target.runtimeEventSeqCursor, + target.status, + target.operationId, + target.statusSeq, + target.updatedAt, + target.sessionId, + input.prepared.sourceEventId, + input.prepared.eventType, + input.prepared.semanticHash, + input.prepared.runtimeOperationEventJson, + target.runtimeEventSeqCursor, + ), + ]; +} + +async function getSessionLifecycleEventReceipt( + database: D1Database, + input: Pick< + PreparedSessionLifecycleEvent, + "eventType" | "runtimeOperationEventJson" | "semanticHash" | "sourceEventId" + > & { + readonly sessionId: SessionId; + }, +): Promise<{ readonly exact: boolean; readonly seq: number } | null> { + const receipt = await getAppDatabase(database) + .select({ + eventType: sessionEventsTable.eventType, + runtimeOperationEventJson: sessionEventsTable.runtimeOperationEventJson, + semanticHash: sessionEventsTable.semanticHash, + seq: sessionEventsTable.seq, + }) + .from(sessionEventsTable) + .where( + and( + eq(sessionEventsTable.sessionId, input.sessionId), + eq(sessionEventsTable.sourceEventId, input.sourceEventId), + ), + ) + .limit(1) + .get(); + + return receipt === undefined + ? null + : { + exact: + receipt.eventType === input.eventType && + receipt.runtimeOperationEventJson === input.runtimeOperationEventJson && + receipt.semanticHash === input.semanticHash, + seq: receipt.seq, + }; +} + export async function listRuntimeSessionTargetsForSandboxIds( database: D1Database, sandboxIds: readonly SandboxId[], @@ -94,9 +332,11 @@ export async function listRuntimeSessionTargetsForSandboxIds( lastRunId: sessionsTable.lastRunId, sandboxId: sandboxSessionsTable.sandboxId, sessionId: sessionsTable.id, + sessionRuntimeEventSeqCursor: sessionsTable.runtimeEventSeqCursor, sessionStatusOperationId: sessionsTable.statusOperationId, sessionStatusSeq: sessionsTable.statusSeq, sessionStatus: sql`${sessionsTable.status}`, + sessionUpdatedAt: sessionsTable.updatedAt, }) .from(sessionsTable) .innerJoin(sandboxSessionsTable, eq(sandboxSessionsTable.sessionId, sessionsTable.id)) @@ -105,6 +345,7 @@ export async function listRuntimeSessionTargetsForSandboxIds( inArray(sandboxSessionsTable.sandboxId, uniqueSandboxIds), eq(sandboxSessionsTable.status, "active"), isNull(sessionsTable.archivedAt), + isNull(sessionsTable.runtimeProvisioningOperationId), inArray(sessionsTable.status, RUNTIME_TARGET_SESSION_STATUSES), ), ) @@ -194,43 +435,612 @@ async function transitionRuntimeTargetSessionStatusBatch( sessionStatus: input.status, sessionStatusOperationId: updated.status_operation_id, sessionStatusSeq: updated.status_seq, + sessionUpdatedAt: timestampMs, }, - previous: target, }, ]; }); } -export async function expireStaleRuntimeOperationTargets( +export async function commitSessionLifecycleEventProjection( + database: D1Database, + input: { + readonly event: RuntimeEventEnvelope; + readonly runtimeOperation?: { + readonly operationId: RuntimeOperationId; + readonly status: RuntimeOperationEventStatus; + }; + readonly status: "IDLE" | "TERMINATED"; + readonly target: RuntimeSessionLifecycleTarget; + readonly timestampMs: number; + }, +): Promise { + if (input.runtimeOperation !== undefined && input.target.agentId == null) { + throw new Error("Runtime operation lifecycle events require an Agent-bound Session."); + } + const prepared = await prepareSessionLifecycleEvent( + input.event, + input.runtimeOperation === undefined + ? undefined + : { agentId: input.target.agentId!, ...input.runtimeOperation }, + ); + if (input.runtimeOperation?.status === "ready") { + const claim = await getRuntimeOperationEventReceipt(database, { + agentId: input.target.agentId!, + operationId: input.runtimeOperation.operationId, + sessionId: input.target.sessionId, + status: "updating", + }); + const ready = readRuntimeOperationEventIdentity(input.event, { + agentId: input.target.agentId!, + operationId: input.runtimeOperation.operationId, + sessionId: input.target.sessionId, + status: "ready", + }); + if (claim === null) { + throw new Error( + `Runtime operation ready source ${prepared.sourceEventId} has no canonical claim.`, + ); + } + if (!runtimeOperationEventIdentitiesEqual(claim, ready)) { + throw new Error( + `Runtime operation ready source ${prepared.sourceEventId} conflicts with its claim.`, + ); + } + } + if (prepared.event.sessionId !== input.target.sessionId) { + throw new Error("Session lifecycle event does not match its target Session."); + } + if (prepared.occurredAtMs !== input.timestampMs) { + throw new Error("Session lifecycle event time does not match its projection timestamp."); + } + + const receiptInput = { + eventType: prepared.eventType, + runtimeOperationEventJson: prepared.runtimeOperationEventJson, + semanticHash: prepared.semanticHash, + sessionId: input.target.sessionId, + sourceEventId: prepared.sourceEventId, + }; + const existingReceipt = await getSessionLifecycleEventReceipt(database, receiptInput); + if (existingReceipt !== null && !existingReceipt.exact) { + throw new Error( + `Session lifecycle source ${prepared.sourceEventId} conflicts with its durable receipt.`, + ); + } + + let changed = 0; + if (existingReceipt !== null) { + const result = await database + .prepare( + `UPDATE session + SET status = ?, + status_operation_id = NULL, + status_seq = status_seq + 1, + updated_at = ? + WHERE id = ? + AND archived_at IS NULL + AND cleanup_operation_kind IS NULL + AND last_run_id IS ? + AND runtime_event_seq_cursor = ? + AND status = ? + AND status_operation_id IS ? + AND status_seq = ? + AND updated_at = ? + AND EXISTS ( + SELECT 1 + FROM session_event AS event + WHERE event.session_id = ? + AND event.source_event_id = ? + AND event.event_type = ? + AND event.runtime_operation_event_json IS ? + AND event.semantic_hash = ? + )`, + ) + .bind( + input.status, + input.timestampMs, + input.target.sessionId, + input.target.lastRunId, + input.target.sessionRuntimeEventSeqCursor, + input.target.sessionStatus, + input.target.sessionStatusOperationId, + input.target.sessionStatusSeq, + input.target.sessionUpdatedAt, + input.target.sessionId, + prepared.sourceEventId, + prepared.eventType, + prepared.runtimeOperationEventJson, + prepared.semanticHash, + ) + .run(); + changed = getD1ChangeCount(result); + } else { + const nextEventSeq = input.target.sessionRuntimeEventSeqCursor + 1; + const nextStatusSeq = input.target.sessionStatusSeq + 1; + const eventStatements = createSessionLifecycleEventInsertStatements(database, { + prepared, + target: { + lastRunId: input.target.lastRunId, + operationId: null, + runtimeEventSeqCursor: nextEventSeq, + sessionId: input.target.sessionId, + status: input.status, + statusSeq: nextStatusSeq, + updatedAt: input.timestampMs, + }, + }); + const results = await database.batch([ + database + .prepare( + `UPDATE session + SET runtime_event_seq_cursor = runtime_event_seq_cursor + 1, + status = ?, + status_operation_id = NULL, + status_seq = status_seq + 1, + updated_at = ? + WHERE id = ? + AND archived_at IS NULL + AND cleanup_operation_kind IS NULL + AND last_run_id IS ? + AND runtime_event_seq_cursor = ? + AND status = ? + AND status_operation_id IS ? + AND status_seq = ? + AND updated_at = ?`, + ) + .bind( + input.status, + input.timestampMs, + input.target.sessionId, + input.target.lastRunId, + input.target.sessionRuntimeEventSeqCursor, + input.target.sessionStatus, + input.target.sessionStatusOperationId, + input.target.sessionStatusSeq, + input.target.sessionUpdatedAt, + ), + ...eventStatements, + ]); + changed = getD1ChangeCount(results[0]); + } + + if (changed > 0) { + return { kind: "applied" }; + } + + const [receipt, session] = await Promise.all([ + getSessionLifecycleEventReceipt(database, receiptInput), + database + .prepare( + `SELECT archived_at, cleanup_operation_kind, last_run_id, + runtime_event_seq_cursor, status, status_operation_id + FROM session + WHERE id = ?`, + ) + .bind(input.target.sessionId) + .first<{ + archived_at: number | null; + cleanup_operation_kind: string | null; + last_run_id: string | null; + runtime_event_seq_cursor: number; + status: string; + status_operation_id: string | null; + }>(), + ]); + if ( + receipt?.exact === true && + session?.archived_at === null && + session.cleanup_operation_kind === null && + session.last_run_id === input.target.lastRunId && + session.runtime_event_seq_cursor >= receipt.seq && + session.status === input.status && + session.status_operation_id === null + ) { + return { kind: "duplicate" }; + } + + return { kind: "stale" }; +} + +interface RuntimeOperationEventReceipt { + readonly agentId: AgentId; + readonly deploymentVersionId: RuntimeOperationEvent["deploymentVersionId"] | null; + readonly deploymentVersionNumber: RuntimeOperationEvent["deploymentVersionNumber"] | null; + readonly eventId: string; + readonly eventJson: string; + readonly eventType: string; + readonly occurredAt: number; + readonly operation: RuntimeOperationEvent["operation"]; + readonly semanticHash: string; + readonly source: string; + readonly sourceEventId: string; + readonly visibility: string; + readonly seq: number; +} + +async function getRuntimeOperationEventReceipt( + database: D1Database, + input: { + readonly agentId: AgentId; + readonly operationId: RuntimeOperationId; + readonly sessionId: SessionId; + readonly status: RuntimeOperationEventStatus; + }, +): Promise { + const sourceEventId = `runtime-operation:${input.operationId}:${input.sessionId}:${input.status}`; + const receipt = await getAppDatabase(database) + .select({ + agentId: sessionEventsTable.agentId, + eventId: sessionEventsTable.id, + eventJson: sessionEventsTable.runtimeOperationEventJson, + eventType: sessionEventsTable.eventType, + occurredAt: sessionEventsTable.occurredAt, + semanticHash: sessionEventsTable.semanticHash, + seq: sessionEventsTable.seq, + source: sessionEventsTable.source, + sourceEventId: sessionEventsTable.sourceEventId, + visibility: sessionEventsTable.visibility, + }) + .from(sessionEventsTable) + .where( + and( + eq(sessionEventsTable.sessionId, input.sessionId), + eq(sessionEventsTable.sourceEventId, sourceEventId), + ), + ) + .limit(1) + .get(); + + if (receipt === undefined) { + return null; + } + const authority = await readRuntimeOperationEventAuthority({ + agentId: input.agentId, + eventId: receipt.eventId, + eventJson: receipt.eventJson, + eventType: receipt.eventType, + occurredAt: receipt.occurredAt, + operationId: input.operationId, + rowAgentId: receipt.agentId, + semanticHash: receipt.semanticHash, + sessionId: input.sessionId, + source: receipt.source, + sourceEventId: receipt.sourceEventId, + status: input.status, + visibility: receipt.visibility, + }); + if (receipt.eventJson === null || receipt.semanticHash === null) { + throw new Error( + `Runtime operation ${input.status} source ${sourceEventId} has no semantic authority.`, + ); + } + + return { + ...receipt, + agentId: authority.agentId, + deploymentVersionId: authority.deploymentVersionId, + deploymentVersionNumber: authority.deploymentVersionNumber, + eventJson: receipt.eventJson, + operation: authority.operation, + semanticHash: receipt.semanticHash, + }; +} + +function runtimeOperationEventReceiptsEqual( + left: RuntimeOperationEventReceipt, + right: RuntimeOperationEventReceipt, +): boolean { + return ( + left.deploymentVersionId === right.deploymentVersionId && + left.deploymentVersionNumber === right.deploymentVersionNumber && + left.eventId === right.eventId && + left.eventJson === right.eventJson && + left.eventType === right.eventType && + left.occurredAt === right.occurredAt && + left.operation === right.operation && + left.semanticHash === right.semanticHash && + left.seq === right.seq && + left.source === right.source && + left.sourceEventId === right.sourceEventId && + left.visibility === right.visibility + ); +} + +function runtimeOperationEventIdentitiesEqual( + left: Pick< + RuntimeOperationEventReceipt, + "agentId" | "deploymentVersionId" | "deploymentVersionNumber" | "operation" + >, + right: Pick< + RuntimeOperationEventReceipt, + "agentId" | "deploymentVersionId" | "deploymentVersionNumber" | "operation" + >, +): boolean { + return ( + left.agentId === right.agentId && + left.deploymentVersionId === right.deploymentVersionId && + left.deploymentVersionNumber === right.deploymentVersionNumber && + left.operation === right.operation + ); +} + +export async function adoptRuntimeOperationReadyReceipt( + database: D1Database, + input: { + readonly operationId: RuntimeOperationId; + readonly target: RuntimeSessionLifecycleTarget; + }, +): Promise<"applied" | "duplicate" | "missing" | "stale"> { + if (input.target.agentId == null) { + throw new Error("Runtime operation ready adoption requires an Agent-bound Session."); + } + const claimSourceEventId = `runtime-operation:${input.operationId}:${input.target.sessionId}:updating`; + const sourceEventId = `runtime-operation:${input.operationId}:${input.target.sessionId}:ready`; + const [claim, receipt] = await Promise.all([ + getRuntimeOperationEventReceipt(database, { + agentId: input.target.agentId, + operationId: input.operationId, + sessionId: input.target.sessionId, + status: "updating", + }), + getRuntimeOperationEventReceipt(database, { + agentId: input.target.agentId, + operationId: input.operationId, + sessionId: input.target.sessionId, + status: "ready", + }), + ]); + + if (receipt === null) { + return "missing"; + } + if (claim === null) { + throw new Error(`Runtime operation ready source ${sourceEventId} has no canonical claim.`); + } + if (!runtimeOperationEventIdentitiesEqual(claim, receipt)) { + throw new Error(`Runtime operation ready source ${sourceEventId} conflicts with its claim.`); + } + + const result = await database + .prepare( + `UPDATE session + SET status = 'IDLE', + status_operation_id = NULL, + status_seq = status_seq + 1, + updated_at = ? + WHERE id = ? + AND archived_at IS NULL + AND cleanup_operation_kind IS NULL + AND last_run_id IS ? + AND runtime_event_seq_cursor = ? + AND status = ? + AND status_operation_id = ? + AND status_seq = ? + AND updated_at = ? + AND EXISTS ( + SELECT 1 + FROM session_event AS event + WHERE event.session_id = ? + AND event.source_event_id = ? + AND event.id = ? + AND event.event_type = ? + AND event.occurred_at = ? + AND event.runtime_operation_event_json = ? + AND event.semantic_hash = ? + AND event.seq = ? + AND event.source = ? + AND event.visibility = ? + ) + AND EXISTS ( + SELECT 1 + FROM session_event AS claim + WHERE claim.session_id = ? + AND claim.source_event_id = ? + AND claim.id = ? + AND claim.event_type = ? + AND claim.occurred_at = ? + AND claim.runtime_operation_event_json = ? + AND claim.semantic_hash = ? + AND claim.seq = ? + AND claim.source = ? + AND claim.visibility = ? + )`, + ) + .bind( + receipt.occurredAt, + input.target.sessionId, + input.target.lastRunId, + input.target.sessionRuntimeEventSeqCursor, + input.target.sessionStatus, + input.operationId, + input.target.sessionStatusSeq, + input.target.sessionUpdatedAt, + input.target.sessionId, + sourceEventId, + receipt.eventId, + receipt.eventType, + receipt.occurredAt, + receipt.eventJson, + receipt.semanticHash, + receipt.seq, + receipt.source, + receipt.visibility, + input.target.sessionId, + claimSourceEventId, + claim.eventId, + claim.eventType, + claim.occurredAt, + claim.eventJson, + claim.semanticHash, + claim.seq, + claim.source, + claim.visibility, + ) + .run(); + if (getD1ChangeCount(result) > 0) { + return "applied"; + } + + const [currentClaim, currentReceipt, session] = await Promise.all([ + getRuntimeOperationEventReceipt(database, { + agentId: input.target.agentId, + operationId: input.operationId, + sessionId: input.target.sessionId, + status: "updating", + }), + getRuntimeOperationEventReceipt(database, { + agentId: input.target.agentId, + operationId: input.operationId, + sessionId: input.target.sessionId, + status: "ready", + }), + database + .prepare( + `SELECT archived_at, cleanup_operation_kind, last_run_id, + runtime_event_seq_cursor, status, status_operation_id + FROM session + WHERE id = ?`, + ) + .bind(input.target.sessionId) + .first<{ + archived_at: number | null; + cleanup_operation_kind: string | null; + last_run_id: string | null; + runtime_event_seq_cursor: number; + status: string; + status_operation_id: string | null; + }>(), + ]); + + return currentClaim !== null && + currentReceipt !== null && + runtimeOperationEventReceiptsEqual(currentClaim, claim) && + runtimeOperationEventReceiptsEqual(currentReceipt, receipt) && + session?.archived_at === null && + session.cleanup_operation_kind === null && + session.last_run_id === input.target.lastRunId && + session.runtime_event_seq_cursor >= receipt.seq && + session.status === "IDLE" && + session.status_operation_id === null + ? "duplicate" + : "stale"; +} + +export async function claimRuntimeOperationTargets( database: D1Database, input: { + readonly event: RuntimeOperationEvent; readonly operationId: RuntimeOperationId; readonly targets: readonly RuntimeSessionTarget[]; }, -): Promise { - if (input.targets.length === 0) { - return []; +): Promise { + const transitions: RuntimeSessionTargetTransition[] = []; + if (input.event.status !== "updating") { + throw new Error("Runtime operation target claims require one updating event."); } + const occurredAtMs = Date.parse(input.event.observedAt); - const expired: RuntimeSessionTarget[] = []; + if (!Number.isFinite(occurredAtMs)) { + throw new Error("Runtime operation start time must be a valid ISO timestamp."); + } for ( let index = 0; index < input.targets.length; index += RUNTIME_TARGET_STATUS_WRITE_BATCH_SIZE ) { - expired.push( - ...(await expireStaleRuntimeOperationTargetsBatch(database, { - operationId: input.operationId, - targets: input.targets.slice(index, index + RUNTIME_TARGET_STATUS_WRITE_BATCH_SIZE), - })), + const targets = input.targets.slice(index, index + RUNTIME_TARGET_STATUS_WRITE_BATCH_SIZE); + const prepared = await Promise.all( + targets.map(async (target) => { + if (target.agentId === null) { + throw new Error("Runtime operation target claims require an Agent-bound Session."); + } + const event = createRuntimeOperationSessionEvent({ + event: input.event, + operationId: input.operationId, + sessionId: target.sessionId, + }); + const preparedEvent = await prepareSessionLifecycleEvent(event, { + agentId: target.agentId, + operationId: input.operationId, + status: "updating", + }); + const nextStatusSeq = target.sessionStatusSeq + 1; + const nextEventSeq = target.sessionRuntimeEventSeqCursor + 1; + + return { + target, + statements: [ + database + .prepare( + `UPDATE session + SET runtime_event_seq_cursor = runtime_event_seq_cursor + 1, + status = 'RESCHEDULING', + status_operation_id = ?, + status_seq = status_seq + 1, + updated_at = ? + WHERE id = ? + AND archived_at IS NULL + AND cleanup_operation_kind IS NULL + AND runtime_provisioning_operation_id IS NULL + AND last_run_id IS ? + AND runtime_event_seq_cursor = ? + AND status = ? + AND status_operation_id IS ? + AND status_seq = ? + AND updated_at = ?`, + ) + .bind( + input.operationId, + occurredAtMs, + target.sessionId, + target.lastRunId, + target.sessionRuntimeEventSeqCursor, + target.sessionStatus, + target.sessionStatusOperationId, + target.sessionStatusSeq, + target.sessionUpdatedAt, + ), + ...createSessionLifecycleEventInsertStatements(database, { + prepared: preparedEvent, + target: { + lastRunId: target.lastRunId, + operationId: input.operationId, + runtimeEventSeqCursor: nextEventSeq, + sessionId: target.sessionId, + status: "RESCHEDULING", + statusSeq: nextStatusSeq, + updatedAt: occurredAtMs, + }, + }), + ], + }; + }), ); + const results = await database.batch(prepared.flatMap((record) => record.statements)); + + for (const [targetIndex, record] of prepared.entries()) { + if (getD1ChangeCount(results[targetIndex * 3]) === 0) { + continue; + } + transitions.push({ + current: { + ...record.target, + sessionRuntimeEventSeqCursor: record.target.sessionRuntimeEventSeqCursor + 1, + sessionStatus: "RESCHEDULING", + sessionStatusOperationId: input.operationId, + sessionStatusSeq: record.target.sessionStatusSeq + 1, + sessionUpdatedAt: occurredAtMs, + }, + }); + } } - return expired; + return transitions; } -async function expireStaleRuntimeOperationTargetsBatch( +export async function listRuntimeOperationTargets( database: D1Database, input: { readonly operationId: RuntimeOperationId; @@ -241,34 +1051,98 @@ async function expireStaleRuntimeOperationTargetsBatch( return []; } - const now = currentTimestampMs(); - const whereClause = and( - eq(sessionsTable.status, "RESCHEDULING"), - eq(sessionsTable.statusOperationId, input.operationId), - lte(sessionsTable.updatedAt, now - RESCHEDULING_RECONNECT_WINDOW_MS), - or( - ...input.targets.map((target) => - and( - eq(sessionsTable.id, target.sessionId), - eq(sessionsTable.statusSeq, target.sessionStatusSeq), + const targetsById = new Map(input.targets.map((target) => [target.sessionId, target])); + const rows = await getAppDatabase(database) + .select({ + lastRunId: sessionsTable.lastRunId, + sessionId: sessionsTable.id, + sessionRuntimeEventSeqCursor: sessionsTable.runtimeEventSeqCursor, + sessionStatusOperationId: sessionsTable.statusOperationId, + sessionStatusSeq: sessionsTable.statusSeq, + sessionStatus: sql`${sessionsTable.status}`, + sessionUpdatedAt: sessionsTable.updatedAt, + }) + .from(sessionsTable) + .where( + and( + isNull(sessionsTable.archivedAt), + isNull(sessionsTable.runtimeProvisioningOperationId), + inArray( + sessionsTable.id, + input.targets.map((target) => target.sessionId), ), + inArray(sessionsTable.status, ["IDLE", "RESCHEDULING"]), + eq(sessionsTable.statusOperationId, input.operationId), ), - ), - ); - const results = await getAppDatabase(database) - .update(sessionsTable) - .set( - createSessionStatusTransitionPatch({ - status: "TERMINATED", - timestampMs: now, - }), ) - .where(whereClause) - .returning({ id: sessionsTable.id }) .all(); - const expiredIds = new Set(results.map((row) => row.id)); - return input.targets.filter((target) => expiredIds.has(target.sessionId)); + return rows.flatMap((row) => { + const target = targetsById.get(row.sessionId); + + return target === undefined ? [] : [{ ...target, ...row }]; + }); } -export { isTerminalRunStatus }; +export async function listStaleRuntimeOperationTargets( + database: D1Database, + input: { + readonly limit: number; + readonly staleUpdatedAtLte: number; + }, +): Promise { + if (!Number.isSafeInteger(input.limit) || input.limit <= 0) { + throw new Error("Stale runtime operation target limit must be a positive integer."); + } + + const rows = await getAppDatabase(database) + .select({ + agentId: sessionsTable.agentId, + creatorAccountId: sessionsTable.creatorAccountId, + lastRunId: sessionsTable.lastRunId, + operationId: sessionsTable.statusOperationId, + sandboxId: sandboxSessionsTable.sandboxId, + sessionId: sessionsTable.id, + sessionRuntimeEventSeqCursor: sessionsTable.runtimeEventSeqCursor, + sessionStatusOperationId: sessionsTable.statusOperationId, + sessionStatusSeq: sessionsTable.statusSeq, + sessionStatus: sql`${sessionsTable.status}`, + sessionUpdatedAt: sessionsTable.updatedAt, + }) + .from(sessionsTable) + .innerJoin(sandboxSessionsTable, eq(sandboxSessionsTable.sessionId, sessionsTable.id)) + .where( + and( + isNull(sessionsTable.archivedAt), + isNull(sessionsTable.cleanupOperationKind), + isNull(sessionsTable.runtimeProvisioningOperationId), + inArray(sessionsTable.status, ["IDLE", "RESCHEDULING"]), + isNotNull(sessionsTable.statusOperationId), + lte(sessionsTable.updatedAt, input.staleUpdatedAtLte), + ), + ) + .orderBy(asc(sessionsTable.updatedAt), asc(sessionsTable.id)) + .limit(input.limit) + .all(); + + return rows.flatMap((row) => + row.operationId === null ? [] : [{ ...row, operationId: row.operationId }], + ); +} + +export async function expireStaleRuntimeOperationTargets( + database: D1Database, + input: { + readonly operationId: RuntimeOperationId; + readonly targets: readonly RuntimeSessionTarget[]; + }, +): Promise { + if (input.targets.length === 0) { + return []; + } + + const currentTargets = await listRuntimeOperationTargets(database, input); + const staleBeforeMs = currentTimestampMs() - RESCHEDULING_RECONNECT_WINDOW_MS; + + return currentTargets.filter((target) => target.sessionUpdatedAt <= staleBeforeMs); +} diff --git a/apps/api/src/modules/runtime/application/session-lifecycle-transition.service.ts b/apps/api/src/modules/runtime/application/session-lifecycle-transition.service.ts deleted file mode 100644 index 9f2f1910..00000000 --- a/apps/api/src/modules/runtime/application/session-lifecycle-transition.service.ts +++ /dev/null @@ -1,23 +0,0 @@ -import type { RunError, SessionRunStatus } from "@mosoo/contracts/session-run"; -import type { SessionRunId } from "@mosoo/id"; - -import { setSessionRunStatus } from "../infrastructure/session-runs/session-run-store.repository"; - -export { createSessionStatusTransitionPatch } from "../infrastructure/session-runs/session-lifecycle-projection.repository"; -export type { SessionRunTransitionOutcome } from "../infrastructure/session-runs/session-run-store.repository"; - -export async function setSystemSessionRunStatus( - database: D1Database, - input: { - error?: RunError | null; - runId: SessionRunId; - status: SessionRunStatus; - }, -) { - return setSessionRunStatus(database, { - ...(input.error !== undefined ? { error: input.error } : {}), - runId: input.runId, - source: "system", - status: input.status, - }); -} diff --git a/apps/api/src/modules/runtime/application/session-runs/cancel-run.service.ts b/apps/api/src/modules/runtime/application/session-runs/cancel-run.service.ts index 7c392a3f..4426cc9b 100644 --- a/apps/api/src/modules/runtime/application/session-runs/cancel-run.service.ts +++ b/apps/api/src/modules/runtime/application/session-runs/cancel-run.service.ts @@ -1,13 +1,13 @@ import type { RuntimeCommand } from "@mosoo/contracts/runtime-command"; import type { SessionRunSummary } from "@mosoo/contracts/session-run"; -import { sessionRunsTable, sessionsTable } from "@mosoo/db"; +import { driverInstancesTable, sessionRunsTable, sessionsTable } from "@mosoo/db"; +import { sleepPromise } from "@mosoo/effects"; import { createPlatformId, parsePlatformId } from "@mosoo/id"; import type { AccountId, DriverCommandId, DriverInstanceId, AppId, - RuntimeEventId, SessionId, SessionRunId, } from "@mosoo/id"; @@ -19,18 +19,18 @@ import { getAppDatabase } from "../../../../platform/db/drizzle"; import { isTruthy } from "../../../../shared/truthiness"; import { ensureAppOwnership } from "../../../apps/application/app.service"; import type { AuthenticatedViewer } from "../../../auth/application/viewer-auth.service"; -import { appendSessionRuntimeEvents } from "../../../sessions/application/session-event-write.service"; import { sessionParticipantCondition } from "../../../sessions/domain/session-access.policy"; +import { RUNTIME_SOCKET_TIMEOUT_MS } from "../../domain/runtime-config"; import { sendDriverInstanceCommand } from "../../infrastructure/driver-instance/client"; import { isDriverControlSocketMissingError } from "../../infrastructure/driver-session-stop-errors"; +import { stopDriverSession } from "../../infrastructure/driver-session-stop.service"; import { expireUndeliveredInputStartCommandsForRun } from "../../infrastructure/session-runs/runtime-command-store.repository"; import { toSessionRunSummary } from "../../infrastructure/session-runs/session-run-row.mapper"; import type { SessionRunRow } from "../../infrastructure/session-runs/session-run-row.mapper"; -import { - getSessionRunSummary, - setSessionRunStatus, -} from "../../infrastructure/session-runs/session-run-store.repository"; -import { createCancelledSessionRunRuntimeEvent } from "./session-run-view-events.service"; +import { getSessionRunSummary } from "../../infrastructure/session-runs/session-run-store.repository"; +import { recordCanonicalSessionRunTerminal } from "./session-run-terminal-failure.service"; + +const RUN_CANCEL_POLL_MS = 100; interface CancelSessionRunInput { appId: AppId; runId: SessionRunId; @@ -42,7 +42,12 @@ async function getOwnedSessionRun( viewerId: AccountId, input: CancelSessionRunInput, ): Promise<{ + driverConnectionId: string | null; + driverGeneration: number | null; driverInstanceId: DriverInstanceId | null; + driverLastHeartbeatAt: number | null; + driverStatus: string | null; + driverUpdatedAt: number | null; run: SessionRunSummary; sessionId: SessionId; } | null> { @@ -53,10 +58,16 @@ async function getOwnedSessionRun( created_at: sessionRunsTable.createdAt, deployment_version_id: sessionRunsTable.deploymentVersionId, deployment_version_number: sessionRunsTable.deploymentVersionNumber, + driver_connection_id: driverInstancesTable.connectionId, + driver_generation: driverInstancesTable.generation, driver_instance_id: sessionRunsTable.driverInstanceId, + driver_last_heartbeat_at: driverInstancesTable.lastHeartbeatAt, + driver_status: driverInstancesTable.status, + driver_updated_at: driverInstancesTable.updatedAt, error_code: sessionRunsTable.errorCode, error_details_json: sessionRunsTable.errorDetailsJson, error_message: sessionRunsTable.errorMessage, + error_retryable: sessionRunsTable.errorRetryable, id: sessionRunsTable.id, model: sessionRunsTable.model, provider: sessionRunsTable.provider, @@ -68,6 +79,10 @@ async function getOwnedSessionRun( updated_at: sessionRunsTable.updatedAt, }) .from(sessionRunsTable) + .leftJoin( + driverInstancesTable, + eq(driverInstancesTable.id, sessionRunsTable.driverInstanceId), + ) .innerJoin(sessionsTable, eq(sessionsTable.id, sessionRunsTable.sessionId)) .where( and( @@ -85,12 +100,43 @@ async function getOwnedSessionRun( } return { + driverConnectionId: row.driver_connection_id, + driverGeneration: row.driver_generation, driverInstanceId: row.driver_instance_id, + driverLastHeartbeatAt: row.driver_last_heartbeat_at, + driverStatus: row.driver_status, + driverUpdatedAt: row.driver_updated_at, run: toSessionRunSummary(row satisfies SessionRunRow), sessionId: row.session_id, }; } +async function waitForDriverTerminalRun( + database: D1Database, + runId: SessionRunId, +): Promise { + const deadline = Date.now() + RUNTIME_SOCKET_TIMEOUT_MS; + + while (true) { + const run = await getSessionRunSummary(database, runId); + if (run === null) { + throw new Error("Session run disappeared while waiting for cancellation."); + } + if ( + run.status === "cancelled" || + run.status === "completed" || + run.status === "expired" || + run.status === "failed" + ) { + return run; + } + if (Date.now() >= deadline) { + throw new Error("Driver did not settle the run cancellation before the control timeout."); + } + await sleepPromise(RUN_CANCEL_POLL_MS); + } +} + export async function cancelRun( bindings: ApiBindings, viewer: AuthenticatedViewer, @@ -137,55 +183,70 @@ export async function cancelRun( }; } - if (isTruthy(run.driverInstanceId)) { + let driverTerminalRun: SessionRunSummary | null = null; + let requiresSyntheticCancellation = + !isTruthy(run.driverInstanceId) || run.driverGeneration === null; + + if (!requiresSyntheticCancellation && run.driverInstanceId && run.driverGeneration !== null) { const command: RuntimeCommand = { commandId: createPlatformId(), kind: "turn.cancel", reason: "viewer.cancelled", + runId, }; try { - await sendDriverInstanceCommand(bindings, run.driverInstanceId, command); + await sendDriverInstanceCommand( + bindings, + run.driverInstanceId, + run.driverGeneration, + command, + ); + driverTerminalRun = await waitForDriverTerminalRun(database, runId); } catch (error) { if (!isDriverControlSocketMissingError(error)) { throw error; } + requiresSyntheticCancellation = true; } } - const outcome = await setSessionRunStatus(database, { - runId, - source: "viewer", - status: "cancelled", - }); - - if (outcome.kind === "repair_needed") { - throw new Error("Session lifecycle projection needs repair."); - } - - if (outcome.kind === "duplicate") { - if (isTruthy(run.driverInstanceId)) { - await expireUndeliveredInputStartCommandsForRun(database, { - driverInstanceId: run.driverInstanceId, + const outcome = requiresSyntheticCancellation + ? await recordCanonicalSessionRunTerminal(bindings, { + assistantMessage: null, + error: null, + ...(run.driverGeneration === null || run.driverInstanceId === null + ? {} + : { + expectedDriverObservation: { + connectionId: run.driverConnectionId, + driverInstanceId: run.driverInstanceId, + generation: run.driverGeneration, + lastHeartbeatAt: run.driverLastHeartbeatAt, + status: run.driverStatus, + updatedAt: run.driverUpdatedAt, + }, + }), runId, - }); - } - - return { - run: outcome.run, - }; - } - - if (outcome.kind === "rejected" || outcome.kind === "stale") { - const latestRun = await getSessionRunSummary(database, runId); + sessionId: run.sessionId, + source: "viewer", + status: "cancelled", + }) + : null; - return { - run: latestRun ?? currentRun, - }; + if ( + outcome?.kind === "committed" && + run.driverInstanceId !== null && + run.driverGeneration !== null + ) { + await stopDriverSession(bindings, { + driverInstanceId: run.driverInstanceId, + expectedDriverGeneration: run.driverGeneration, + expectedSessionRunId: runId, + reason: "viewer.cancelled", + }); } - const updatedRun = outcome.run; - if (isTruthy(run.driverInstanceId)) { await expireUndeliveredInputStartCommandsForRun(database, { driverInstanceId: run.driverInstanceId, @@ -193,17 +254,9 @@ export async function cancelRun( }); } - const cancelledEvent = createCancelledSessionRunRuntimeEvent({ - eventId: createPlatformId(), - run: updatedRun, - sessionId: run.sessionId, - sourceEventId: `viewer-cancel:${runId}:cancelled`, - }); - await appendSessionRuntimeEvents({ - bindings, - events: [cancelledEvent], - sessionId: run.sessionId, - }); + if (outcome?.kind === "stale") { + return { run: outcome.run }; + } logInfo("session.turn.cancelled", { driverInstanceId: run.driverInstanceId, @@ -214,6 +267,6 @@ export async function cancelRun( }); return { - run: updatedRun, + run: outcome?.run ?? driverTerminalRun ?? currentRun, }; } diff --git a/apps/api/src/modules/runtime/application/session-runs/dispatch-queued-run.service.ts b/apps/api/src/modules/runtime/application/session-runs/dispatch-queued-run.service.ts index b04bfe64..c9ffc96a 100644 --- a/apps/api/src/modules/runtime/application/session-runs/dispatch-queued-run.service.ts +++ b/apps/api/src/modules/runtime/application/session-runs/dispatch-queued-run.service.ts @@ -6,14 +6,13 @@ import { logError, logInfo, logWarn } from "../../../../platform/cloudflare/logg import type { ApiBindings } from "../../../../platform/cloudflare/worker-types"; import type { AuthenticatedViewer } from "../../../auth/application/viewer-auth.service"; import { fileStore } from "../../../files/application/file-store"; -import { appendSessionRuntimeEvents } from "../../../sessions/application/session-event-write.service"; import { getSupportedRuntimeId } from "../../domain/runtime-config"; import { hydrateCachedRunContextFromSession } from "../session-definition/hydrate-run-context.service"; import { appendSessionResourceContextToPrompt } from "../session-resources/session-resource-prompt.service"; import { dispatchSessionRun } from "./dispatch-run.service"; import { describeRunError } from "./run-error-message"; -import { getSessionRunState, updateSessionRunStatusIfActive } from "./session-run-state.repository"; -import { createFailedSessionRunRuntimeEvent } from "./session-run-view-events.service"; +import { getSessionRunState } from "./session-run-state.repository"; +import { recordCanonicalSessionRunTerminal } from "./session-run-terminal-failure.service"; import { appendSessionRuntimeTimingEventBestEffort, createRuntimeTimingRecorder, @@ -35,42 +34,28 @@ async function failQueuedSessionRunBeforeDispatch( message, retryable: false, } as const; - // Only fail the run while it is still queued. Once another dispatcher CASed - // it to booting, this path is a losing contender and its hydration error - // must not tear down the run the winner is provisioning. - const failedRun = await updateSessionRunStatusIfActive(bindings.DB, { + const outcome = await recordCanonicalSessionRunTerminal(bindings, { + assistantMessage: null, error: runError, - expectedCurrentStatus: "queued", + expectedRunStatus: "queued", runId: input.sessionRunId, + sessionId: input.sessionId, + source: "api", status: "failed", }); - if (!failedRun) { - const state = await getSessionRunState(bindings.DB, input.sessionRunId); - + if (outcome.kind === "stale") { logWarn("session.run.context_hydration.failed.run-not-queued", { message, runId: input.sessionRunId, sessionId: input.sessionId, - status: state?.status ?? null, + status: outcome.run.status, traceId: input.traceId, }); return; } - await appendSessionRuntimeEvents({ - bindings, - events: [ - createFailedSessionRunRuntimeEvent({ - run: failedRun, - runError, - sessionId: input.sessionId, - }), - ], - sessionId: input.sessionId, - }); - logError("session.run.context_hydration.failed", { message, runId: input.sessionRunId, diff --git a/apps/api/src/modules/runtime/application/session-runs/dispatch-run-cleanup.service.ts b/apps/api/src/modules/runtime/application/session-runs/dispatch-run-cleanup.service.ts index 36dcf031..4e282d56 100644 --- a/apps/api/src/modules/runtime/application/session-runs/dispatch-run-cleanup.service.ts +++ b/apps/api/src/modules/runtime/application/session-runs/dispatch-run-cleanup.service.ts @@ -9,6 +9,7 @@ import { expireUndeliveredInputStartCommandsForRun } from "../../infrastructure/ export async function cleanupDispatchedDriver( bindings: ApiBindings, input: { + driverGeneration: number; driverInstanceId: DriverInstanceId; reason: string; runId: SessionRunId; @@ -23,6 +24,8 @@ export async function cleanupDispatchedDriver( }); await stopDriverSession(bindings, { driverInstanceId: input.driverInstanceId, + expectedDriverGeneration: input.driverGeneration, + expectedSessionRunId: input.runId, reason: input.reason, }); } catch (error) { @@ -32,6 +35,7 @@ export async function cleanupDispatchedDriver( try { await createRuntimeSubjectLifecycleService(bindings).releaseRunLease({ driverInstanceId: input.driverInstanceId, + expectedDriverGeneration: input.driverGeneration, expectedSessionRunId: input.runId, }); } catch (releaseError) { diff --git a/apps/api/src/modules/runtime/application/session-runs/dispatch-run.service.ts b/apps/api/src/modules/runtime/application/session-runs/dispatch-run.service.ts index e812ae35..a2c1fdfc 100644 --- a/apps/api/src/modules/runtime/application/session-runs/dispatch-run.service.ts +++ b/apps/api/src/modules/runtime/application/session-runs/dispatch-run.service.ts @@ -118,6 +118,7 @@ export async function dispatchSessionRun( }, ): Promise { const sandboxId = input.profile.sandbox.id; + let driverGeneration: number | null = null; let driverInstanceId: DriverInstanceId | null = null; let prepareTimingEventPromise: Promise = Promise.resolve(); // Boot-payload config traces are owner-debug telemetry; they persist off the @@ -208,6 +209,7 @@ export async function dispatchSessionRun( traceId: input.traceId, }); runLease = preparedRunLease; + driverGeneration = preparedRunLease.driverGeneration; driverInstanceId = preparedRunLease.driverInstanceId; const preparedDriverInstanceId = preparedRunLease.driverInstanceId; logInfo("session.run.prepared", { @@ -232,6 +234,7 @@ export async function dispatchSessionRun( await dispatchTiming.measure("dispatchDriverTurn", () => executionPlane.dispatchTurn(bindings, { attachmentIds: input.attachmentIds, + driverGeneration: preparedRunLease.driverGeneration, driverInstanceId: preparedDriverInstanceId, prompt: input.prompt, sessionRunId: input.sessionRunId, @@ -273,8 +276,9 @@ export async function dispatchSessionRun( traceId: input.traceId, }); - if (isTruthy(driverInstanceId)) { + if (isTruthy(driverInstanceId) && driverGeneration !== null) { await cleanupDispatchedDriver(bindings, { + driverGeneration, driverInstanceId, reason: "session.run.pre-ready-retry", runId: input.sessionRunId, @@ -285,6 +289,7 @@ export async function dispatchSessionRun( runLease?.release(); runLease = null; + driverGeneration = null; driverInstanceId = null; // Stop retrying if the run was cancelled or failed elsewhere while @@ -310,8 +315,9 @@ export async function dispatchSessionRun( await pendingBootPayloadEvents; if (error instanceof SessionRunNoLongerActiveError) { - if (isTruthy(driverInstanceId)) { + if (isTruthy(driverInstanceId) && driverGeneration !== null) { await cleanupDispatchedDriver(bindings, { + driverGeneration, driverInstanceId, reason: `session.run.${error.status}`, runId: input.sessionRunId, @@ -340,8 +346,9 @@ export async function dispatchSessionRun( retryable: false, } as const; - if (isTruthy(driverInstanceId)) { + if (isTruthy(driverInstanceId) && driverGeneration !== null) { await cleanupDispatchedDriver(bindings, { + driverGeneration, driverInstanceId, reason: "session.run.provision-failed", runId: input.sessionRunId, @@ -356,12 +363,6 @@ export async function dispatchSessionRun( sessionId: input.sessionId, source: "api", }); - if (failureOutcome.kind === "repair_needed") { - throw new Error("Session lifecycle projection needs repair.", { - cause: error, - }); - } - if (failureOutcome.kind === "not_failed") { const state = await getSessionRunState(bindings.DB, input.sessionRunId); diff --git a/apps/api/src/modules/runtime/application/session-runs/prewarm-agent-session-runtime.service.ts b/apps/api/src/modules/runtime/application/session-runs/prewarm-agent-session-runtime.service.ts index ba2eb7f4..af1ec072 100644 --- a/apps/api/src/modules/runtime/application/session-runs/prewarm-agent-session-runtime.service.ts +++ b/apps/api/src/modules/runtime/application/session-runs/prewarm-agent-session-runtime.service.ts @@ -73,25 +73,27 @@ export async function prewarmAgentSessionRuntime( } const sandboxId = hydrated.value.profile.sandbox.id; - const { subject: sandbox } = await timing.measure("activateRuntimeSubject", () => - createRuntimeSubjectLifecycleService(bindings).activate({ - agentId: hydrated.value.profile.agentId, - executionOwnerUserId: hydrated.value.profile.session.origin.executionOwnerUserId, - kind: hydrated.value.profile.kind, - networkConstraints: resolveRuntimeSubjectNetworkConstraints(bindings, { - envVars: hydrated.value.profile.envVars, + const { incarnation: sandboxIncarnation, subject: sandbox } = await timing.measure( + "activateRuntimeSubject", + () => + createRuntimeSubjectLifecycleService(bindings).activate({ + agentId: hydrated.value.profile.agentId, + executionOwnerUserId: hydrated.value.profile.session.origin.executionOwnerUserId, kind: hydrated.value.profile.kind, - network: hydrated.value.profile.network, - requestUrl: request.requestUrl, + networkConstraints: resolveRuntimeSubjectNetworkConstraints(bindings, { + envVars: hydrated.value.profile.envVars, + kind: hydrated.value.profile.kind, + network: hydrated.value.profile.network, + requestUrl: request.requestUrl, + subjectKind: hydrated.value.profile.sandbox.subjectKind, + }), + runtimeSubjectId: sandboxId, + appId: session.appId, + purpose: "prewarm", + subjectId: hydrated.value.profile.sandbox.subjectId, subjectKind: hydrated.value.profile.sandbox.subjectKind, + timing, }), - runtimeSubjectId: sandboxId, - appId: session.appId, - purpose: "prewarm", - subjectId: hydrated.value.profile.sandbox.subjectId, - subjectKind: hydrated.value.profile.sandbox.subjectKind, - timing, - }), ); handles.subject = sandbox; @@ -103,6 +105,7 @@ export async function prewarmAgentSessionRuntime( origin: hydrated.value.profile.session.origin, sandbox, sandboxId, + sandboxIncarnation, sessionId: session.id, timing, }), @@ -136,6 +139,7 @@ export async function prewarmAgentSessionRuntime( resolvedSkillCatalog: hydrated.value.skillCatalog, resolvedSkills: hydrated.value.skills, sandbox, + sandboxIncarnation, sandboxSessionId: session.id, sessionId: session.id, }), diff --git a/apps/api/src/modules/runtime/application/session-runs/resolve-permission-request.service.ts b/apps/api/src/modules/runtime/application/session-runs/resolve-permission-request.service.ts index 7a9509b8..7a0dfa0b 100644 --- a/apps/api/src/modules/runtime/application/session-runs/resolve-permission-request.service.ts +++ b/apps/api/src/modules/runtime/application/session-runs/resolve-permission-request.service.ts @@ -1,6 +1,6 @@ import { driverInstancesTable, sessionRunsTable, sessionsTable } from "@mosoo/db"; import { createPlatformId, parsePlatformId } from "@mosoo/id"; -import type { AccountId, DriverInstanceId, AppId, SessionId } from "@mosoo/id"; +import type { AccountId, DriverInstanceId, AppId, SessionId, SessionRunId } from "@mosoo/id"; import { and, eq, inArray } from "drizzle-orm"; import type { ApiBindings } from "../../../../platform/cloudflare/worker-types"; @@ -16,6 +16,7 @@ interface ResolveDriverPermissionInput { driverInstanceId: DriverInstanceId; appId: AppId; requestId: string; + runId: SessionRunId; sessionId: SessionId; } @@ -29,6 +30,8 @@ export async function resolvePermissionRequest( const row = (await getAppDatabase(bindings.DB) .select({ + driverGeneration: driverInstancesTable.generation, + runId: sessionRunsTable.id, sessionId: sessionsTable.id, }) .from(driverInstancesTable) @@ -43,6 +46,7 @@ export async function resolvePermissionRequest( .where( and( eq(driverInstancesTable.id, input.driverInstanceId), + eq(sessionRunsTable.id, input.runId), eq(sessionsTable.id, input.sessionId), eq(sessionsTable.appId, input.appId), sessionParticipantCondition(viewerId), @@ -55,10 +59,11 @@ export async function resolvePermissionRequest( throw new Error("Driver instance not found."); } - await sendDriverInstanceCommand(bindings, input.driverInstanceId, { + await sendDriverInstanceCommand(bindings, input.driverInstanceId, row.driverGeneration, { commandId: createPlatformId(), decision: input.decision, kind: "permission.resolve", requestId: input.requestId, + runId: row.runId, }); } diff --git a/apps/api/src/modules/runtime/application/session-runs/send-agent-session-events.service.ts b/apps/api/src/modules/runtime/application/session-runs/send-agent-session-events.service.ts index b01c269a..15df7863 100644 --- a/apps/api/src/modules/runtime/application/session-runs/send-agent-session-events.service.ts +++ b/apps/api/src/modules/runtime/application/session-runs/send-agent-session-events.service.ts @@ -258,7 +258,7 @@ async function handleAgentSessionEvent(input: { if (updated) { await appendSessionRuntimeEvents({ bindings: input.bindings, - events: [updated.event], + events: updated.events, sessionId: input.sessionId, }); } diff --git a/apps/api/src/modules/runtime/application/session-runs/session-permission-decision.service.ts b/apps/api/src/modules/runtime/application/session-runs/session-permission-decision.service.ts index ad1dc011..3b600dce 100644 --- a/apps/api/src/modules/runtime/application/session-runs/session-permission-decision.service.ts +++ b/apps/api/src/modules/runtime/application/session-runs/session-permission-decision.service.ts @@ -1,5 +1,5 @@ import { parsePlatformId } from "@mosoo/id"; -import type { DriverInstanceId, AppId, SessionId } from "@mosoo/id"; +import type { DriverInstanceId, AppId, SessionId, SessionRunId } from "@mosoo/id"; import type { RuntimeEventEnvelope } from "@mosoo/runtime-events"; import type { ApiBindings } from "../../../../platform/cloudflare/worker-types"; @@ -17,7 +17,7 @@ import { resolvePermissionRequest } from "./resolve-permission-request.service"; type PermissionDecision = "allow_once" | "reject_once"; export interface SessionPermissionStateUpdate { - event: RuntimeEventEnvelope; + events: RuntimeEventEnvelope[]; state: SessionLiveState; } @@ -68,27 +68,31 @@ function requirePermissionRequestDriverInstanceId( async function createPermissionStateUpdate(input: { currentState: SessionLiveState; + events: RuntimeEventEnvelope[]; +}): Promise { + return { + events: input.events, + state: input.events.reduce(applyRuntimeEventToSessionLiveState, input.currentState), + }; +} + +async function createPermissionResolvedEvent(input: { outcome?: PermissionDecision; - permissionRequests: SessionLiveState["permissionRequests"]; - requestId?: string; + requestId: string; + runId: SessionRunId; sessionId: SessionId; -}): Promise { - const event = createSessionRuntimeEvent({ +}): Promise { + return createSessionRuntimeEvent({ actor: "user", kind: "permission.resolved", origin: "viewer", payload: { ...(input.outcome === undefined ? {} : { outcome: input.outcome }), - permissionRequests: input.permissionRequests, - ...(input.requestId === undefined ? {} : { requestId: input.requestId }), + requestId: input.requestId, }, + runId: input.runId, sessionId: input.sessionId, }); - - return { - event, - state: applyRuntimeEventToSessionLiveState(input.currentState, event), - }; } export async function resolveSessionPermissionDecision( @@ -112,19 +116,22 @@ export async function resolveSessionPermissionDecision( driverInstanceId: requirePermissionRequestDriverInstanceId(request), appId: input.appId, requestId: input.requestId, + runId: parsePlatformId(request.runId, "permission request run id"), sessionId: input.sessionId, }); - const permissionRequests = currentState.permissionRequests.filter( - (candidate) => candidate.requestId !== input.requestId, - ); + const runId = parsePlatformId(request.runId, "permission request run id"); return createPermissionStateUpdate({ currentState, - outcome: input.decision, - permissionRequests, - requestId: input.requestId, - sessionId: input.sessionId, + events: [ + await createPermissionResolvedEvent({ + outcome: input.decision, + requestId: input.requestId, + runId, + sessionId: input.sessionId, + }), + ], }); } @@ -141,8 +148,6 @@ export async function rejectSessionPermissionRequests( return null; } - const remainingRequests: SessionLiveState["permissionRequests"] = []; - const cleanupResults = await runOrderedAsyncTasks( currentState.permissionRequests.map((request) => async () => { try { @@ -151,6 +156,7 @@ export async function rejectSessionPermissionRequests( driverInstanceId: requirePermissionRequestDriverInstanceId(request), appId: input.appId, requestId: request.requestId, + runId: parsePlatformId(request.runId, "permission request run id"), sessionId: input.sessionId, }); return { rejected: true, request }; @@ -161,19 +167,25 @@ export async function rejectSessionPermissionRequests( }), ); - for (const result of cleanupResults) { - if (!result.rejected) { - remainingRequests.push(result.request); - } - } + const rejectedRequests = cleanupResults.flatMap((result) => + result.rejected ? [result.request] : [], + ); - if (remainingRequests.length === currentState.permissionRequests.length) { + if (rejectedRequests.length === 0) { return null; } return createPermissionStateUpdate({ currentState, - permissionRequests: remainingRequests, - sessionId: input.sessionId, + events: await Promise.all( + rejectedRequests.map((request) => + createPermissionResolvedEvent({ + outcome: "reject_once", + requestId: request.requestId, + runId: parsePlatformId(request.runId, "permission request run id"), + sessionId: input.sessionId, + }), + ), + ), }); } diff --git a/apps/api/src/modules/runtime/application/session-runs/session-run-state.repository.ts b/apps/api/src/modules/runtime/application/session-runs/session-run-state.repository.ts index dd282636..8ca8dc5e 100644 --- a/apps/api/src/modules/runtime/application/session-runs/session-run-state.repository.ts +++ b/apps/api/src/modules/runtime/application/session-runs/session-run-state.repository.ts @@ -1,4 +1,4 @@ -import type { RunError, SessionRunSummary } from "@mosoo/contracts/session-run"; +import type { SessionRunSummary } from "@mosoo/contracts/session-run"; import { sessionRunsTable } from "@mosoo/db"; import type { DriverInstanceId, SessionRunId } from "@mosoo/id"; import { eq } from "drizzle-orm"; @@ -59,40 +59,6 @@ export async function getSessionRunState( return row; } -export async function updateSessionRunStatusIfActive( - database: D1Database, - input: { - error?: RunError | null; - expectedCurrentStatus?: SessionRunSummary["status"]; - runId: SessionRunId; - status: SessionRunSummary["status"]; - }, -): Promise { - const outcome = await setSessionRunStatus(database, { - ...(input.error !== undefined ? { error: input.error } : {}), - ...(input.expectedCurrentStatus !== undefined - ? { expectedCurrentStatus: input.expectedCurrentStatus } - : {}), - runId: input.runId, - source: "api", - status: input.status, - }); - - switch (outcome.kind) { - case "applied": - case "duplicate": { - return outcome.run; - } - case "repair_needed": { - throw new Error("Session lifecycle projection needs repair."); - } - case "rejected": - case "stale": { - return null; - } - } -} - export async function acquireSessionRunDispatch( database: D1Database, runId: SessionRunId, diff --git a/apps/api/src/modules/runtime/application/session-runs/session-run-terminal-failure.service.ts b/apps/api/src/modules/runtime/application/session-runs/session-run-terminal-failure.service.ts index 2c8cc17b..4775e8db 100644 --- a/apps/api/src/modules/runtime/application/session-runs/session-run-terminal-failure.service.ts +++ b/apps/api/src/modules/runtime/application/session-runs/session-run-terminal-failure.service.ts @@ -1,28 +1,167 @@ -import type { RunError } from "@mosoo/contracts/session-run"; -import type { SessionId, SessionRunId } from "@mosoo/id"; +import type { RunError, SessionRunSummary } from "@mosoo/contracts/session-run"; +import { + sessionEventsTable, + sessionMessagesTable, + sessionRunsTable, + sessionsTable, +} from "@mosoo/db"; +import { createPlatformId } from "@mosoo/id"; +import type { + DriverInstanceId, + RuntimeOperationId, + RuntimeEventId, + SessionId, + SessionRunId, +} from "@mosoo/id"; +import { and, eq, inArray, sql } from "drizzle-orm"; import type { ApiBindings } from "../../../../platform/cloudflare/worker-types"; -import { appendSessionRuntimeEvents } from "../../../sessions/application/session-event-write.service"; -import { createSessionRunTerminalFailureSourceId } from "../../domain/session-run-terminal-event-id"; +import { getAppDatabase } from "../../../../platform/db/drizzle"; +import { currentTimestampMs, toIsoString } from "../../../../time"; +import { publishPersistedSessionRuntimeEvents } from "../../../sessions/application/session-event-write.service"; +import { readTerminalEventSemanticAuthority } from "../../../sessions/domain/session-terminal-event-authority"; +import { createSessionRunTerminalSourceId } from "../../domain/session-run-terminal-event-id"; +import type { PreparedAssistantMessageProjection } from "../../infrastructure/driver-instance/assistant-message-projection"; +import { commitTerminalRunProjection } from "../../infrastructure/driver-instance/completed-run-commit.repository"; +import type { + ExpectedTerminalDriverObservation, + ExpectedTerminalSessionObservation, + HostTerminalRunStatus, + TerminalRunProjectionSource, +} from "../../infrastructure/driver-instance/completed-run-commit.repository"; +import { createCanonicalDriverRunFailedEvent } from "../../infrastructure/driver-instance/driver-event-canonicalization"; +import { getSessionRunSummary } from "../../infrastructure/session-runs/session-run-store.repository"; import { - getSessionRunSummary, - setSessionRunStatus, -} from "../../infrastructure/session-runs/session-run-store.repository"; -import type { SessionRunTransitionOutcome } from "../../infrastructure/session-runs/session-run-store.repository"; -import { createFailedSessionRunRuntimeEvent } from "./session-run-view-events.service"; + createCompletedSessionRunRuntimeEvent, + createFailedSessionRunRuntimeEvent, + createSessionRunUpdatedEvent, +} from "./session-run-view-events.service"; export type CanonicalSessionRunFailureOutcome = + | { kind: "failed" } + | { kind: "not_failed"; status: SessionRunSummary["status"] }; + +export type CanonicalSessionRunTerminalOutcome = | { - kind: "failed"; + commitKind: "applied" | "duplicate"; + kind: "committed"; + run: SessionRunSummary; } - | { - kind: "not_failed"; - transition: SessionRunTransitionOutcome; + | { kind: "stale"; run: SessionRunSummary }; + +function terminalRunSummary( + current: SessionRunSummary, + input: { + error: RunError | null; + status: HostTerminalRunStatus; + }, + timestamp: string, +): SessionRunSummary { + return { + ...current, + completedAt: current.completedAt ?? timestamp, + error: input.error, + startedAt: current.startedAt ?? timestamp, + status: input.status, + updatedAt: current.status === input.status ? current.updatedAt : timestamp, + }; +} + +async function readRunExecutionIdentity( + database: D1Database, + runId: SessionRunId, +): Promise<{ driverInstanceId: DriverInstanceId; runtimeId: string } | null> { + const row = await getAppDatabase(database) + .select({ + driverInstanceId: sessionRunsTable.driverInstanceId, + runtimeId: sql< + string | null + >`coalesce(${sessionRunsTable.runtimeId}, ${sessionsTable.runtimeId})`, + }) + .from(sessionRunsTable) + .innerJoin(sessionsTable, eq(sessionsTable.id, sessionRunsTable.sessionId)) + .where(eq(sessionRunsTable.id, runId)) + .limit(1) + .get(); + + return row?.driverInstanceId === null || row?.runtimeId === null || row === undefined + ? null + : { driverInstanceId: row.driverInstanceId, runtimeId: row.runtimeId }; +} + +async function hasCompletedWithoutAssistantAuthority( + database: D1Database, + input: { readonly runId: SessionRunId; readonly sessionId: SessionId }, +): Promise { + const db = getAppDatabase(database); + const [receipts, assistantMessages] = await Promise.all([ + db + .select({ + eventType: sessionEventsTable.eventType, + semanticHash: sessionEventsTable.semanticHash, + sourceEventId: sessionEventsTable.sourceEventId, + streamId: sessionEventsTable.streamId, + terminalEventJson: sessionEventsTable.terminalEventJson, + }) + .from(sessionEventsTable) + .where( + and( + eq(sessionEventsTable.sessionId, input.sessionId), + eq(sessionEventsTable.runId, input.runId), + inArray(sessionEventsTable.eventType, ["run.cancelled", "run.completed", "run.failed"]), + ), + ) + .all(), + db + .select({ + id: sessionMessagesTable.id, + projectionFormat: sessionMessagesTable.projectionFormat, + }) + .from(sessionMessagesTable) + .where( + and( + eq(sessionMessagesTable.sessionId, input.sessionId), + eq(sessionMessagesTable.sessionRunId, input.runId), + eq(sessionMessagesTable.role, "assistant"), + ), + ) + .all(), + ]); + const [receipt] = receipts; + const finalAssistantMessages = assistantMessages.filter( + (message) => message.id.toString() !== input.runId, + ); + + if (receipts.length === 0) { + return finalAssistantMessages.length === 0; + } + if ( + receipts.length !== 1 || + receipt?.eventType !== "run.completed" || + receipt.sourceEventId !== createSessionRunTerminalSourceId(input.runId, "run.completed") + ) { + return false; + } + if (receipt.semanticHash === null) { + if (receipt.terminalEventJson !== null) { + throw new Error(`Session run ${input.runId} has an invalid legacy terminal authority.`); } - | { - kind: "repair_needed"; - transition: Extract; - }; + return ( + finalAssistantMessages.length === 1 && + finalAssistantMessages[0]?.projectionFormat === "materialized" + ); + } + const authority = await readTerminalEventSemanticAuthority({ + eventJson: receipt.terminalEventJson, + eventType: receipt.eventType, + runId: input.runId, + semanticHash: receipt.semanticHash, + sessionId: input.sessionId, + sourceEventId: receipt.sourceEventId, + streamId: receipt.streamId, + }); + return authority.finalMessageId === null && finalAssistantMessages.length === 0; +} export async function recordCanonicalSessionRunFailure( bindings: ApiBindings, @@ -30,41 +169,164 @@ export async function recordCanonicalSessionRunFailure( error: RunError; runId: SessionRunId; sessionId: SessionId; - source: "api" | "driver"; + source: TerminalRunProjectionSource; }, ): Promise { - const outcome = await setSessionRunStatus(bindings.DB, { + const outcome = await recordCanonicalSessionRunTerminal(bindings, { + assistantMessage: null, error: input.error, runId: input.runId, + sessionId: input.sessionId, source: input.source, status: "failed", }); - if (outcome.kind === "repair_needed") { - return { kind: "repair_needed", transition: outcome }; - } + return outcome.kind === "committed" + ? { kind: "failed" } + : { kind: "not_failed", status: outcome.run.status }; +} - const run = - outcome.kind === "applied" || outcome.kind === "duplicate" - ? outcome.run - : await getSessionRunSummary(bindings.DB, input.runId); +export async function recordCanonicalSessionRunTerminal( + bindings: ApiBindings, + input: { + assistantMessage: PreparedAssistantMessageProjection | null; + deliver?: boolean; + error: RunError | null; + expectedDriverObservation?: ExpectedTerminalDriverObservation; + expectedRunStatus?: SessionRunSummary["status"]; + expectedSessionObservation?: ExpectedTerminalSessionObservation; + expectedSessionOperationId?: RuntimeOperationId | null; + lifecycle?: "IDLE" | "TERMINATED"; + runId: SessionRunId; + sessionId: SessionId; + source: TerminalRunProjectionSource; + sourceEventId?: string; + status: HostTerminalRunStatus; + timestampMs?: number; + }, +): Promise { + const current = await getSessionRunSummary(bindings.DB, input.runId); + if (current === null) { + throw new Error("Session Run was not found while recording its terminal projection."); + } + if ( + current.status !== input.status && + (current.status === "cancelled" || + current.status === "completed" || + current.status === "expired" || + current.status === "failed") + ) { + return { kind: "stale", run: current }; + } + if (input.expectedRunStatus !== undefined && current.status !== input.expectedRunStatus) { + return { kind: "stale", run: current }; + } - if (run?.status !== "failed" || run.error === null) { - return { kind: "not_failed", transition: outcome }; + const error = current.status === input.status ? current.error : input.error; + if (input.status === "failed" && error === null) { + throw new Error("A failed Session Run requires one durable RunError."); + } + const completedReceiptWithoutAssistant = + input.status === "completed" && input.assistantMessage === null + ? await hasCompletedWithoutAssistantAuthority(bindings.DB, { + runId: input.runId, + sessionId: input.sessionId, + }) + : false; + if ( + input.status === "completed" && + (error !== null || (input.assistantMessage === null && !completedReceiptWithoutAssistant)) + ) { + throw new Error("A completed Session Run requires one sealed final assistant reference."); + } + if (input.status !== "completed" && input.assistantMessage !== null) { + throw new Error("Only a completed Session Run can reference a final assistant message."); } - await appendSessionRuntimeEvents({ - bindings, - events: [ - createFailedSessionRunRuntimeEvent({ - run, - runError: run.error, - sessionId: input.sessionId, - sourceEventId: createSessionRunTerminalFailureSourceId(input.runId), - }), - ], + const timestampMs = input.timestampMs ?? currentTimestampMs(); + const timestamp = toIsoString(timestampMs); + const run = terminalRunSummary(current, { error, status: input.status }, timestamp); + const kind = input.status === "expired" ? "run.cancelled" : (`run.${input.status}` as const); + const sourceEventId = input.sourceEventId ?? createSessionRunTerminalSourceId(input.runId, kind); + const lifecycle = input.lifecycle ?? "IDLE"; + const event = await (async () => { + if (input.status === "completed") { + return input.assistantMessage === null + ? createSessionRunUpdatedEvent(run, input.sessionId, lifecycle, sourceEventId) + : createCompletedSessionRunRuntimeEvent({ + finalMessageId: input.assistantMessage.id, + lifecycle, + run, + sessionId: input.sessionId, + sourceEventId, + }); + } + if (input.status !== "failed") { + return createSessionRunUpdatedEvent(run, input.sessionId, lifecycle, sourceEventId); + } + + const execution = + lifecycle === "IDLE" && input.source === "driver" && input.sourceEventId === undefined + ? await readRunExecutionIdentity(bindings.DB, input.runId) + : null; + return execution === null + ? createFailedSessionRunRuntimeEvent({ + lifecycle, + run, + runError: error!, + sessionId: input.sessionId, + sourceEventId, + }) + : createCanonicalDriverRunFailedEvent({ + driverInstanceId: execution.driverInstanceId, + error: error!, + id: createPlatformId(), + occurredAt: timestamp, + runId: input.runId, + runtimeId: execution.runtimeId, + sessionId: input.sessionId, + traceId: run.traceId, + }); + })(); + const committed = await commitTerminalRunProjection(bindings.DB, { + assistantMessage: input.assistantMessage, + error, + ...(input.expectedDriverObservation === undefined + ? {} + : { expectedDriverObservation: input.expectedDriverObservation }), + ...(input.expectedRunStatus === undefined + ? {} + : { expectedRunStatus: input.expectedRunStatus }), + ...(input.expectedSessionObservation === undefined + ? {} + : { expectedSessionObservation: input.expectedSessionObservation }), + ...(input.expectedSessionOperationId === undefined + ? {} + : { expectedSessionOperationId: input.expectedSessionOperationId }), + runId: input.runId, sessionId: input.sessionId, + source: input.source, + targetStatus: input.status, + terminalEvent: { event, occurredAt: timestampMs, sourceEventId }, + timestampMs, }); + const persisted = await getSessionRunSummary(bindings.DB, input.runId); + if (persisted === null) { + throw new Error("Session Run disappeared after its terminal projection."); + } - return { kind: "failed" }; + if (committed.kind === "stale") { + return { kind: "stale", run: persisted }; + } + if (persisted.status !== input.status) { + throw new Error("Atomic terminal projection returned without its exact Session Run status."); + } + if (input.deliver !== false) { + await publishPersistedSessionRuntimeEvents({ + bindings, + events: [event], + sessionId: input.sessionId, + }); + } + return { commitKind: committed.kind, kind: "committed", run: persisted }; } diff --git a/apps/api/src/modules/runtime/application/session-runs/session-run-view-events.service.ts b/apps/api/src/modules/runtime/application/session-runs/session-run-view-events.service.ts index 8c7f5ca1..0fdae9b0 100644 --- a/apps/api/src/modules/runtime/application/session-runs/session-run-view-events.service.ts +++ b/apps/api/src/modules/runtime/application/session-runs/session-run-view-events.service.ts @@ -120,60 +120,54 @@ export function createQueuedSessionRunRuntimeEvents(input: { ]; } -export function createCancelledSessionRunRuntimeEvent(input: { +export function createFailedSessionRunRuntimeEvent(input: { eventId?: RuntimeEventId; lifecycle?: Extract; run: SessionRunSummary; - runError?: RunError | null; + runError: RunError; sessionId: SessionId; sourceEventId?: string; }): RuntimeEventEnvelope { - const run: SessionRunView = { - ...toSessionRunView(input.run), - error: input.runError - ? { - ...input.runError, - details: toPrimitiveRecord(input.runError.details), - } - : input.run.error, - status: "cancelled", - }; - return createSessionRuntimeEvent({ ...(input.eventId === undefined ? {} : { id: input.eventId }), - ...(input.sourceEventId === undefined ? {} : { sourceEventId: input.sourceEventId }), - kind: "run.cancelled", + kind: "run.failed", payload: { + error: { + code: input.runError.code, + details: toPrimitiveRecord(input.runError.details), + message: input.runError.message, + retryable: input.runError.retryable, + }, + recoverable: input.runError.retryable, lifecycle: input.lifecycle ?? "IDLE", - run, + run: toSessionRunView(input.run), }, runId: input.run.id, sessionId: input.sessionId, + ...(input.sourceEventId === undefined ? {} : { sourceEventId: input.sourceEventId }), traceId: input.run.traceId, }); } -export function createFailedSessionRunRuntimeEvent(input: { +export function createCompletedSessionRunRuntimeEvent(input: { + eventId?: RuntimeEventId; + finalMessageId: SessionMessageId; + lifecycle?: Extract; run: SessionRunSummary; - runError: RunError; sessionId: SessionId; sourceEventId?: string; }): RuntimeEventEnvelope { return createSessionRuntimeEvent({ - kind: "run.failed", + ...(input.eventId === undefined ? {} : { id: input.eventId }), + ...(input.sourceEventId === undefined ? {} : { sourceEventId: input.sourceEventId }), + kind: "run.completed", payload: { - error: { - code: input.runError.code, - details: toPrimitiveRecord(input.runError.details), - message: input.runError.message, - retryable: input.runError.retryable, - }, - lifecycle: "IDLE", + finalMessageId: input.finalMessageId, + lifecycle: input.lifecycle ?? "IDLE", run: toSessionRunView(input.run), }, runId: input.run.id, sessionId: input.sessionId, - ...(input.sourceEventId === undefined ? {} : { sourceEventId: input.sourceEventId }), traceId: input.run.traceId, }); } @@ -182,12 +176,14 @@ export function createSessionLifecycleTerminatedEvent(input: { eventId?: RuntimeEventId; lastSeen: string; message: string; + occurredAtMs?: number; reason: string; sessionId: SessionId; sourceEventId?: string; }): RuntimeEventEnvelope { return createSessionRuntimeEvent({ ...(input.eventId === undefined ? {} : { id: input.eventId }), + ...(input.occurredAtMs === undefined ? {} : { occurredAtMs: input.occurredAtMs }), ...(input.sourceEventId === undefined ? {} : { sourceEventId: input.sourceEventId }), kind: "session.lifecycle.updated", payload: { diff --git a/apps/api/src/modules/runtime/application/session-runs/stale-run-reconciliation.service.ts b/apps/api/src/modules/runtime/application/session-runs/stale-run-reconciliation.service.ts index 6d60ead7..39f4dd17 100644 --- a/apps/api/src/modules/runtime/application/session-runs/stale-run-reconciliation.service.ts +++ b/apps/api/src/modules/runtime/application/session-runs/stale-run-reconciliation.service.ts @@ -1,10 +1,11 @@ import type { RunError } from "@mosoo/contracts/session-run"; -import { driverInstancesTable, sessionRunsTable } from "@mosoo/db"; +import { driverInstancesTable, sessionRunsTable, sessionsTable } from "@mosoo/db"; import type { DriverInstanceId, SessionId, SessionRunId } from "@mosoo/id"; import { and, asc, desc, eq, inArray, isNull, lte, notInArray, or, sql } from "drizzle-orm"; import { alias } from "drizzle-orm/sqlite-core"; import { logWarn } from "../../../../platform/cloudflare/logger"; +import type { ApiBindings } from "../../../../platform/cloudflare/worker-types"; import { getAppDatabase } from "../../../../platform/db/drizzle"; import { currentTimestampMs } from "../../../../time"; import { @@ -13,15 +14,17 @@ import { } from "../../domain/runtime-config"; import { classifyReclaim } from "../../domain/session-run-reclaim-recovery"; import { recordRuntimeRunLeaseReleasedOutcome } from "../../infrastructure/runtime-subject-lifecycle/runtime-run-lease-store"; -import { setSessionRunStatus } from "../../infrastructure/session-runs/session-run-store.repository"; +import { recordCanonicalSessionRunTerminal } from "./session-run-terminal-failure.service"; export interface ActiveRunDriverRow { driver_error_message: string | null; + driver_generation: number | null; driver_instance_id: DriverInstanceId | null; driver_last_heartbeat_at: number | null; driver_status: string | null; driver_updated_at: number | null; run_id: SessionRunId; + run_status: "queued" | "booting" | "running" | "waiting_input"; session_id: SessionId; run_trace_id: string | null; run_updated_at: number; @@ -74,11 +77,13 @@ const ACTIVE_SESSION_RUN_STATUSES = ["queued", "booting", "running", "waiting_in function activeRunDriverColumns() { return { driver_error_message: runDriverInstancesTable.errorMessage, + driver_generation: runDriverInstancesTable.generation, driver_instance_id: sessionRunsTable.driverInstanceId, driver_last_heartbeat_at: runDriverInstancesTable.lastHeartbeatAt, driver_status: runDriverInstancesTable.status, driver_updated_at: runDriverInstancesTable.updatedAt, run_id: sessionRunsTable.id, + run_status: sql`${sessionRunsTable.status}`, run_trace_id: sessionRunsTable.traceId, run_updated_at: sessionRunsTable.updatedAt, session_id: sessionRunsTable.sessionId, @@ -118,6 +123,7 @@ async function findStaleActiveRun( (await getAppDatabase(database) .select(activeRunDriverColumns()) .from(sessionRunsTable) + .innerJoin(sessionsTable, eq(sessionsTable.id, sessionRunsTable.sessionId)) .leftJoin( runDriverInstancesTable, eq(runDriverInstancesTable.id, sessionRunsTable.driverInstanceId), @@ -125,6 +131,7 @@ async function findStaleActiveRun( .where( and( eq(sessionRunsTable.sessionId, sessionId), + isNull(sessionsTable.archivedAt), inArray(sessionRunsTable.status, ACTIVE_SESSION_RUN_STATUSES), ), ) @@ -152,6 +159,7 @@ async function findStaleActiveRuns( return getAppDatabase(database) .select(activeRunDriverColumns()) .from(sessionRunsTable) + .innerJoin(sessionsTable, eq(sessionsTable.id, sessionRunsTable.sessionId)) .leftJoin( runDriverInstancesTable, eq(runDriverInstancesTable.id, sessionRunsTable.driverInstanceId), @@ -159,6 +167,7 @@ async function findStaleActiveRuns( .where( and( inArray(sessionRunsTable.status, ACTIVE_SESSION_RUN_STATUSES), + isNull(sessionsTable.archivedAt), staleActiveRunPredicate(input.nowMs), ), ) @@ -169,29 +178,29 @@ async function findStaleActiveRuns( async function failStaleActiveRun(database: D1Database, staleRun: ActiveRunDriverRow) { const error = staleRunError(staleRun); - const outcome = await setSessionRunStatus(database, { + const outcome = await recordCanonicalSessionRunTerminal({ DB: database } as ApiBindings, { + assistantMessage: null, + deliver: false, error, + expectedDriverObservation: { + driverInstanceId: staleRun.driver_instance_id, + lastHeartbeatAt: staleRun.driver_last_heartbeat_at, + status: staleRun.driver_status, + updatedAt: staleRun.driver_updated_at, + }, + expectedRunStatus: staleRun.run_status, runId: staleRun.run_id, + sessionId: staleRun.session_id, source: "maintenance", status: "failed", }); - switch (outcome.kind) { - case "applied": - case "duplicate": { - await releaseStaleRunLease(database, staleRun); - return true; - } - case "repair_needed": { - throw new Error( - "Stale session run reconciliation left the session lifecycle projection stale.", - ); - } - case "rejected": - case "stale": { - return false; - } + if (outcome.kind === "stale") { + return false; } + + await releaseStaleRunLease(database, staleRun); + return true; } // Failing the run ends the lease, but only the release write re-arms the @@ -201,12 +210,13 @@ async function releaseStaleRunLease( database: D1Database, staleRun: ActiveRunDriverRow, ): Promise { - if (staleRun.driver_instance_id === null) { + if (staleRun.driver_instance_id === null || staleRun.driver_generation === null) { return; } const outcome = await recordRuntimeRunLeaseReleasedOutcome(database, { driverInstanceId: staleRun.driver_instance_id, + expectedDriverGeneration: staleRun.driver_generation, expectedSessionRunId: staleRun.run_id, }); diff --git a/apps/api/src/modules/runtime/application/session-runs/terminal-run-reconciliation.service.ts b/apps/api/src/modules/runtime/application/session-runs/terminal-run-reconciliation.service.ts index 264d4311..a689eb5b 100644 --- a/apps/api/src/modules/runtime/application/session-runs/terminal-run-reconciliation.service.ts +++ b/apps/api/src/modules/runtime/application/session-runs/terminal-run-reconciliation.service.ts @@ -1,103 +1,106 @@ import type { SessionStatus } from "@mosoo/contracts/session"; -import type { SessionRunStatus, SessionRunSummary } from "@mosoo/contracts/session-run"; +import type { SessionRunStatus } from "@mosoo/contracts/session-run"; import { driverInstancesTable, sessionEventsTable, + sessionMessagesTable, sessionRunsTable, sessionsTable, } from "@mosoo/db"; -import type { SessionId, SessionRunId } from "@mosoo/id"; -import { and, asc, eq, inArray, isNull, notExists, or, sql } from "drizzle-orm"; +import type { PlatformId, RuntimeOperationId, SessionId, SessionRunId } from "@mosoo/id"; +import { and, asc, eq, gt, inArray, isNull, lte, notExists, or, sql } from "drizzle-orm"; +import { createErrorLogContext, logWarn } from "../../../../platform/cloudflare/logger"; import type { ApiBindings } from "../../../../platform/cloudflare/worker-types"; -import { getAppDatabase } from "../../../../platform/db/drizzle"; -import { appendSessionRuntimeEvents } from "../../../sessions/application/session-event-write.service"; +import { getAppDatabase, getD1ChangeCount } from "../../../../platform/db/drizzle"; +import { currentTimestampMs } from "../../../../time"; +import { readTerminalEventSemanticAuthority } from "../../../sessions/domain/session-terminal-event-authority"; +import { isSealedPublicSessionMessageStream } from "../../../sessions/infrastructure/session-message-event-stream.repository"; import { createSessionRunTerminalSourceId } from "../../domain/session-run-terminal-event-id"; -import { - getSessionRunSummariesByIds, - setSessionRunStatus, -} from "../../infrastructure/session-runs/session-run-store.repository"; -import type { SessionRunTransitionOutcome } from "../../infrastructure/session-runs/session-run-store.repository"; -import { - createFailedSessionRunRuntimeEvent, - createSessionRunUpdatedEvent, -} from "./session-run-view-events.service"; +import { prepareAssistantMessageProjection } from "../../infrastructure/driver-instance/assistant-message-projection"; +import type { PreparedAssistantMessageProjection } from "../../infrastructure/driver-instance/assistant-message-projection"; +import type { + HostTerminalRunStatus, + TerminalRunProjectionSource, +} from "../../infrastructure/driver-instance/completed-run-commit.repository"; +import { adoptTerminalRunProjection } from "../../infrastructure/driver-instance/completed-run-commit.repository"; +import { getSessionRunSummary } from "../../infrastructure/session-runs/session-run-store.repository"; +import { recordCanonicalSessionRunTerminal } from "./session-run-terminal-failure.service"; const TERMINAL_RUN_STATUSES = ["cancelled", "completed", "expired", "failed"] as const; const TERMINAL_DRIVER_STATUSES = ["failed", "stopped"] as const; +const TERMINAL_RECONCILIATION_RETRY_AFTER_MS = 10 * 60_000; +const TERMINAL_SOURCES = new Set([ + "api", + "driver", + "maintenance", + "runtime_operation", + "system", + "viewer", +]); interface TerminalRunCandidate { readonly runId: SessionRunId; + readonly runStatus: SessionRunStatus; + readonly runStatusOperationId: RuntimeOperationId | null; + readonly runStatusSeq: number; + readonly runStatusSource: string; + readonly runTerminalReconciliationAttemptedAt: number | null; + readonly runUpdatedAt: number; readonly sessionId: SessionId; readonly sessionLastRunId: SessionRunId | null; readonly sessionStatus: SessionStatus; } +interface TerminalEventReceipt { + readonly eventType: string; + readonly runId: SessionRunId | null; + readonly semanticHash: string | null; + readonly sourceEventId: string; + readonly streamId: string | null; + readonly terminalEventJson: string | null; +} + +interface CompletedAssistantRow { + readonly contentText: string; + readonly createdByAccountId: PlatformId; + readonly id: string; + readonly planJson: string | null; + readonly projectionFormat: "event_stream_v3" | "materialized"; + readonly segmentsJson: string | null; +} + export interface TerminalRunReconciliationResult { + readonly failures: readonly { + readonly message: string; + readonly runId: SessionRunId; + readonly sessionId: SessionId; + }[]; readonly reconciledRunIds: readonly SessionRunId[]; readonly reconciledSessionIds: readonly SessionId[]; } -function assertTerminalRunProjection(outcome: SessionRunTransitionOutcome): void { - switch (outcome.kind) { - case "applied": - case "duplicate": { - return; - } - case "repair_needed": { - throw new Error("Terminal run reconciliation left the session lifecycle projection stale."); - } - case "rejected": - case "stale": { - throw new Error("Terminal run reconciliation lost a concurrent run transition."); - } - } -} - function terminalEventKind( status: SessionRunStatus, ): "run.cancelled" | "run.completed" | "run.failed" { switch (status) { - case "completed": { + case "completed": return "run.completed"; - } - case "failed": { + case "failed": return "run.failed"; - } case "cancelled": - case "expired": { + case "expired": return "run.cancelled"; - } case "queued": case "booting": case "running": - case "waiting_input": { + case "waiting_input": throw new Error(`Expected terminal Session Run status, received ${status}.`); - } } } -function createTerminalRunRecoveryEvent(input: { - readonly kind: "run.cancelled" | "run.completed" | "run.failed"; - readonly run: SessionRunSummary; - readonly sessionId: SessionId; - readonly sourceEventId: string; -}) { - if (input.kind !== "run.failed") { - return createSessionRunUpdatedEvent(input.run, input.sessionId, "IDLE", input.sourceEventId); - } - - return createFailedSessionRunRuntimeEvent({ - run: input.run, - runError: input.run.error ?? { - code: "runtime.terminal_error_missing", - details: {}, - message: "The run failed without a persisted error.", - retryable: false, - }, - sessionId: input.sessionId, - sourceEventId: input.sourceEventId, - }); +function isHostTerminalRunStatus(status: SessionRunStatus): status is HostTerminalRunStatus { + return TERMINAL_RUN_STATUSES.some((terminalStatus) => terminalStatus === status); } async function findTerminalRunCandidates( @@ -128,10 +131,17 @@ async function findTerminalRunCandidates( eq(sessionsTable.lastRunId, sessionRunsTable.id), eq(sessionsTable.status, "RUNNING"), ); + const retryBefore = currentTimestampMs() - TERMINAL_RECONCILIATION_RETRY_AFTER_MS; return database .select({ runId: sessionRunsTable.id, + runStatus: sessionRunsTable.status, + runStatusOperationId: sessionRunsTable.statusOperationId, + runStatusSeq: sessionRunsTable.statusSeq, + runStatusSource: sessionRunsTable.statusSource, + runTerminalReconciliationAttemptedAt: sessionRunsTable.terminalReconciliationAttemptedAt, + runUpdatedAt: sessionRunsTable.updatedAt, sessionId: sessionRunsTable.sessionId, sessionLastRunId: sessionsTable.lastRunId, sessionStatus: sessionsTable.status, @@ -142,6 +152,12 @@ async function findTerminalRunCandidates( .where( and( inArray(sessionRunsTable.status, TERMINAL_RUN_STATUSES), + or( + isNull(sessionRunsTable.terminalReconciliationAttemptedAt), + lte(sessionRunsTable.terminalReconciliationAttemptedAt, retryBefore), + gt(sessionRunsTable.updatedAt, sessionRunsTable.terminalReconciliationAttemptedAt), + gt(sessionsTable.updatedAt, sessionRunsTable.terminalReconciliationAttemptedAt), + ), isNull(sessionsTable.archivedAt), inArray(sessionsTable.status, ["IDLE", "RESCHEDULING", "RUNNING"]), or( @@ -152,103 +168,346 @@ async function findTerminalRunCandidates( or(staleSessionProjection, missingTerminalEvent), ), ) - .orderBy(asc(sessionRunsTable.updatedAt), asc(sessionRunsTable.id)) + .orderBy( + asc( + sql`coalesce(${sessionRunsTable.terminalReconciliationAttemptedAt}, ${sessionRunsTable.updatedAt})`, + ), + asc(sessionRunsTable.id), + ) .limit(limit) .all(); } -async function readPersistedTerminalEventKeys( +async function recordTerminalReconciliationAttempt( + bindings: ApiBindings, + candidate: TerminalRunCandidate, + attemptedAt: number, +): Promise { + const result = await getAppDatabase(bindings.DB) + .update(sessionRunsTable) + .set({ terminalReconciliationAttemptedAt: attemptedAt }) + .where( + and( + eq(sessionRunsTable.id, candidate.runId), + eq(sessionRunsTable.status, candidate.runStatus), + eq(sessionRunsTable.statusSeq, candidate.runStatusSeq), + eq(sessionRunsTable.statusSource, candidate.runStatusSource), + eq(sessionRunsTable.updatedAt, candidate.runUpdatedAt), + candidate.runStatusOperationId === null + ? isNull(sessionRunsTable.statusOperationId) + : eq(sessionRunsTable.statusOperationId, candidate.runStatusOperationId), + candidate.runTerminalReconciliationAttemptedAt === null + ? isNull(sessionRunsTable.terminalReconciliationAttemptedAt) + : eq( + sessionRunsTable.terminalReconciliationAttemptedAt, + candidate.runTerminalReconciliationAttemptedAt, + ), + ), + ) + .run(); + return getD1ChangeCount(result) > 0; +} + +async function readTerminalEventReceipts( bindings: ApiBindings, runIds: readonly SessionRunId[], -): Promise> { +): Promise> { if (runIds.length === 0) { - return new Set(); + return new Map(); } - const rows = await getAppDatabase(bindings.DB) .select({ eventType: sessionEventsTable.eventType, runId: sessionEventsTable.runId, + semanticHash: sessionEventsTable.semanticHash, + sourceEventId: sessionEventsTable.sourceEventId, + streamId: sessionEventsTable.streamId, + terminalEventJson: sessionEventsTable.terminalEventJson, }) .from(sessionEventsTable) .where( and( - inArray(sessionEventsTable.runId, [...runIds]), + inArray(sessionEventsTable.runId, runIds), inArray(sessionEventsTable.eventType, ["run.cancelled", "run.completed", "run.failed"]), ), ) .all(); - return new Set( - rows.flatMap((row) => (row.runId === null ? [] : [`${row.runId}:${row.eventType}`])), + return new Map( + [...Map.groupBy(rows, (row) => row.runId)].flatMap(([runId, receipts]) => + runId === null ? [] : [[runId, receipts]], + ), ); } -/** - * Repairs terminal Run projections after the Driver can no longer replay its - * final event. The terminal Run row itself is the durable, idempotent repair - * obligation: a missing matching terminal session_event is reconstructed with - * a stable source id, while a duplicate status transition repairs the owning - * Session lifecycle projection. - */ +export async function assertCanonicalTerminalSessionRunProjection( + bindings: ApiBindings, + input: { + readonly runId: SessionRunId; + readonly sessionId: SessionId; + readonly status: HostTerminalRunStatus; + }, +): Promise { + const run = await getSessionRunSummary(bindings.DB, input.runId); + if (run?.status !== input.status) { + throw new Error(`Terminal run ${input.runId} has no unique canonical terminal receipt.`); + } + const outcome = await adoptTerminalRunProjection(bindings.DB, { + runId: input.runId, + sessionId: input.sessionId, + }); + if (outcome.kind === "missing" || outcome.kind === "stale") { + throw new Error(`Terminal run ${input.runId} has no unique canonical terminal receipt.`); + } +} + +async function resolveCompletedAssistant( + bindings: ApiBindings, + input: { + readonly legacy: boolean; + readonly runId: SessionRunId; + readonly sessionId: SessionId; + readonly streamId?: string | null; + }, +): Promise { + const rows: CompletedAssistantRow[] = await getAppDatabase(bindings.DB) + .select({ + contentText: sessionMessagesTable.contentText, + createdByAccountId: sessionMessagesTable.createdByAccountId, + id: sessionMessagesTable.id, + planJson: sessionMessagesTable.planJson, + projectionFormat: sessionMessagesTable.projectionFormat, + segmentsJson: sessionMessagesTable.segmentsJson, + }) + .from(sessionMessagesTable) + .where( + and( + eq(sessionMessagesTable.sessionId, input.sessionId), + eq(sessionMessagesTable.sessionRunId, input.runId), + eq(sessionMessagesTable.role, "assistant"), + ), + ) + .all(); + + const isEventStreamReference = (row: CompletedAssistantRow): boolean => + row.contentText === "" && + row.planJson === null && + row.projectionFormat === "event_stream_v3" && + row.segmentsJson === null; + const withoutToolCarrier = (): CompletedAssistantRow[] => { + const carrier = rows.find((row) => row.id === input.runId); + if (carrier !== undefined && !isEventStreamReference(carrier)) { + throw new Error(`Completed run ${input.runId} has an invalid parentless tool carrier.`); + } + return rows.filter((row) => row.id !== input.runId); + }; + + if (input.legacy) { + const authorityRows = withoutToolCarrier(); + if (authorityRows.length !== 1 || authorityRows[0]?.projectionFormat !== "materialized") { + throw new Error(`Legacy completed run ${input.runId} has ambiguous assistant authority.`); + } + return null; + } + + if (input.streamId === null) { + if (withoutToolCarrier().length !== 0) { + throw new Error( + `Completed run ${input.runId} has assistant rows despite declaring no final stream.`, + ); + } + return null; + } + + const authorityRows = + input.streamId === undefined + ? withoutToolCarrier() + : rows.filter((row) => row.id === input.streamId); + const otherRows = + input.streamId === undefined ? [] : rows.filter((row) => row.id !== input.streamId); + if (input.streamId !== undefined) { + const carrier = otherRows.find((row) => row.id === input.runId); + if ( + otherRows.length > 1 || + (carrier === undefined && otherRows.length !== 0) || + (carrier !== undefined && !isEventStreamReference(carrier)) + ) { + throw new Error( + `Completed run ${input.runId} has no unique event-stream assistant reference.`, + ); + } + } + + if (authorityRows.length === 0 && input.streamId === undefined) { + return null; + } + + const [row] = authorityRows; + if (authorityRows.length !== 1 || row === undefined || !isEventStreamReference(row)) { + throw new Error(`Completed run ${input.runId} has no unique event-stream assistant reference.`); + } + const sealed = await isSealedPublicSessionMessageStream(bindings.DB, { + processType: "agent.message.delta", + runId: input.runId, + sessionId: input.sessionId, + streamId: row.id, + }); + if (!sealed) { + throw new Error(`Completed run ${input.runId} has no sealed authoritative assistant stream.`); + } + + return prepareAssistantMessageProjection({ + createdByAccountId: row.createdByAccountId, + messageId: row.id, + sessionId: input.sessionId, + sessionRunId: input.runId, + }); +} + export async function reconcileTerminalSessionRuns( bindings: ApiBindings, input: { readonly limit: number; }, ): Promise { + const database = getAppDatabase(bindings.DB); const candidates = await findTerminalRunCandidates(bindings, input.limit); const runIds = candidates.map((candidate) => candidate.runId); - const [runsById, persistedTerminalEventKeys] = await Promise.all([ - getSessionRunSummariesByIds(bindings.DB, runIds), - readPersistedTerminalEventKeys(bindings, runIds), - ]); + const receiptsByRunId = await readTerminalEventReceipts(bindings, runIds); + const failures: { + message: string; + runId: SessionRunId; + sessionId: SessionId; + }[] = []; const reconciledRunIds: SessionRunId[] = []; const reconciledSessionIds = new Set(); for (const candidate of candidates) { - const run = runsById.get(candidate.runId); - - if (run === undefined) { - continue; - } - - if (candidate.sessionLastRunId === run.id && candidate.sessionStatus === "RUNNING") { - const projection = await setSessionRunStatus(bindings.DB, { - runId: run.id, - source: "maintenance", - status: run.status, - }); - assertTerminalRunProjection(projection); - } - - const kind = terminalEventKind(run.status); - const eventKey = `${run.id}:${kind}`; - - if (!persistedTerminalEventKeys.has(eventKey)) { - const sourceEventId = createSessionRunTerminalSourceId(run.id, kind); - const persisted = await appendSessionRuntimeEvents({ - bindings, - events: [ - createTerminalRunRecoveryEvent({ - kind, - run, + try { + if (!(await recordTerminalReconciliationAttempt(bindings, candidate, currentTimestampMs()))) { + continue; + } + const run = await getSessionRunSummary(bindings.DB, candidate.runId); + if (run === null) { + continue; + } + if (!isHostTerminalRunStatus(run.status)) { + continue; + } + const terminalStatus = run.status; + const source = TERMINAL_SOURCES.has(candidate.runStatusSource as TerminalRunProjectionSource) + ? (candidate.runStatusSource as TerminalRunProjectionSource) + : null; + if (source === null) { + throw new Error(`Terminal run ${run.id} has an unknown durable source.`); + } + if (source === "runtime_operation" && candidate.runStatusOperationId === null) { + throw new Error(`Runtime-operation terminal run ${run.id} has no operation identity.`); + } + const receipts = receiptsByRunId.get(run.id) ?? []; + const expectedKind = terminalEventKind(terminalStatus); + const matchingReceipts = receipts.filter((receipt) => receipt.eventType === expectedKind); + const [matchingReceipt] = matchingReceipts; + const legacy = matchingReceipts.length === 1 && matchingReceipt?.semanticHash === null; + if (receipts.length > 1 || matchingReceipts.length > 1) { + throw new Error(`Terminal run ${run.id} has ambiguous durable terminal receipts.`); + } + if ( + matchingReceipt !== undefined && + matchingReceipt.sourceEventId !== createSessionRunTerminalSourceId(run.id, expectedKind) + ) { + throw new Error(`Terminal run ${run.id} has a noncanonical terminal receipt identity.`); + } + if (legacy && matchingReceipt?.terminalEventJson !== null) { + throw new Error(`Terminal run ${run.id} has invalid legacy semantic authority.`); + } + const semanticAuthority = + matchingReceipt === undefined || matchingReceipt.semanticHash === null + ? null + : await readTerminalEventSemanticAuthority({ + eventJson: matchingReceipt.terminalEventJson, + eventType: matchingReceipt.eventType, + runId: run.id, + semanticHash: matchingReceipt.semanticHash, + sessionId: candidate.sessionId, + sourceEventId: matchingReceipt.sourceEventId, + streamId: matchingReceipt.streamId, + }); + const assistantMessage = + terminalStatus === "completed" && matchingReceipt === undefined + ? await resolveCompletedAssistant(bindings, { + legacy, + runId: run.id, + sessionId: candidate.sessionId, + }) + : null; + const commitKind = await (async () => { + if (matchingReceipt !== undefined) { + const outcome = await adoptTerminalRunProjection(bindings.DB, { + runId: run.id, sessionId: candidate.sessionId, - sourceEventId, - }), - ], - sessionId: candidate.sessionId, - }); + }); + if (outcome.kind === "missing") { + throw new Error(`Terminal run ${run.id} lost its durable terminal receipt.`); + } + return outcome.kind === "stale" ? null : outcome.kind; + } - if (persisted.persistedCount > 0) { + const outcome = await recordCanonicalSessionRunTerminal(bindings, { + assistantMessage, + deliver: false, + error: run.error, + ...(source === "runtime_operation" + ? { expectedSessionOperationId: candidate.runStatusOperationId } + : {}), + runId: run.id, + sessionId: candidate.sessionId, + source, + status: terminalStatus, + }); + return outcome.kind === "stale" ? null : outcome.commitKind; + })(); + if (commitKind === null) { + continue; + } + if (commitKind === "applied") { reconciledRunIds.push(run.id); } + const session = await database + .select({ + lastRunId: sessionsTable.lastRunId, + status: sessionsTable.status, + }) + .from(sessionsTable) + .where(eq(sessionsTable.id, candidate.sessionId)) + .limit(1) + .get(); + const expectedSessionStatus = semanticAuthority?.lifecycle ?? "IDLE"; + if (session?.lastRunId !== run.id) { + continue; + } + if (session.status !== expectedSessionStatus) { + throw new Error( + `Terminal run ${run.id} did not converge its current Session to ${expectedSessionStatus}.`, + ); + } + reconciledSessionIds.add(candidate.sessionId); + } catch (error) { + failures.push({ + message: error instanceof Error ? error.message : "Unknown terminal reconciliation error.", + runId: candidate.runId, + sessionId: candidate.sessionId, + }); + logWarn("runtime.terminal_run.reconciliation_failed", { + ...createErrorLogContext(error), + runId: candidate.runId, + sessionId: candidate.sessionId, + }); } - - reconciledSessionIds.add(candidate.sessionId); } return { + failures, reconciledRunIds, reconciledSessionIds: [...reconciledSessionIds], }; diff --git a/apps/api/src/modules/runtime/domain/runtime-kind-policy.ts b/apps/api/src/modules/runtime/domain/runtime-kind-policy.ts index dd6c5684..dfeb2ddf 100644 --- a/apps/api/src/modules/runtime/domain/runtime-kind-policy.ts +++ b/apps/api/src/modules/runtime/domain/runtime-kind-policy.ts @@ -16,7 +16,6 @@ export type RuntimeCheckpointRule = readonly updateSubjectCheckpoint: true; } | { - readonly sanitizeTransientState: boolean; readonly type: "session_workspaces"; readonly updateSubjectCheckpoint: false; }; @@ -89,16 +88,10 @@ const SUBJECT_MEMORY_CHECKPOINT = { } as const satisfies RuntimeCheckpointRule; const SESSION_WORKSPACES_CHECKPOINT = { - sanitizeTransientState: false, type: "session_workspaces", updateSubjectCheckpoint: false, } as const satisfies RuntimeCheckpointRule; -const CATTLE_SESSION_WORKSPACE_CHECKPOINT = { - ...SESSION_WORKSPACES_CHECKPOINT, - sanitizeTransientState: true, -} as const satisfies RuntimeCheckpointRule; - const SUBJECT_MEMORY_CLEAR = { path: SANDBOX_MEMORY_PATH, type: "subject_memory", @@ -115,8 +108,8 @@ export const RUNTIME_KIND_POLICIES = { createOnHibernate: [], createOnRecreate: [], createOnReset: [], - createOnTerminal: [CATTLE_SESSION_WORKSPACE_CHECKPOINT], - restoreOnActivate: [CATTLE_SESSION_WORKSPACE_CHECKPOINT], + createOnTerminal: [SESSION_WORKSPACES_CHECKPOINT], + restoreOnActivate: [SESSION_WORKSPACES_CHECKPOINT], }, // Cattle commits the complete session workspace, including provider-native // state, before terminal lease release. Platform history remains the diff --git a/apps/api/src/modules/runtime/domain/runtime-subject-lifecycle.machine.ts b/apps/api/src/modules/runtime/domain/runtime-subject-lifecycle.machine.ts index c1fbb050..b260e29e 100644 --- a/apps/api/src/modules/runtime/domain/runtime-subject-lifecycle.machine.ts +++ b/apps/api/src/modules/runtime/domain/runtime-subject-lifecycle.machine.ts @@ -1,5 +1,4 @@ import type { SandboxStatus } from "@mosoo/contracts/sandbox"; -import { createMachine, transition } from "xstate"; export const RUNTIME_SUBJECT_CLAIMABLE_STATUSES = [ "active", @@ -11,7 +10,14 @@ export const RUNTIME_SUBJECT_OPERATION_STATUSES = [ "destroying", ] as const satisfies readonly SandboxStatus[]; +export const RUNTIME_SUBJECT_RECOVERABLE_OPERATION_STATUSES = [ + "restoring", + ...RUNTIME_SUBJECT_OPERATION_STATUSES, +] as const satisfies readonly SandboxStatus[]; + export type RuntimeSubjectOperationStatus = (typeof RUNTIME_SUBJECT_OPERATION_STATUSES)[number]; +export type RuntimeSubjectRecoverableOperationStatus = + (typeof RUNTIME_SUBJECT_RECOVERABLE_OPERATION_STATUSES)[number]; // There is no dedicated failure state. A failed lifecycle step makes the // container untrustworthy, so activation first enters `destroying`. Successful @@ -24,14 +30,6 @@ export type RuntimeSubjectLifecycleEvent = | { type: "runtime_subject.cold" } | { type: "runtime_subject.destroy" }; -const RUNTIME_SUBJECT_STATUS_BY_EVENT = { - "runtime_subject.activate": "restoring", - "runtime_subject.active": "active", - "runtime_subject.back_up": "backing_up", - "runtime_subject.cold": "cold", - "runtime_subject.destroy": "destroying", -} as const satisfies Record; - const RUNTIME_SUBJECT_EVENT_BY_STATUS = { active: { type: "runtime_subject.active" }, backing_up: { type: "runtime_subject.back_up" }, @@ -40,49 +38,13 @@ const RUNTIME_SUBJECT_EVENT_BY_STATUS = { restoring: { type: "runtime_subject.activate" }, } as const satisfies Record; -const runtimeSubjectLifecycleMachine = createMachine({ - id: "runtimeSubjectLifecycle", - initial: "cold", - states: { - active: { - on: { - "runtime_subject.active": "active", - "runtime_subject.back_up": "backing_up", - "runtime_subject.cold": "cold", - "runtime_subject.destroy": "destroying", - }, - }, - backing_up: { - on: { - "runtime_subject.active": "active", - "runtime_subject.cold": "cold", - "runtime_subject.destroy": "destroying", - }, - }, - cold: { - on: { - "runtime_subject.activate": "restoring", - "runtime_subject.back_up": "backing_up", - "runtime_subject.destroy": "destroying", - }, - }, - destroying: { - on: { - "runtime_subject.cold": "cold", - }, - }, - restoring: { - on: { - "runtime_subject.active": "active", - "runtime_subject.cold": "cold", - "runtime_subject.destroy": "destroying", - }, - }, - }, - types: {} as { - events: RuntimeSubjectLifecycleEvent; - }, -}); +const RUNTIME_SUBJECT_TRANSITIONS: Record = { + active: ["backing_up", "cold", "destroying"], + backing_up: ["active", "cold", "destroying"], + cold: ["restoring", "backing_up", "destroying"], + destroying: ["cold"], + restoring: ["active", "cold", "destroying"], +}; export type RuntimeSubjectTransitionDecision = | { @@ -126,11 +88,7 @@ export function decideRuntimeSubjectTransition(input: { }; } - const snapshot = runtimeSubjectLifecycleMachine.resolveState({ value: input.currentStatus }); - const [nextSnapshot] = transition(runtimeSubjectLifecycleMachine, snapshot, event); - const nextStatus = readRuntimeSubjectSnapshotValue(nextSnapshot.value); - - if (nextStatus === input.currentStatus) { + if (!RUNTIME_SUBJECT_TRANSITIONS[input.currentStatus].includes(input.targetStatus)) { return { currentStatus: input.currentStatus, event, @@ -143,17 +101,7 @@ export function decideRuntimeSubjectTransition(input: { return { event, kind: "accepted", - nextStatus, + nextStatus: input.targetStatus, previousStatus: input.currentStatus, }; } - -function readRuntimeSubjectSnapshotValue(value: unknown): SandboxStatus { - if (typeof value !== "string" || !(value in RUNTIME_SUBJECT_EVENT_BY_STATUS)) { - throw new Error("Runtime subject lifecycle machine returned an unknown state."); - } - - return RUNTIME_SUBJECT_STATUS_BY_EVENT[ - toRuntimeSubjectLifecycleEvent(value as SandboxStatus).type - ]; -} diff --git a/apps/api/src/modules/runtime/domain/sandbox-network-constraints.ts b/apps/api/src/modules/runtime/domain/sandbox-network-constraints.ts index 9445daf1..24f3b19b 100644 --- a/apps/api/src/modules/runtime/domain/sandbox-network-constraints.ts +++ b/apps/api/src/modules/runtime/domain/sandbox-network-constraints.ts @@ -15,6 +15,18 @@ export interface SandboxNetworkConstraints { readonly networkPolicy: EnvironmentNetworkPolicy; } +export async function hashSandboxNetworkConstraints(input: unknown): Promise { + const constraints = parseSandboxNetworkConstraints(input); + const digest = await crypto.subtle.digest( + "SHA-256", + new TextEncoder().encode( + JSON.stringify([constraints.networkPolicy, ...constraints.allowedHosts]), + ), + ); + + return new Uint8Array(digest).toHex(); +} + export function normalizeSandboxNetworkHost(host: string): string { const normalized = host.trim().toLowerCase().replace(/\.$/u, ""); diff --git a/apps/api/src/modules/runtime/domain/session-run-lifecycle.machine.ts b/apps/api/src/modules/runtime/domain/session-run-lifecycle.machine.ts index 26a12636..5273df95 100644 --- a/apps/api/src/modules/runtime/domain/session-run-lifecycle.machine.ts +++ b/apps/api/src/modules/runtime/domain/session-run-lifecycle.machine.ts @@ -1,5 +1,4 @@ import type { SessionRunStatus } from "@mosoo/contracts/session-run"; -import { createMachine, transition } from "xstate"; const TERMINAL_SESSION_RUN_STATUSES = [ "cancelled", @@ -27,17 +26,6 @@ export type SessionRunLifecycleEvent = | { type: "run.start" } | { type: "run.wait_for_input" }; -const SESSION_RUN_STATUS_BY_EVENT = { - "run.boot": "booting", - "run.cancel": "cancelled", - "run.complete": "completed", - "run.expire": "expired", - "run.fail": "failed", - "run.queue": "queued", - "run.start": "running", - "run.wait_for_input": "waiting_input", -} as const satisfies Record; - const SESSION_RUN_EVENT_BY_STATUS = { booting: { type: "run.boot" }, cancelled: { type: "run.cancel" }, @@ -49,56 +37,16 @@ const SESSION_RUN_EVENT_BY_STATUS = { waiting_input: { type: "run.wait_for_input" }, } as const satisfies Record; -const sessionRunLifecycleMachine = createMachine({ - id: "sessionRunLifecycle", - initial: "queued", - states: { - booting: { - on: { - "run.cancel": "cancelled", - "run.complete": "completed", - "run.expire": "expired", - "run.fail": "failed", - "run.start": "running", - "run.wait_for_input": "waiting_input", - }, - }, - cancelled: {}, - completed: {}, - expired: {}, - failed: {}, - queued: { - on: { - "run.boot": "booting", - "run.cancel": "cancelled", - "run.expire": "expired", - "run.fail": "failed", - "run.start": "running", - }, - }, - running: { - on: { - "run.cancel": "cancelled", - "run.complete": "completed", - "run.expire": "expired", - "run.fail": "failed", - "run.wait_for_input": "waiting_input", - }, - }, - waiting_input: { - on: { - "run.cancel": "cancelled", - "run.complete": "completed", - "run.expire": "expired", - "run.fail": "failed", - "run.start": "running", - }, - }, - }, - types: {} as { - events: SessionRunLifecycleEvent; - }, -}); +const previousStatusesByTarget: Readonly> = { + booting: ["queued"], + cancelled: ["booting", "queued", "running", "waiting_input"], + completed: ["booting", "running", "waiting_input"], + expired: ["booting", "queued", "running", "waiting_input"], + failed: ["booting", "queued", "running", "waiting_input"], + queued: [], + running: ["booting", "queued", "waiting_input"], + waiting_input: ["booting", "running"], +}; export type SessionRunTransitionDecision = | { @@ -167,11 +115,7 @@ export function decideSessionRunTransition(input: { }; } - const snapshot = sessionRunLifecycleMachine.resolveState({ value: input.currentStatus }); - const [nextSnapshot] = transition(sessionRunLifecycleMachine, snapshot, event); - const nextStatus = readSessionRunSnapshotValue(nextSnapshot.value); - - if (nextStatus === input.currentStatus) { + if (!previousStatusesByTarget[input.targetStatus].includes(input.currentStatus)) { return { currentStatus: input.currentStatus, event, @@ -184,15 +128,7 @@ export function decideSessionRunTransition(input: { return { event, kind: "accepted", - nextStatus, + nextStatus: input.targetStatus, previousStatus: input.currentStatus, }; } - -function readSessionRunSnapshotValue(value: unknown): SessionRunStatus { - if (typeof value !== "string" || !(value in SESSION_RUN_EVENT_BY_STATUS)) { - throw new Error("Session run lifecycle machine returned an unknown state."); - } - - return SESSION_RUN_STATUS_BY_EVENT[toSessionRunLifecycleEvent(value as SessionRunStatus).type]; -} diff --git a/apps/api/src/modules/runtime/domain/session-run-terminal-event-id.ts b/apps/api/src/modules/runtime/domain/session-run-terminal-event-id.ts index 2faef709..a0dbfdd7 100644 --- a/apps/api/src/modules/runtime/domain/session-run-terminal-event-id.ts +++ b/apps/api/src/modules/runtime/domain/session-run-terminal-event-id.ts @@ -1,17 +1,7 @@ import type { SessionRunId } from "@mosoo/id"; -import type { RuntimeEventKind } from "@mosoo/runtime-events"; +import { createSessionRunTerminalSourceId } from "@mosoo/runtime-events"; -type TerminalSessionRunEventKind = Extract< - RuntimeEventKind, - "run.cancelled" | "run.completed" | "run.failed" ->; - -export function createSessionRunTerminalSourceId( - runId: SessionRunId, - kind: TerminalSessionRunEventKind, -): string { - return `session-run-terminal:${runId}:${kind}`; -} +export { createSessionRunTerminalSourceId }; export function createSessionRunTerminalFailureSourceId(runId: SessionRunId): string { return createSessionRunTerminalSourceId(runId, "run.failed"); diff --git a/apps/api/src/modules/runtime/infrastructure/driver-instance/assistant-message-projection.ts b/apps/api/src/modules/runtime/infrastructure/driver-instance/assistant-message-projection.ts index 6e49a1a5..88550b18 100644 --- a/apps/api/src/modules/runtime/infrastructure/driver-instance/assistant-message-projection.ts +++ b/apps/api/src/modules/runtime/infrastructure/driver-instance/assistant-message-projection.ts @@ -1,161 +1,48 @@ -import type { SessionMessageSegment } from "@mosoo/contracts/session"; -import { sessionMessagesTable } from "@mosoo/db"; import { parsePlatformId } from "@mosoo/id"; -import type { - DriverInstanceId, - PlatformId, - SessionId, - SessionMessageId, - SessionRunId, -} from "@mosoo/id"; -import { and, desc, eq } from "drizzle-orm"; - -import { logInfo, logWarn } from "../../../../platform/cloudflare/logger"; -import { getAppDatabase } from "../../../../platform/db/drizzle"; -import type { - SessionLiveState, - SessionLiveStateMessage, - SessionViewSegment, -} from "../../../sessions/application/session-live-state.service"; -import { insertSessionMessage } from "../../../sessions/application/session-message-write.service"; - -function toSessionMessageSegments(segments: SessionViewSegment[]): SessionMessageSegment[] { - const result: SessionMessageSegment[] = []; - - for (const segment of segments) { - switch (segment.kind) { - case "text": { - result.push({ kind: "text", text: segment.text }); - break; - } - case "reasoning": { - break; - } - case "tool_use": { - result.push({ - argsText: segment.argsText, - kind: "tool_use", - path: segment.path, - tool: segment.tool, - toolCallId: segment.toolCallId, - }); - break; - } - case "tool_result": { - result.push({ - kind: "tool_result", - output: segment.output, - tool: segment.tool, - toolCallId: segment.toolCallId, - }); - break; - } - default: { - const exhaustiveSegment: never = segment; - - throw new Error(`Unsupported session segment kind: ${String(exhaustiveSegment)}`); - } - } - } - - return result; +import type { PlatformId, SessionId, SessionMessageId, SessionRunId } from "@mosoo/id"; + +export interface PreparedAssistantMessageProjection { + contentText: string; + createdByAccountId: PlatformId; + id: SessionMessageId; + planJson: string | null; + projectionFormat: "event_stream_v3"; + segmentsJson: string | null; + sessionId: SessionId; + sessionRunId: SessionRunId; } -function findAssistantMessage( - messages: SessionLiveStateMessage[], - messageId: string, -): SessionLiveStateMessage | null { - return ( - messages.find((message) => message.id === messageId && message.role === "assistant") ?? null - ); -} - -export async function persistAssistantMessageProjection( - database: D1Database, - input: { - createdByAccountId: PlatformId; - driverInstanceId: DriverInstanceId; - messageId: string; - messageText: string; - sessionId: SessionId; - sessionRunId: SessionRunId; - state: SessionLiveState; - }, -): Promise { - const message = findAssistantMessage(input.state.messages, input.messageId); - const useStructuredProjection = message?.content === input.messageText; - - if (message !== null && !useStructuredProjection) { - logWarn("runtime.assistant.message.snapshot_mismatch", { - driverInstanceId: input.driverInstanceId, - finalMessageId: input.messageId, - projectedTextLength: message.content.length, - reason: "RUN_FINISHED final assistant snapshot did not match the live projection", - sessionId: input.sessionId, - sessionRunId: input.sessionRunId, - snapshotTextLength: input.messageText.length, - }); - } - +export function prepareAssistantMessageProjection(input: { + createdByAccountId: PlatformId; + messageId: string; + sessionId: SessionId; + sessionRunId: SessionRunId; +}): PreparedAssistantMessageProjection { const messageId = parsePlatformId(input.messageId, "assistant message id"); - const plan = useStructuredProjection && message !== null ? message.plan : []; - const segments = - useStructuredProjection && message !== null - ? toSessionMessageSegments(message.segments) - : [{ kind: "text" as const, text: input.messageText }]; - const persistedMessage = await readPersistedAssistantMessage(database, input.sessionRunId); - - if (persistedMessage !== null) { - if (persistedMessage.content !== input.messageText) { - throw new Error( - `Canonical final assistant message conflicts with the persisted projection for run ${input.sessionRunId}.`, - ); - } - - // Provider reconnects can replay the same canonical snapshot after the - // driver process has generated a new message id. The run-level text is the - // durable identity here: preserve the first canonical transcript row. - return; - } - - logInfo("runtime.assistant.message.persisting", { - driverInstanceId: input.driverInstanceId, - planEntries: plan.length, - segmentCount: segments.length, + return { + // The authoritative aggregate lives in paginated session_event rows. This + // row is only the transcript identity/order reference; materializing the + // same unbounded body here would exceed D1's per-row limit. + contentText: "", + createdByAccountId: input.createdByAccountId, + id: messageId, + planJson: null, + projectionFormat: "event_stream_v3", + segmentsJson: null, sessionId: input.sessionId, sessionRunId: input.sessionRunId, - textLength: input.messageText.length, - }); + }; +} - await insertSessionMessage(database, { - content: input.messageText, +export function prepareToolCarrierProjection(input: { + createdByAccountId: PlatformId; + sessionId: SessionId; + sessionRunId: SessionRunId; +}): PreparedAssistantMessageProjection { + return prepareAssistantMessageProjection({ createdByAccountId: input.createdByAccountId, - id: messageId, - plan, - role: "assistant", - segments, + messageId: input.sessionRunId, sessionId: input.sessionId, sessionRunId: input.sessionRunId, }); } - -async function readPersistedAssistantMessage( - database: D1Database, - sessionRunId: SessionRunId, -): Promise<{ content: string; id: SessionMessageId } | null> { - const row = - (await getAppDatabase(database) - .select({ content: sessionMessagesTable.contentText, id: sessionMessagesTable.id }) - .from(sessionMessagesTable) - .where( - and( - eq(sessionMessagesTable.sessionRunId, sessionRunId), - eq(sessionMessagesTable.role, "assistant"), - ), - ) - .orderBy(desc(sessionMessagesTable.seq)) - .limit(1) - .get()) ?? null; - - return row; -} diff --git a/apps/api/src/modules/runtime/infrastructure/driver-instance/client.ts b/apps/api/src/modules/runtime/infrastructure/driver-instance/client.ts index eeb3b54f..154a32d4 100644 --- a/apps/api/src/modules/runtime/infrastructure/driver-instance/client.ts +++ b/apps/api/src/modules/runtime/infrastructure/driver-instance/client.ts @@ -3,7 +3,6 @@ import type { DriverInstanceId } from "@mosoo/id"; import type { ApiBindings } from "../../../../platform/cloudflare/worker-types"; import type { - DriverInstanceHelloResult, DriverInstanceReadyResult, DriverInstanceSnapshot, DriverInstanceWaitForCloseResult, @@ -87,26 +86,18 @@ export async function upgradeDriverInstanceSocket( ); } -export async function waitForDriverInstanceHello( - env: ApiBindings, - driverInstanceId: DriverInstanceId, - timeoutMs: number, -): Promise { - return expectJson( - await getDriverConnectionStub(env, driverInstanceId).fetch( - createDoRequest(driverInstanceId, `/wait/hello?timeoutMs=${timeoutMs}`), - ), - ); -} - export async function waitForDriverInstanceReady( env: ApiBindings, driverInstanceId: DriverInstanceId, + generation: number, timeoutMs: number, ): Promise { return expectJson( await getDriverConnectionStub(env, driverInstanceId).fetch( - createDoRequest(driverInstanceId, `/wait/ready?timeoutMs=${timeoutMs}`), + createDoRequest( + driverInstanceId, + `/wait/ready?generation=${generation}&timeoutMs=${timeoutMs}`, + ), ), ); } @@ -114,11 +105,15 @@ export async function waitForDriverInstanceReady( export async function waitForDriverInstanceClose( env: ApiBindings, driverInstanceId: DriverInstanceId, + generation: number, timeoutMs: number, ): Promise { return expectJson( await getDriverConnectionStub(env, driverInstanceId).fetch( - createDoRequest(driverInstanceId, `/wait/close?timeoutMs=${timeoutMs}`), + createDoRequest( + driverInstanceId, + `/wait/close?generation=${generation}&timeoutMs=${timeoutMs}`, + ), ), ); } @@ -137,12 +132,13 @@ export async function getDriverInstanceSnapshot( export async function sendDriverInstanceCommand( env: ApiBindings, driverInstanceId: DriverInstanceId, + generation: number, command: RuntimeCommand, ): Promise { await expectJson<{ ok: true }>( await getDriverConnectionStub(env, driverInstanceId).fetch( createDoRequest(driverInstanceId, "/control/send", { - body: JSON.stringify(command), + body: JSON.stringify({ command, generation }), headers: { "content-type": "application/json", }, @@ -155,12 +151,13 @@ export async function sendDriverInstanceCommand( export async function failDriverInstance( env: ApiBindings, driverInstanceId: DriverInstanceId, + generation: number, message: string, ): Promise { await expectJson<{ ok: true }>( await getDriverConnectionStub(env, driverInstanceId).fetch( createDoRequest(driverInstanceId, "/control/fail", { - body: JSON.stringify({ message }), + body: JSON.stringify({ generation, message }), headers: { "content-type": "application/json", }, @@ -173,12 +170,13 @@ export async function failDriverInstance( export async function destroyDriverInstanceDurableObject( env: ApiBindings, driverInstanceId: DriverInstanceId, + generation: number, reason: string, ): Promise { await expectJson<{ ok: true }>( await getDriverConnectionStub(env, driverInstanceId).fetch( createDoRequest(driverInstanceId, "/control/destroy", { - body: JSON.stringify({ reason }), + body: JSON.stringify({ generation, reason }), headers: { "content-type": "application/json", }, diff --git a/apps/api/src/modules/runtime/infrastructure/driver-instance/commands.ts b/apps/api/src/modules/runtime/infrastructure/driver-instance/commands.ts index 4dec7623..58a9334f 100644 --- a/apps/api/src/modules/runtime/infrastructure/driver-instance/commands.ts +++ b/apps/api/src/modules/runtime/infrastructure/driver-instance/commands.ts @@ -12,7 +12,7 @@ export interface DriverInstanceCommandState { } interface CommandOptions { - markCommandDelivered: (command: RuntimeCommand) => Promise; + markCommandDelivered: (command: RuntimeCommand) => Promise<"delivered" | "discarded" | "retry">; persistCommandQueue: () => Promise; } @@ -37,17 +37,17 @@ async function dispatchQueuedRuntimeCommands( try { waiter.assertActiveConnection(); await options.persistCommandQueue(); - const delivered = await waiter.markCommandDelivered(command); + const delivery = await waiter.markCommandDelivered(command); waiter.assertActiveConnection(); - if (!delivered) { + if (delivery === "retry") { state.commandQueue.unshift(command); await options.persistCommandQueue(); waiter.deferred.resolve(null); continue; } - waiter.deferred.resolve(command); + waiter.deferred.resolve(delivery === "delivered" ? command : null); } catch { state.commandQueue.unshift(command); await options.persistCommandQueue(); @@ -89,16 +89,16 @@ export async function nextRuntimeCommand( } options.assertActiveConnection(); - const delivered = await options.markCommandDelivered(command); + const delivery = await options.markCommandDelivered(command); options.assertActiveConnection(); - if (!delivered) { + if (delivery === "retry") { state.commandQueue.unshift(command); await options.persistCommandQueue(); return null; } - return command; + return delivery === "delivered" ? command : null; } if (state.terminalized) { diff --git a/apps/api/src/modules/runtime/infrastructure/driver-instance/completed-run-commit.repository.ts b/apps/api/src/modules/runtime/infrastructure/driver-instance/completed-run-commit.repository.ts new file mode 100644 index 00000000..90e84470 --- /dev/null +++ b/apps/api/src/modules/runtime/infrastructure/driver-instance/completed-run-commit.repository.ts @@ -0,0 +1,2864 @@ +import type { AgentKind } from "@mosoo/contracts/agent"; +import type { SessionStatus } from "@mosoo/contracts/session"; +import { DurableRunError } from "@mosoo/contracts/session-run"; +import type { RunError, SessionRunStatus } from "@mosoo/contracts/session-run"; +import { parseSchemaValue } from "@mosoo/contracts/validation"; +import { createPlatformId } from "@mosoo/id"; +import type { + DriverInstanceId, + PlatformId, + RuntimeEventId, + RuntimeOperationId, + SessionId, + SessionRunId, +} from "@mosoo/id"; +import { + createSessionRunTerminalSourceId, + createRuntimeEventSemanticHash, + readRuntimeEventPayload, + readRuntimeEventString, + readRuntimeRunPayload, + stringifyRuntimeEventSemanticValue, +} from "@mosoo/runtime-events"; +import type { RuntimeRunView } from "@mosoo/runtime-events"; + +import { getD1ChangeCount } from "../../../../platform/db/drizzle"; +import { currentTimestampMs, toIsoString } from "../../../../time"; +import { createSessionRuntimeEventProjection } from "../../../sessions/domain/session-runtime-event-projection"; +import { readTerminalEventSemanticAuthority } from "../../../sessions/domain/session-terminal-event-authority"; +import type { SessionRuntimeEventInput } from "../../../sessions/infrastructure/session-runtime-event-store.repository"; +import { decideSessionRunTransition } from "../../domain/session-run-lifecycle.machine"; +import { + prepareAssistantMessageProjection, + prepareToolCarrierProjection, +} from "./assistant-message-projection"; +import type { PreparedAssistantMessageProjection } from "./assistant-message-projection"; +import { prepareRuntimeArtifactPromotion } from "./runtime-artifact-attempt.repository"; + +interface CompletionStateRow { + archived_at: number | null; + cleanup_operation_kind: "archive" | "delete" | null; + completed_at: number | null; + created_at: number; + error_code: string | null; + error_details_json: string | null; + error_message: string | null; + error_retryable: number | null; + driver_last_heartbeat_at: number | null; + driver_connection_id: string | null; + driver_generation: number | null; + driver_status: string | null; + driver_status_operation_id: string | null; + driver_updated_at: number | null; + last_run_id: SessionRunId | null; + message_seq_cursor: number; + tool_carrier_anchor_json: string | null; + permission_request_count: number; + run_status: SessionRunStatus; + run_status_operation_id: string | null; + run_status_seq: number; + run_status_source: string; + run_trace_id: string; + run_updated_at: number; + run_created_by_account_id: PlatformId; + run_driver_instance_id: DriverInstanceId | null; + runtime_event_seq_cursor: number; + session_kind: AgentKind; + session_status: SessionStatus; + session_status_operation_id: string | null; + session_status_seq: number; + session_updated_at: number; + started_at: number | null; + final_message_artifact_json: string | null; + unfinalized_model_call_count: number; +} + +interface PersistedAssistantMessageRow { + content_text: string; + created_at: number; + created_by_account_id: string; + id: string; + plan_json: string | null; + projection_format: "event_stream_v3" | "materialized"; + segments_json: string | null; + seq: number; + session_id: string; + session_run_id: string; +} + +interface PreparedTerminalAssistantMessage { + createdAt: number; + firstEventSeq: number; + message: PreparedAssistantMessageProjection; +} + +interface GuardedAssistantMessage { + contentText: string; + createdAt: number; + createdByAccountId: string; + id: string; + planJson: string | null; + projectionFormat: "event_stream_v3" | "materialized"; + segmentsJson: string | null; + seq: number; + sessionId: SessionId; +} + +interface TranscriptAnchor { + occurredAt: number; + seq: number; +} + +interface PersistedTerminalEventRow { + artifact_attempt_id: string | null; + artifact_manifest_json: string | null; + artifact_manifest_sha256: string | null; + created_at: number; + ended_at: number | null; + event_type: string; + family: string; + id: RuntimeEventId; + occurred_at: number; + process_status: string; + process_type: string; + run_id: string | null; + semantic_hash: string | null; + terminal_event_json: string | null; + seq: number; + session_id: string; + source: string; + source_event_id: string; + stream_id: string | null; + tool_call_id: string | null; + tool_input_json: string | null; + tool_name: string | null; + tokens: number | null; + trace_id: string | null; + visibility: string; +} + +interface PersistedTerminalAuthorityRow extends PersistedTerminalEventRow { + agent_id: string; + content_text: string; + mcp_command_id: string | null; + tool_input_delta_json: string | null; + tool_output_delta_text: string | null; + tool_output_text: string | null; + tool_parent_message_id: string | null; + tool_result_message_id: string | null; + tool_status: string | null; +} + +interface CompletionSnapshot { + assistantMessages: PersistedAssistantMessageRow[]; + state: CompletionStateRow | null; + terminalEvents: PersistedTerminalEventRow[]; +} + +export type CompletedRunCommitResult = + | { + kind: "applied" | "duplicate"; + persistedSourceEventIds: readonly string[]; + runDurationMs: number | null; + } + | { + currentStatus: SessionRunStatus; + kind: "stale"; + persistedSourceEventIds: readonly []; + runDurationMs: null; + }; + +export type AdoptTerminalRunProjectionResult = + | CompletedRunCommitResult + | { + kind: "missing"; + persistedSourceEventIds: readonly []; + runDurationMs: null; + }; + +export type DriverTerminalRunStatus = "cancelled" | "completed" | "failed"; +export type HostTerminalRunStatus = DriverTerminalRunStatus | "expired"; +export type TerminalRunProjectionSource = + | "api" + | "driver" + | "maintenance" + | "runtime_operation" + | "system" + | "viewer"; + +export interface ExpectedTerminalSessionObservation { + lastRunId: SessionRunId | null; + status: SessionStatus; + statusSeq: number; + updatedAt: number; +} + +export interface ExpectedTerminalDriverObservation { + connectionId?: string | null; + driverInstanceId: DriverInstanceId | null; + generation?: number; + lastHeartbeatAt?: number | null; + status?: string | null; + updatedAt?: number | null; +} + +interface AtomicTerminalDriverReleaseObservation { + readonly connectionId: string | null; + readonly driverInstanceId: DriverInstanceId; + readonly generation: number; +} + +function readAtomicTerminalDriverReleaseObservation( + observation: ExpectedTerminalDriverObservation | undefined, +): AtomicTerminalDriverReleaseObservation | null { + if ( + observation?.driverInstanceId === null || + observation?.driverInstanceId === undefined || + observation.connectionId === undefined || + observation.generation === undefined + ) { + return null; + } + + return { + connectionId: observation.connectionId, + driverInstanceId: observation.driverInstanceId, + generation: observation.generation, + }; +} + +function expectedDriverObservationMatchesState( + state: CompletionStateRow, + observation: ExpectedTerminalDriverObservation, +): boolean { + return ( + state.run_driver_instance_id === observation.driverInstanceId && + (observation.connectionId === undefined || + state.driver_connection_id === observation.connectionId) && + (observation.generation === undefined || state.driver_generation === observation.generation) && + (observation.status === undefined || state.driver_status === observation.status) && + (observation.updatedAt === undefined || state.driver_updated_at === observation.updatedAt) && + (observation.lastHeartbeatAt === undefined || + state.driver_last_heartbeat_at === observation.lastHeartbeatAt) + ); +} + +function terminalEventKind( + status: HostTerminalRunStatus, +): "run.cancelled" | "run.completed" | "run.failed" { + return status === "expired" ? "run.cancelled" : `run.${status}`; +} + +function terminalLifecycleEvent(status: HostTerminalRunStatus) { + switch (status) { + case "cancelled": + return "run.cancel"; + case "completed": + return "run.complete"; + case "failed": + return "run.fail"; + case "expired": + return "run.expire"; + } +} + +function readTerminalRunOperationId(input: { + expectedSessionOperationId?: RuntimeOperationId | null; + source: TerminalRunProjectionSource; +}): RuntimeOperationId | null { + if (input.source !== "runtime_operation") { + return null; + } + if (input.expectedSessionOperationId === undefined || input.expectedSessionOperationId === null) { + throw new Error("A runtime-operation terminal transition requires its operation identity."); + } + return input.expectedSessionOperationId; +} + +interface PreparedTerminalEvent { + artifactAttemptId: string | null; + artifactManifestJson: string | null; + artifactManifestSha256: string | null; + contentText: string; + createdAt: number; + endedAt: number; + eventType: string; + explicitRunView: RuntimeRunView | null; + family: string; + id: RuntimeEventId; + occurredAt: number; + processStatus: string; + processType: string; + semanticHash: string; + terminalEventJson: string; + sessionStatus: Extract; + source: string; + sourceEventId: string; + streamId: string | null; + toolCallId: string | null; + toolInputJson: string | null; + toolName: string | null; + tokens: number | null; + traceId: string | null; + visibility: string; +} + +async function readTerminalAuthorityReceipt( + database: D1Database, + runId: SessionRunId, +): Promise { + const { results } = await database + .prepare( + `SELECT agent_id, artifact_attempt_id, artifact_manifest_json, + artifact_manifest_sha256, content_text, created_at, ended_at, event_type, + family, id, mcp_command_id, occurred_at, process_status, process_type, run_id, + semantic_hash, terminal_event_json, seq, session_id, source, source_event_id, + stream_id, tool_call_id, + tool_input_delta_json, tool_input_json, tool_name, tool_output_delta_text, + tool_output_text, tool_parent_message_id, tool_result_message_id, tool_status, + tokens, trace_id, visibility + FROM session_event + WHERE run_id = ? + AND event_type IN ('run.cancelled', 'run.completed', 'run.failed') + ORDER BY seq`, + ) + .bind(runId) + .all(); + + if (results.length > 1) { + throw new Error(`Session run ${runId} has conflicting durable terminal events.`); + } + + return results[0] ?? null; +} + +const PERSISTED_TERMINAL_EVENT_KEYS = [ + "artifact_attempt_id", + "artifact_manifest_json", + "artifact_manifest_sha256", + "created_at", + "ended_at", + "event_type", + "family", + "id", + "occurred_at", + "process_status", + "process_type", + "run_id", + "semantic_hash", + "terminal_event_json", + "seq", + "session_id", + "source", + "source_event_id", + "stream_id", + "tool_call_id", + "tool_input_json", + "tool_name", + "tokens", + "trace_id", + "visibility", +] as const satisfies readonly (keyof PersistedTerminalEventRow)[]; + +const PERSISTED_TERMINAL_AUTHORITY_KEYS = [ + ...PERSISTED_TERMINAL_EVENT_KEYS, + "agent_id", + "content_text", + "mcp_command_id", + "tool_input_delta_json", + "tool_output_delta_text", + "tool_output_text", + "tool_parent_message_id", + "tool_result_message_id", + "tool_status", +] as const satisfies readonly (keyof PersistedTerminalAuthorityRow)[]; + +function hasSameTerminalAuthority( + left: PersistedTerminalAuthorityRow, + right: PersistedTerminalAuthorityRow, +): boolean { + return PERSISTED_TERMINAL_AUTHORITY_KEYS.every((key) => left[key] === right[key]); +} + +function assertSnapshotTerminalAuthority( + snapshot: CompletionSnapshot, + expected: PersistedTerminalAuthorityRow, + runId: SessionRunId, +): void { + const [actual] = snapshot.terminalEvents; + if ( + snapshot.terminalEvents.length !== 1 || + actual === undefined || + PERSISTED_TERMINAL_EVENT_KEYS.some((key) => actual[key] !== expected[key]) + ) { + throw new Error(`Session run ${runId} terminal authority changed during adoption.`); + } +} + +async function assertDurableTerminalAuthority( + database: D1Database, + expected: PersistedTerminalAuthorityRow, + runId: SessionRunId, +): Promise { + const actual = await readTerminalAuthorityReceipt(database, runId); + if (actual === null || !hasSameTerminalAuthority(actual, expected)) { + throw new Error(`Session run ${runId} terminal authority changed during adoption.`); + } +} + +async function readCompletionSnapshot( + database: D1Database, + input: { + finalMessageId: string | null; + runId: SessionRunId; + sessionId: SessionId; + sourceEventId: string; + }, +): Promise { + const [state, assistantMessages, terminalEvents] = await Promise.all([ + database + .prepare( + `SELECT + s.archived_at, + s.cleanup_operation_kind, + r.completed_at, + r.created_at, + r.created_by_account_id AS run_created_by_account_id, + r.error_code, + r.error_details_json, + r.error_message, + r.error_retryable, + driver.connection_id AS driver_connection_id, + driver.generation AS driver_generation, + driver.last_heartbeat_at AS driver_last_heartbeat_at, + driver.status AS driver_status, + driver.status_operation_id AS driver_status_operation_id, + driver.updated_at AS driver_updated_at, + s.last_run_id, + s.message_seq_cursor, + ( + SELECT json_object('occurredAt', output.occurred_at, 'seq', output.seq) + FROM session_event AS output + WHERE output.session_id = s.id + AND output.run_id = r.id + AND output.event_type = 'tool.call.updated' + AND output.visibility = 'all_consumers' + AND output.tool_call_id IS NOT NULL + AND output.seq < COALESCE( + ( + SELECT terminal.seq + FROM session_event AS terminal + WHERE terminal.session_id = s.id + AND terminal.run_id = r.id + AND terminal.event_type IN ('run.cancelled', 'run.completed', 'run.failed') + ORDER BY terminal.seq + LIMIT 1 + ), + s.runtime_event_seq_cursor + 1 + ) + AND ( + ( + output.tool_parent_message_id IS NOT NULL + AND (? IS NULL OR output.tool_parent_message_id <> ?) + ) + OR ( + ( + output.tool_output_delta_text IS NOT NULL + OR output.tool_output_text IS NOT NULL + ) + AND NOT EXISTS ( + SELECT 1 + FROM session_event AS identity + WHERE identity.session_id = output.session_id + AND identity.run_id = output.run_id + AND identity.event_type = 'tool.call.updated' + AND identity.visibility = 'all_consumers' + AND identity.tool_call_id = output.tool_call_id + AND identity.tool_parent_message_id IS NOT NULL + AND identity.seq <= output.seq + ) + ) + ) + ORDER BY output.seq + LIMIT 1 + ) AS tool_carrier_anchor_json, + ( + SELECT COUNT(*) + FROM session_permission_request AS permission + WHERE permission.session_id = r.session_id AND permission.run_id = r.id + ) AS permission_request_count, + r.status AS run_status, + r.status_operation_id AS run_status_operation_id, + r.status_seq AS run_status_seq, + r.status_source AS run_status_source, + r.trace_id AS run_trace_id, + r.updated_at AS run_updated_at, + r.driver_instance_id AS run_driver_instance_id, + ( + SELECT COUNT(*) + FROM session_model_call AS model_call + WHERE model_call.session_id = r.session_id + AND model_call.session_run_id = r.id + AND ( + model_call.completed_at IS NULL + OR model_call.status <> CASE + WHEN r.status = 'completed' THEN 'completed' + ELSE 'failed' + END + ) + ) AS unfinalized_model_call_count, + s.runtime_event_seq_cursor, + s.kind AS session_kind, + s.status AS session_status, + s.status_operation_id AS session_status_operation_id, + s.status_seq AS session_status_seq, + s.updated_at AS session_updated_at, + r.started_at, + ( + SELECT json_object('occurredAt', message.occurred_at, 'seq', message.seq) + FROM session_event AS message + WHERE ? IS NOT NULL + AND message.session_id = s.id + AND message.run_id = r.id + AND message.stream_id = ? + AND message.process_type = 'agent.message.delta' + AND message.visibility = 'all_consumers' + AND message.event_type = 'message.added' + AND message.seq < COALESCE( + ( + SELECT terminal.seq + FROM session_event AS terminal + WHERE terminal.session_id = s.id + AND terminal.run_id = r.id + AND terminal.event_type IN ('run.cancelled', 'run.completed', 'run.failed') + ORDER BY terminal.seq + LIMIT 1 + ), + s.runtime_event_seq_cursor + 1 + ) + ORDER BY message.seq DESC + LIMIT 1 + ) AS final_message_artifact_json + FROM session_run AS r + INNER JOIN session AS s ON s.id = r.session_id + LEFT JOIN driver_instance AS driver ON driver.id = r.driver_instance_id + WHERE r.id = ? AND r.session_id = ? + LIMIT 1`, + ) + .bind( + input.finalMessageId, + input.finalMessageId, + input.finalMessageId, + input.finalMessageId, + input.runId, + input.sessionId, + ) + .first(), + database + .prepare( + `SELECT content_text, created_at, created_by_account_id, id, plan_json, projection_format, + segments_json, seq, session_id, session_run_id + FROM session_message + WHERE session_run_id = ? AND role = 'assistant' + ORDER BY seq`, + ) + .bind(input.runId) + .all(), + database + .prepare( + `SELECT artifact_attempt_id, artifact_manifest_json, artifact_manifest_sha256, + created_at, ended_at, event_type, family, id, occurred_at, + process_status, process_type, run_id, semantic_hash, terminal_event_json, seq, + session_id, source, source_event_id, stream_id, tool_call_id, tool_input_json, + tool_name, tokens, trace_id, visibility + FROM session_event + WHERE session_id = ? + AND ( + source_event_id = ? + OR (run_id = ? AND event_type IN ('run.cancelled', 'run.completed', 'run.failed')) + ) + ORDER BY seq`, + ) + .bind(input.sessionId, input.sourceEventId, input.runId) + .all(), + ]); + + return { + assistantMessages: assistantMessages.results, + state, + terminalEvents: terminalEvents.results, + }; +} + +function parseTranscriptAnchor(value: string | null, label: string): TranscriptAnchor | null { + if (value === null) { + return null; + } + const parsed: unknown = JSON.parse(value); + if ( + typeof parsed !== "object" || + parsed === null || + !("occurredAt" in parsed) || + typeof parsed.occurredAt !== "number" || + !("seq" in parsed) || + typeof parsed.seq !== "number" + ) { + throw new Error(`Canonical ${label} transcript anchor is invalid.`); + } + return { occurredAt: parsed.occurredAt, seq: parsed.seq }; +} + +function prepareTerminalAssistantMessages( + snapshot: CompletionSnapshot, + input: { + finalMessage: PreparedAssistantMessageProjection | null; + runId: SessionRunId; + sessionId: SessionId; + }, +): PreparedTerminalAssistantMessage[] { + const { state } = snapshot; + if (state === null) { + return []; + } + const finalArtifact = parseTranscriptAnchor(state.final_message_artifact_json, "final message"); + const toolArtifact = parseTranscriptAnchor(state.tool_carrier_anchor_json, "tool carrier"); + + if (input.finalMessage !== null && finalArtifact === null) { + throw new Error( + `Canonical final assistant ${input.finalMessage.id} has no pre-terminal message stream.`, + ); + } + + const final = + input.finalMessage === null || finalArtifact === null + ? null + : { + createdAt: finalArtifact.occurredAt, + firstEventSeq: finalArtifact.seq, + message: input.finalMessage, + }; + const carrier = + toolArtifact === null + ? null + : { + createdAt: toolArtifact.occurredAt, + firstEventSeq: toolArtifact.seq, + message: prepareToolCarrierProjection({ + createdByAccountId: state.run_created_by_account_id, + sessionId: input.sessionId, + sessionRunId: input.runId, + }), + }; + + if (final !== null && carrier !== null && final.message.id === carrier.message.id) { + return [ + final.firstEventSeq <= carrier.firstEventSeq + ? final + : { + ...final, + createdAt: carrier.createdAt, + firstEventSeq: carrier.firstEventSeq, + }, + ]; + } + + return [final, carrier] + .filter((message): message is PreparedTerminalAssistantMessage => message !== null) + .toSorted((left, right) => left.firstEventSeq - right.firstEventSeq); +} + +function isCanonicalAssistantMessage( + row: PersistedAssistantMessageRow, + expected: PreparedTerminalAssistantMessage, +): boolean { + return ( + row.content_text === expected.message.contentText && + row.created_at === expected.createdAt && + row.created_by_account_id === expected.message.createdByAccountId && + row.id === expected.message.id && + row.plan_json === expected.message.planJson && + row.projection_format === expected.message.projectionFormat && + row.segments_json === expected.message.segmentsJson && + row.session_id === expected.message.sessionId && + row.session_run_id === expected.message.sessionRunId + ); +} + +function assertCanonicalAssistantMessages( + rows: readonly PersistedAssistantMessageRow[], + expected: readonly PreparedTerminalAssistantMessage[], + runId: SessionRunId, +): void { + if ( + rows.length === expected.length && + rows.every((row, index) => { + const message = expected[index]; + return message !== undefined && isCanonicalAssistantMessage(row, message); + }) + ) { + return; + } + + throw new Error( + `Canonical assistant messages conflict with the persisted projection for run ${runId}.`, + ); +} + +function assertRepairableAssistantMessages( + rows: readonly PersistedAssistantMessageRow[], + expected: readonly PreparedTerminalAssistantMessage[], + runId: SessionRunId, +): void { + if ( + rows.length <= expected.length && + rows.every((row, index) => { + const message = expected[index]; + return message !== undefined && isCanonicalAssistantMessage(row, message); + }) + ) { + return; + } + + throw new Error(`Canonical assistant messages are not safely repairable for run ${runId}.`); +} + +function assertCanonicalTerminalRunError( + state: CompletionStateRow, + expected: RunError | null, + runId: SessionRunId, +): void { + const expectedDetails = + expected === null ? null : stringifyRuntimeEventSemanticValue(expected.details); + const persistedDetails = + state.error_details_json === null + ? null + : stringifyRuntimeEventSemanticValue(JSON.parse(state.error_details_json)); + + if ( + state.error_code !== (expected?.code ?? null) || + persistedDetails !== expectedDetails || + state.error_message !== (expected?.message ?? null) || + state.error_retryable !== (expected === null ? null : Number(expected.retryable)) + ) { + throw new Error(`Canonical terminal error conflicts with the persisted run ${runId}.`); + } +} + +function assertCanonicalTerminalEvent( + rows: readonly PersistedTerminalEventRow[], + expected: { + eventType: ReturnType; + runId: SessionRunId; + sourceEventId: string; + semanticHash: string; + }, +): PersistedTerminalEventRow { + const [row] = rows; + + if ( + rows.length !== 1 || + row === undefined || + row.event_type !== expected.eventType || + row.run_id !== expected.runId || + row.semantic_hash !== expected.semanticHash || + row.source_event_id !== expected.sourceEventId + ) { + throw new Error( + `Canonical ${expected.eventType} receipt conflicts with the persisted projection for run ${expected.runId}.`, + ); + } + + return row; +} + +function readRunDurationMs(state: CompletionStateRow): number | null { + if (state.completed_at === null) { + return null; + } + + return Math.max(0, state.completed_at - (state.started_at ?? state.created_at)); +} + +function projectedSessionOperationId( + state: CompletionStateRow, + sessionStatus: PreparedTerminalEvent["sessionStatus"], +): string | null { + return sessionStatus === "IDLE" ? state.session_status_operation_id : null; +} + +function isLegacyTerminalProjection(snapshot: CompletionSnapshot): boolean { + return snapshot.terminalEvents.length === 1 && snapshot.terminalEvents[0]?.semantic_hash === null; +} + +function assertLegacyAssistantMessages( + rows: readonly PersistedAssistantMessageRow[], + expected: readonly PreparedTerminalAssistantMessage[], + runId: SessionRunId, + sessionId: SessionId, +): void { + const materialized = rows.filter((row) => row.projection_format === "materialized"); + const carriers = rows.filter((row) => row.projection_format === "event_stream_v3"); + const expectedCarrier = expected.find(({ message }) => String(message.id) === String(runId)); + if ( + materialized.length > 1 || + carriers.length > 1 || + materialized.length + carriers.length !== rows.length || + rows.some((row) => row.session_id !== sessionId || row.session_run_id !== runId) || + carriers.some( + (row) => + expectedCarrier === undefined || + row.id !== runId || + !isCanonicalAssistantMessage(row, expectedCarrier), + ) + ) { + throw new Error(`Legacy terminal assistant messages conflict with run ${runId}.`); + } +} + +function staleTerminalResult(state: CompletionStateRow): CompletedRunCommitResult { + return { + currentStatus: state.run_status, + kind: "stale", + persistedSourceEventIds: [], + runDurationMs: null, + }; +} + +function hasSafelyRepairableStaleSessionProjection( + state: CompletionStateRow, + input: { runId: SessionRunId }, +): boolean { + return ( + state.archived_at === null && + state.cleanup_operation_kind === null && + state.last_run_id === input.runId && + state.session_status === "RUNNING" && + state.session_status_operation_id === null && + state.session_updated_at <= state.run_updated_at + ); +} + +function explicitRunViewMatchesFreshState( + state: CompletionStateRow, + input: { + createdAt: number; + error: RunError | null; + explicitRunView: RuntimeRunView | null; + runId: SessionRunId; + targetStatus: HostTerminalRunStatus; + }, +): boolean { + const run = input.explicitRunView; + if (run === null) { + return true; + } + + return ( + run.completedAt === toIsoString(state.completed_at ?? input.createdAt) && + run.id === input.runId && + run.startedAt === toIsoString(state.started_at ?? input.createdAt) && + run.status === input.targetStatus && + run.traceId === state.run_trace_id && + stringifyRuntimeEventSemanticValue(run.error) === + stringifyRuntimeEventSemanticValue(input.error) + ); +} + +function classifyTerminalSnapshot( + snapshot: CompletionSnapshot, + input: { + assistantMessage: PreparedAssistantMessageProjection | null; + assistantMessages: readonly PreparedTerminalAssistantMessage[]; + createdAt: number; + error: RunError | null; + explicitRunView: RuntimeRunView | null; + expectedDriverObservation?: ExpectedTerminalDriverObservation; + expectedRunStatus?: SessionRunStatus; + expectedSessionObservation?: ExpectedTerminalSessionObservation; + expectedSessionOperationId?: RuntimeOperationId | null; + eventId: RuntimeEventId; + runId: SessionRunId; + runStatusOperationId: RuntimeOperationId | null; + semanticHash: string; + sessionId: SessionId; + sessionStatus: PreparedTerminalEvent["sessionStatus"]; + sourceEventId: string; + source: TerminalRunProjectionSource; + targetStatus: HostTerminalRunStatus; + }, +): CompletedRunCommitResult | null { + const { state } = snapshot; + + if (state === null) { + throw new Error(`Session run ${input.runId} was not found for atomic completion.`); + } + const terminalDriverRelease = readAtomicTerminalDriverReleaseObservation( + input.expectedDriverObservation, + ); + if (state.run_status === input.targetStatus) { + if ( + input.expectedDriverObservation !== undefined && + !expectedDriverObservationMatchesState(state, input.expectedDriverObservation) + ) { + return staleTerminalResult(state); + } + if ( + terminalDriverRelease !== null && + state.driver_status_operation_id !== null && + state.driver_status_operation_id !== input.runId && + state.driver_status !== "stopping" + ) { + return staleTerminalResult(state); + } + if (isLegacyTerminalProjection(snapshot)) { + if (input.assistantMessage !== null) { + throw new Error( + "A legacy materialized terminal run cannot adopt an event-stream reference.", + ); + } + assertLegacyAssistantMessages( + snapshot.assistantMessages, + input.assistantMessages, + input.runId, + input.sessionId, + ); + assertCanonicalTerminalRunError(state, input.error, input.runId); + const [legacyEvent] = snapshot.terminalEvents; + if ( + legacyEvent === undefined || + legacyEvent.event_type !== terminalEventKind(input.targetStatus) || + legacyEvent.run_id !== input.runId || + legacyEvent.source_event_id !== + createSessionRunTerminalSourceId(input.runId, terminalEventKind(input.targetStatus)) + ) { + throw new Error(`Legacy terminal projection conflicts with run ${input.runId}.`); + } + const cursorCovered = + legacyEvent.seq <= state.runtime_event_seq_cursor && + snapshot.assistantMessages.every((message) => message.seq <= state.message_seq_cursor); + if ( + cursorCovered && + state.permission_request_count === 0 && + state.unfinalized_model_call_count === 0 + ) { + return { + kind: "duplicate", + persistedSourceEventIds: [], + runDurationMs: readRunDurationMs(state), + }; + } + if ( + state.archived_at !== null || + state.last_run_id !== input.runId || + state.session_status_operation_id !== null || + (state.session_status !== "IDLE" && state.session_status !== "TERMINATED") + ) { + throw new Error( + `Legacy terminal projection for run ${input.runId} is not safely repairable.`, + ); + } + return null; + } + if ( + state.run_status_operation_id !== input.runStatusOperationId || + state.run_status_source !== input.source + ) { + return staleTerminalResult(state); + } + if (!explicitRunViewMatchesFreshState(state, input)) { + return staleTerminalResult(state); + } + assertCanonicalTerminalRunError(state, input.error, input.runId); + assertRepairableAssistantMessages( + snapshot.assistantMessages, + input.assistantMessages, + input.runId, + ); + + if (snapshot.terminalEvents.length === 0) { + if ( + state.archived_at !== null || + state.last_run_id !== input.runId || + (state.session_status !== "RUNNING" && state.session_status !== input.sessionStatus) || + state.session_status_operation_id !== null || + state.session_updated_at > state.run_updated_at + ) { + throw new Error( + `Missing terminal projection for run ${input.runId} is not safely repairable.`, + ); + } + return null; + } + + const hasMissingAssistantMessages = + snapshot.assistantMessages.length < input.assistantMessages.length; + if (!hasMissingAssistantMessages) { + assertCanonicalAssistantMessages( + snapshot.assistantMessages, + input.assistantMessages, + input.runId, + ); + } + const terminalEvent = assertCanonicalTerminalEvent(snapshot.terminalEvents, { + ...input, + eventType: terminalEventKind(input.targetStatus), + semanticHash: input.semanticHash, + }); + if (terminalEvent.seq > state.runtime_event_seq_cursor) { + throw new Error( + `Canonical terminal event is ahead of the Session cursor for run ${input.runId}.`, + ); + } + if ( + hasMissingAssistantMessages || + snapshot.assistantMessages.some((message) => message.seq > state.message_seq_cursor) || + state.permission_request_count !== 0 + ) { + return null; + } + + if (state.unfinalized_model_call_count > 0) { + return null; + } + + if (hasSafelyRepairableStaleSessionProjection(state, input)) { + return null; + } + + return { + kind: terminalEvent.id === input.eventId ? "applied" : "duplicate", + persistedSourceEventIds: terminalEvent.id === input.eventId ? [input.sourceEventId] : [], + runDurationMs: readRunDurationMs(state), + }; + } + + if (["cancelled", "completed", "expired", "failed"].includes(state.run_status)) { + return staleTerminalResult(state); + } + + if (input.expectedRunStatus !== undefined && state.run_status !== input.expectedRunStatus) { + return staleTerminalResult(state); + } + if ( + input.expectedSessionObservation !== undefined && + (state.last_run_id !== input.expectedSessionObservation.lastRunId || + state.session_status !== input.expectedSessionObservation.status || + state.session_status_seq !== input.expectedSessionObservation.statusSeq || + state.session_updated_at !== input.expectedSessionObservation.updatedAt) + ) { + return staleTerminalResult(state); + } + if ( + input.expectedDriverObservation !== undefined && + (!expectedDriverObservationMatchesState(state, input.expectedDriverObservation) || + (terminalDriverRelease !== null && + state.driver_status_operation_id !== null && + state.driver_status !== "stopping")) + ) { + return staleTerminalResult(state); + } + + if ( + input.expectedSessionOperationId !== undefined && + state.session_status_operation_id !== input.expectedSessionOperationId + ) { + return staleTerminalResult(state); + } + if (!explicitRunViewMatchesFreshState(state, input)) { + return staleTerminalResult(state); + } + + return null; +} + +async function prepareTerminalEvent( + record: SessionRuntimeEventInput, + input: { + assistantMessage: PreparedAssistantMessageProjection | null; + error: RunError | null; + runId: SessionRunId; + sessionId: SessionId; + targetStatus: HostTerminalRunStatus; + timestampMs?: number; + }, +): Promise { + const expectedKind = terminalEventKind(input.targetStatus); + const expectedSourceEventId = createSessionRunTerminalSourceId(input.runId, expectedKind); + + if ( + record.event.kind !== expectedKind || + record.event.runId !== input.runId || + record.event.sessionId !== input.sessionId || + record.sourceEventId !== expectedSourceEventId || + record.event.sourceEventId !== expectedSourceEventId + ) { + throw new Error( + `Atomic run terminal projection requires one exact canonical ${expectedKind} identity.`, + ); + } + + const eventPayload = readRuntimeEventPayload(record.event); + const runtimeRunPayload = readRuntimeRunPayload(record.event); + const runPayload = runtimeRunPayload.run; + if ( + runPayload === null || + runPayload.id !== input.runId || + runPayload.status !== input.targetStatus + ) { + throw new Error(`Atomic ${expectedKind} payload does not match its terminal run identity.`); + } + if (runtimeRunPayload.lifecycle !== "IDLE" && runtimeRunPayload.lifecycle !== "TERMINATED") { + throw new Error(`Atomic ${expectedKind} projection requires a terminal Session lifecycle.`); + } + if (input.targetStatus === "failed") { + if (input.error === null || runPayload.error === null) { + throw new Error("Atomic run.failed projection requires one exact RunError."); + } + if ( + runPayload.error.code !== input.error.code || + stringifyRuntimeEventSemanticValue(runPayload.error.details) !== + stringifyRuntimeEventSemanticValue(input.error.details) || + runPayload.error.message !== input.error.message || + runPayload.error.retryable !== input.error.retryable + ) { + throw new Error("Atomic run.failed payload conflicts with its persisted RunError."); + } + } else if (input.targetStatus === "completed") { + if (input.error !== null || runPayload.error !== null) { + throw new Error(`Atomic ${expectedKind} projection cannot persist a RunError.`); + } + } else if ((input.error === null) !== (runPayload.error === null)) { + throw new Error(`Atomic ${expectedKind} RunError must be present together.`); + } else if ( + input.error !== null && + runPayload.error !== null && + (runPayload.error.code !== input.error.code || + stringifyRuntimeEventSemanticValue(runPayload.error.details) !== + stringifyRuntimeEventSemanticValue(input.error.details) || + runPayload.error.message !== input.error.message || + runPayload.error.retryable !== input.error.retryable) + ) { + throw new Error(`Atomic ${expectedKind} projection cannot persist a RunError.`); + } + + const finalMessageId = readRuntimeEventString(eventPayload, "finalMessageId"); + if ( + input.targetStatus === "completed" && + (finalMessageId === null) !== (input.assistantMessage === null) + ) { + throw new Error("run.completed final message identity and reference must be present together."); + } + if (input.assistantMessage !== null) { + if ( + input.targetStatus !== "completed" || + input.assistantMessage.sessionId !== input.sessionId || + input.assistantMessage.sessionRunId !== input.runId || + input.assistantMessage.id !== finalMessageId + ) { + throw new Error("Atomic final assistant reference conflicts with run.completed."); + } + } + + const timestampMs = input.timestampMs ?? currentTimestampMs(); + const occurredAt = record.occurredAt ?? timestampMs; + const parsedEndedAt = Date.parse(record.event.occurredAt); + const projection = createSessionRuntimeEventProjection(record.event); + const artifactAttemptId = record.artifactAttemptId ?? null; + const artifactManifestJson = record.artifactManifestJson ?? null; + const artifactManifestSha256 = record.artifactManifestSha256 ?? null; + if ( + (artifactAttemptId === null) !== (artifactManifestJson === null) || + (artifactAttemptId === null) !== (artifactManifestSha256 === null) || + (artifactAttemptId !== null && expectedKind !== "run.completed") + ) { + throw new Error("Atomic terminal artifact projection is incomplete or out of scope."); + } + + if ( + projection.eventType !== expectedKind || + projection.runId !== input.runId || + projection.visibility !== "all_consumers" + ) { + throw new Error("Atomic run terminal event projection is not public and run-scoped."); + } + + return { + artifactAttemptId, + artifactManifestJson, + artifactManifestSha256, + contentText: projection.contentText, + createdAt: timestampMs, + endedAt: + Number.isFinite(parsedEndedAt) && parsedEndedAt >= occurredAt ? parsedEndedAt : occurredAt, + eventType: projection.eventType, + explicitRunView: Object.hasOwn(eventPayload, "run") ? runPayload : null, + family: projection.family, + id: createPlatformId(), + occurredAt, + processStatus: projection.processStatus, + processType: projection.processType, + semanticHash: await createRuntimeEventSemanticHash(record.event), + terminalEventJson: stringifyRuntimeEventSemanticValue(record.event), + sessionStatus: runtimeRunPayload.lifecycle, + source: projection.source, + sourceEventId: expectedSourceEventId, + streamId: projection.streamId, + toolCallId: projection.toolCallId, + toolInputJson: projection.toolInputJson, + toolName: projection.toolName, + tokens: projection.tokens, + traceId: projection.traceId, + visibility: projection.visibility, + }; +} + +function prepareTerminalEventInsert( + database: D1Database, + input: { + assistantMessageCount: number; + expectedDriverObservation?: ExpectedTerminalDriverObservation; + event: PreparedTerminalEvent; + runId: SessionRunId; + sessionId: SessionId; + state: CompletionStateRow; + }, +): D1PreparedStatement { + const { event, state } = input; + + return database + .prepare( + `/* completed-run:event */ + INSERT INTO session_event ( + agent_id, artifact_attempt_id, artifact_manifest_json, artifact_manifest_sha256, + content_text, created_at, ended_at, event_type, family, id, + occurred_at, process_status, process_type, run_id, semantic_hash, terminal_event_json, + seq, session_id, source_event_id, source, stream_id, tool_call_id, tool_input_json, + tool_name, tokens, trace_id, visibility + ) + SELECT + s.agent_id, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, r.id, ?, ?, + s.runtime_event_seq_cursor + 1, s.id, ?, ?, ?, ?, ?, ?, ?, ?, ? + FROM session AS s + INNER JOIN session_run AS r ON r.session_id = s.id + WHERE s.id = ? + AND r.id = ? + AND r.status = ? + AND r.status_seq = ? + AND r.status_operation_id IS ? + AND r.status_source = ? + AND (? = 0 OR r.driver_instance_id IS ?) + AND ( + ? = 0 OR ( + SELECT driver.connection_id FROM driver_instance AS driver + WHERE driver.id = r.driver_instance_id + ) IS ? + ) + AND ( + ? = 0 OR ( + SELECT driver.generation FROM driver_instance AS driver + WHERE driver.id = r.driver_instance_id + ) IS ? + ) + AND ( + ? = 0 OR ( + SELECT driver.status FROM driver_instance AS driver + WHERE driver.id = r.driver_instance_id + ) IS ? + ) + AND ( + ? = 0 OR ( + SELECT driver.updated_at FROM driver_instance AS driver + WHERE driver.id = r.driver_instance_id + ) IS ? + ) + AND ( + ? = 0 OR ( + SELECT driver.last_heartbeat_at FROM driver_instance AS driver + WHERE driver.id = r.driver_instance_id + ) IS ? + ) + AND s.archived_at IS ? + AND s.cleanup_operation_kind IS ? + AND s.last_run_id IS ? + AND s.status = ? + AND s.status_operation_id IS ? + AND s.status_seq = ? + AND s.updated_at = ? + AND s.message_seq_cursor = ? + AND s.runtime_event_seq_cursor = ? + AND ( + SELECT COUNT(*) + FROM session_message AS m + WHERE m.session_run_id = r.id AND m.role = 'assistant' + ) = ? + AND NOT EXISTS ( + SELECT 1 + FROM session_event AS existing + WHERE existing.session_id = s.id + AND ( + existing.source_event_id = ? + OR ( + existing.run_id = r.id + AND existing.event_type IN ('run.cancelled', 'run.completed', 'run.failed') + ) + ) + ) + AND NOT EXISTS ( + SELECT 1 + FROM driver_command AS command + WHERE command.kind = 'mcp.execute' + AND command.status = 'accepted' + AND json_extract(command.payload_json, '$.runId') = r.id + ) + AND NOT EXISTS ( + SELECT 1 + FROM external_tool_effect AS effect + INNER JOIN driver_command AS command ON command.id = effect.command_id + WHERE effect.session_run_id = r.id + AND effect.status IN ('claimed', 'succeeded', 'unknown') + AND command.status IN ('queued', 'delivered', 'accepted') + )`, + ) + .bind( + event.artifactAttemptId, + event.artifactManifestJson, + event.artifactManifestSha256, + event.contentText, + event.createdAt, + event.endedAt, + event.eventType, + event.family, + event.id, + event.occurredAt, + event.processStatus, + event.processType, + event.semanticHash, + event.terminalEventJson, + event.sourceEventId, + event.source, + event.streamId, + event.toolCallId, + event.toolInputJson, + event.toolName, + event.tokens, + event.traceId, + event.visibility, + input.sessionId, + input.runId, + state.run_status, + state.run_status_seq, + state.run_status_operation_id, + state.run_status_source, + Number(input.expectedDriverObservation !== undefined), + input.expectedDriverObservation?.driverInstanceId ?? null, + Number(input.expectedDriverObservation?.connectionId !== undefined), + input.expectedDriverObservation?.connectionId ?? null, + Number(input.expectedDriverObservation?.generation !== undefined), + input.expectedDriverObservation?.generation ?? null, + Number(input.expectedDriverObservation?.status !== undefined), + input.expectedDriverObservation?.status ?? null, + Number(input.expectedDriverObservation?.updatedAt !== undefined), + input.expectedDriverObservation?.updatedAt ?? null, + Number(input.expectedDriverObservation?.lastHeartbeatAt !== undefined), + input.expectedDriverObservation?.lastHeartbeatAt ?? null, + state.archived_at, + state.cleanup_operation_kind, + state.last_run_id, + state.session_status, + state.session_status_operation_id, + state.session_status_seq, + state.session_updated_at, + state.message_seq_cursor, + state.runtime_event_seq_cursor, + input.assistantMessageCount, + event.sourceEventId, + ); +} + +function prepareTerminalEventAdoptionFence( + database: D1Database, + row: PersistedTerminalAuthorityRow, +): D1PreparedStatement { + return database + .prepare( + `/* completed-run:adopt-event */ + INSERT INTO session_event (id) + SELECT ? + WHERE NOT EXISTS ( + SELECT 1 + FROM session_event + WHERE id = ? + AND agent_id = ? + AND artifact_attempt_id IS ? + AND artifact_manifest_json IS ? + AND artifact_manifest_sha256 IS ? + AND content_text = ? + AND created_at = ? + AND ended_at IS ? + AND event_type = ? + AND family = ? + AND mcp_command_id IS ? + AND occurred_at = ? + AND process_status = ? + AND process_type = ? + AND run_id IS ? + AND semantic_hash IS ? + AND terminal_event_json IS ? + AND seq = ? + AND session_id = ? + AND source = ? + AND source_event_id = ? + AND stream_id IS ? + AND tool_call_id IS ? + AND tool_input_delta_json IS ? + AND tool_input_json IS ? + AND tool_name IS ? + AND tool_output_delta_text IS ? + AND tool_output_text IS ? + AND tool_parent_message_id IS ? + AND tool_result_message_id IS ? + AND tool_status IS ? + AND tokens IS ? + AND trace_id IS ? + AND visibility = ? + )`, + ) + .bind( + row.id, + row.id, + row.agent_id, + row.artifact_attempt_id, + row.artifact_manifest_json, + row.artifact_manifest_sha256, + row.content_text, + row.created_at, + row.ended_at, + row.event_type, + row.family, + row.mcp_command_id, + row.occurred_at, + row.process_status, + row.process_type, + row.run_id, + row.semantic_hash, + row.terminal_event_json, + row.seq, + row.session_id, + row.source, + row.source_event_id, + row.stream_id, + row.tool_call_id, + row.tool_input_delta_json, + row.tool_input_json, + row.tool_name, + row.tool_output_delta_text, + row.tool_output_text, + row.tool_parent_message_id, + row.tool_result_message_id, + row.tool_status, + row.tokens, + row.trace_id, + row.visibility, + ); +} + +function prepareAssistantMessageInsert( + database: D1Database, + input: { + eventId: RuntimeEventId; + message: PreparedTerminalAssistantMessage; + seqOffset: number; + }, +): D1PreparedStatement { + return database + .prepare( + `/* completed-run:message */ + INSERT INTO session_message ( + content_text, created_at, created_by_account_id, id, plan_json, role, + projection_format, segments_json, seq, session_id, session_run_id + ) + SELECT ?, ?, ?, ?, ?, 'assistant', ?, ?, s.message_seq_cursor + ?, s.id, ? + FROM session AS s + INNER JOIN session_event AS terminal + ON terminal.session_id = s.id AND terminal.id = ? + WHERE s.id = ? + AND terminal.event_type IN ('run.cancelled', 'run.completed', 'run.failed')`, + ) + .bind( + input.message.message.contentText, + input.message.createdAt, + input.message.message.createdByAccountId, + input.message.message.id, + input.message.message.planJson, + input.message.message.projectionFormat, + input.message.message.segmentsJson, + input.seqOffset, + input.message.message.sessionRunId, + input.eventId, + input.message.message.sessionId, + ); +} + +function prepareRunTerminalUpdate( + database: D1Database, + input: { + error: RunError | null; + eventId: RuntimeEventId; + runId: SessionRunId; + sessionId: SessionId; + source: TerminalRunProjectionSource; + statusOperationId: RuntimeOperationId | null; + state: CompletionStateRow; + targetStatus: HostTerminalRunStatus; + timestampMs: number; + }, +): D1PreparedStatement { + return database + .prepare( + `/* completed-run:run */ + UPDATE session_run + SET completed_at = ?, + error_code = ?, + error_details_json = ?, + error_message = ?, + error_retryable = ?, + started_at = COALESCE(started_at, ?), + status = ?, + status_changed_at = ?, + status_event = ?, + status_operation_id = ?, + status_seq = status_seq + 1, + status_source = ?, + updated_at = ? + WHERE id = ? + AND session_id = ? + AND status = ? + AND status_seq = ? + AND EXISTS ( + SELECT 1 + FROM session_event AS terminal + WHERE terminal.id = ? + AND terminal.session_id = ? + AND terminal.run_id = ? + AND terminal.event_type = ? + )`, + ) + .bind( + input.timestampMs, + input.error?.code ?? null, + input.error === null ? null : stringifyRuntimeEventSemanticValue(input.error.details), + input.error?.message ?? null, + input.error === null ? null : Number(input.error.retryable), + input.timestampMs, + input.targetStatus, + input.timestampMs, + terminalLifecycleEvent(input.targetStatus), + input.statusOperationId, + input.source, + input.timestampMs, + input.runId, + input.sessionId, + input.state.run_status, + input.state.run_status_seq, + input.eventId, + input.sessionId, + input.runId, + terminalEventKind(input.targetStatus), + ); +} + +function prepareSessionModelCallsTerminalUpdate( + database: D1Database, + input: { + eventId: RuntimeEventId; + runId: SessionRunId; + semanticHash: string | null; + sessionId: SessionId; + targetStatus: HostTerminalRunStatus; + timestampMs: number; + }, +): D1PreparedStatement { + const status = input.targetStatus === "completed" ? "completed" : "failed"; + + return database + .prepare( + `/* completed-run:model-calls */ + UPDATE session_model_call + SET completed_at = COALESCE(completed_at, ?), + status = ?, + updated_at = MAX(updated_at, ?) + WHERE session_id = ? + AND session_run_id = ? + AND EXISTS ( + SELECT 1 + FROM session_event AS terminal + WHERE terminal.id = ? + AND terminal.session_id = ? + AND terminal.run_id = ? + AND terminal.event_type = ? + AND terminal.semantic_hash IS ? + )`, + ) + .bind( + input.timestampMs, + status, + input.timestampMs, + input.sessionId, + input.runId, + input.eventId, + input.sessionId, + input.runId, + terminalEventKind(input.targetStatus), + input.semanticHash, + ); +} + +function prepareSessionTerminalUpdate( + database: D1Database, + input: { + assistantMessageCount: number; + eventId: RuntimeEventId; + lastMessageAt: number; + runId: SessionRunId; + sessionId: SessionId; + sessionOperationId: string | null; + sessionStatus: PreparedTerminalEvent["sessionStatus"]; + state: CompletionStateRow; + targetStatus: HostTerminalRunStatus; + timestampMs: number; + }, +): D1PreparedStatement { + return database + .prepare( + `/* completed-run:session */ + UPDATE session + SET last_message_at = CASE WHEN ? > 0 THEN ? ELSE last_message_at END, + message_seq_cursor = message_seq_cursor + ?, + runtime_event_seq_cursor = runtime_event_seq_cursor + 1, + status = ?, + status_operation_id = ?, + status_seq = status_seq + 1, + updated_at = MAX(updated_at, ?), + workspace_checkpoint_required = CASE + WHEN ? = 'completed' AND kind = 'cattle' THEN 1 + ELSE workspace_checkpoint_required + END + WHERE id = ? + AND archived_at IS ? + AND cleanup_operation_kind IS ? + AND last_run_id = ? + AND status = ? + AND status_operation_id IS ? + AND status_seq = ? + AND updated_at = ? + AND message_seq_cursor = ? + AND runtime_event_seq_cursor = ? + AND EXISTS ( + SELECT 1 + FROM session_event AS terminal + WHERE terminal.id = ? + AND terminal.session_id = ? + AND terminal.run_id = ? + AND terminal.event_type = ? + ) + AND EXISTS ( + SELECT 1 + FROM session_run AS completed_run + WHERE completed_run.id = ? + AND completed_run.session_id = ? + AND completed_run.status = ? + AND completed_run.status_seq = ? + )`, + ) + .bind( + input.assistantMessageCount, + input.lastMessageAt, + input.assistantMessageCount, + input.sessionStatus, + input.sessionOperationId, + input.timestampMs, + input.targetStatus, + input.sessionId, + input.state.archived_at, + input.state.cleanup_operation_kind, + input.runId, + input.state.session_status, + input.state.session_status_operation_id, + input.state.session_status_seq, + input.state.session_updated_at, + input.state.message_seq_cursor, + input.state.runtime_event_seq_cursor, + input.eventId, + input.sessionId, + input.runId, + terminalEventKind(input.targetStatus), + input.runId, + input.sessionId, + input.targetStatus, + input.state.run_status_seq + 1, + ); +} + +function prepareSessionTerminalRepair( + database: D1Database, + input: { + eventId: RuntimeEventId; + lastMessageAt: number; + messageCursorIncrement: number; + runId: SessionRunId; + runtimeEventCursorIncrement: 0 | 1; + sessionId: SessionId; + sessionOperationId: string | null; + sessionStatus: PreparedTerminalEvent["sessionStatus"]; + state: CompletionStateRow; + targetStatus: HostTerminalRunStatus; + timestampMs: number; + }, +): D1PreparedStatement { + const touchLastMessage = Number(input.messageCursorIncrement > 0); + + return database + .prepare( + `/* completed-run:session-repair */ + UPDATE session + SET last_message_at = CASE WHEN ? = 1 THEN ? ELSE last_message_at END, + message_seq_cursor = message_seq_cursor + ?, + runtime_event_seq_cursor = runtime_event_seq_cursor + ?, + status = CASE WHEN last_run_id = ? THEN ? ELSE status END, + status_operation_id = CASE + WHEN last_run_id = ? THEN ? + ELSE status_operation_id + END, + status_seq = status_seq + CASE + WHEN last_run_id = ? AND (status <> ? OR status_operation_id IS NOT ?) + THEN 1 + ELSE 0 + END, + updated_at = MAX(updated_at, ?), + workspace_checkpoint_required = CASE + WHEN ? = 'completed' AND kind = 'cattle' THEN 1 + ELSE workspace_checkpoint_required + END + WHERE id = ? + AND archived_at IS ? + AND cleanup_operation_kind IS ? + AND last_run_id IS ? + AND status = ? + AND status_operation_id IS ? + AND status_seq = ? + AND updated_at = ? + AND message_seq_cursor = ? + AND runtime_event_seq_cursor = ? + AND EXISTS ( + SELECT 1 + FROM session_event AS terminal + WHERE terminal.id = ? + AND terminal.session_id = ? + AND terminal.run_id = ? + AND terminal.event_type = ? + ) + AND EXISTS ( + SELECT 1 + FROM session_run AS terminal_run + WHERE terminal_run.id = ? + AND terminal_run.session_id = ? + AND terminal_run.status = ? + AND terminal_run.status_seq = ? + )`, + ) + .bind( + touchLastMessage, + input.lastMessageAt, + input.messageCursorIncrement, + input.runtimeEventCursorIncrement, + input.runId, + input.sessionStatus, + input.runId, + input.sessionOperationId, + input.runId, + input.sessionStatus, + input.sessionOperationId, + input.timestampMs, + input.targetStatus, + input.sessionId, + input.state.archived_at, + input.state.cleanup_operation_kind, + input.state.last_run_id, + input.state.session_status, + input.state.session_status_operation_id, + input.state.session_status_seq, + input.state.session_updated_at, + input.state.message_seq_cursor, + input.state.runtime_event_seq_cursor, + input.eventId, + input.sessionId, + input.runId, + terminalEventKind(input.targetStatus), + input.runId, + input.sessionId, + input.targetStatus, + input.state.run_status_seq, + ); +} + +function preparePermissionRequestDelete( + database: D1Database, + input: { + eventId: RuntimeEventId; + runId: SessionRunId; + sessionId: SessionId; + targetStatus: HostTerminalRunStatus; + }, +): D1PreparedStatement { + return database + .prepare( + `/* completed-run:permissions */ + DELETE FROM session_permission_request + WHERE session_id = ? + AND run_id = ? + AND EXISTS ( + SELECT 1 + FROM session_event AS terminal + INNER JOIN session_run AS completed_run + ON completed_run.id = terminal.run_id + WHERE terminal.id = ? + AND terminal.session_id = ? + AND terminal.run_id = ? + AND terminal.event_type = ? + AND completed_run.status = ? + )`, + ) + .bind( + input.sessionId, + input.runId, + input.eventId, + input.sessionId, + input.runId, + terminalEventKind(input.targetStatus), + input.targetStatus, + ); +} + +function prepareTerminalDriverReleaseClaim( + database: D1Database, + input: { + eventId: RuntimeEventId; + observation: AtomicTerminalDriverReleaseObservation; + runId: SessionRunId; + sessionId: SessionId; + targetStatus: HostTerminalRunStatus; + }, +): D1PreparedStatement { + return database + .prepare( + `/* completed-run:driver-release-claim */ + UPDATE driver_instance + SET status_operation_id = ? + WHERE id = ? + AND generation = ? + AND connection_id IS ? + AND status IN ('provisioning', 'connecting', 'ready', 'stopped', 'failed') + AND status_operation_id IS NULL + AND NOT EXISTS ( + SELECT 1 + FROM session_run AS active_successor + WHERE active_successor.driver_instance_id = driver_instance.id + AND active_successor.id <> ? + AND active_successor.status IN ('queued', 'booting', 'running', 'waiting_input') + ) + AND EXISTS ( + SELECT 1 + FROM session_event AS terminal + INNER JOIN session_run AS terminal_run + ON terminal_run.id = terminal.run_id + AND terminal_run.session_id = terminal.session_id + WHERE terminal.id = ? + AND terminal.session_id = ? + AND terminal.run_id = ? + AND terminal.event_type = ? + AND terminal_run.driver_instance_id = driver_instance.id + AND terminal_run.status = ? + )`, + ) + .bind( + input.runId, + input.observation.driverInstanceId, + input.observation.generation, + input.observation.connectionId, + input.runId, + input.eventId, + input.sessionId, + input.runId, + terminalEventKind(input.targetStatus), + input.targetStatus, + ); +} + +async function claimTerminalDriverForAdoption( + database: D1Database, + input: { + fence: PersistedTerminalAuthorityRow; + observation: AtomicTerminalDriverReleaseObservation; + result: Extract; + runId: SessionRunId; + sessionId: SessionId; + state: CompletionStateRow; + targetStatus: HostTerminalRunStatus; + }, +): Promise { + const results = await database.batch([ + prepareTerminalEventAdoptionFence(database, input.fence), + prepareTerminalDriverReleaseClaim(database, { + eventId: input.fence.id, + observation: input.observation, + runId: input.runId, + sessionId: input.sessionId, + targetStatus: input.targetStatus, + }), + ]); + + return getD1ChangeCount(results[1]) === 1 ? input.result : staleTerminalResult(input.state); +} + +function prepareTerminalCommitGuard( + database: D1Database, + input: { + assistantMessages: readonly GuardedAssistantMessage[]; + driverRelease: AtomicTerminalDriverReleaseObservation | null; + event: PreparedTerminalEvent; + error: RunError | null; + messageCursorIncrement: number; + repairingExistingTerminal: boolean; + runId: SessionRunId; + runtimeEventCursorIncrement: 0 | 1; + sessionId: SessionId; + source: string; + state: CompletionStateRow; + statusOperationId: string | null; + targetStatus: HostTerminalRunStatus; + terminalEventId: RuntimeEventId; + terminalSemanticHash: string | null; + terminalSourceEventId: string; + }, +): D1PreparedStatement { + const sameCurrentRun = input.state.last_run_id === input.runId; + const repairedLifecycle = + input.repairingExistingTerminal && + sameCurrentRun && + (input.state.session_status !== input.event.sessionStatus || + input.state.session_status_operation_id !== + projectedSessionOperationId(input.state, input.event.sessionStatus)); + const expectedSessionStatus = sameCurrentRun + ? input.event.sessionStatus + : input.state.session_status; + const expectedSessionOperationId = sameCurrentRun + ? projectedSessionOperationId(input.state, input.event.sessionStatus) + : input.state.session_status_operation_id; + const expectedSessionStatusSeq = + input.state.session_status_seq + + (input.repairingExistingTerminal ? Number(repairedLifecycle) : 1); + const expectedRunStatusSeq = + input.state.run_status_seq + Number(!input.repairingExistingTerminal); + const assistantMessagesJson = JSON.stringify(input.assistantMessages); + const expectedErrorDetailsJson = input.repairingExistingTerminal + ? input.state.error_details_json + : input.error === null + ? null + : stringifyRuntimeEventSemanticValue(input.error.details); + + return database + .prepare( + `/* completed-run:guard */ + INSERT INTO session_event (id) + SELECT ? + WHERE NOT EXISTS ( + SELECT 1 + FROM session_event AS terminal + INNER JOIN session_run AS terminal_run + ON terminal_run.id = terminal.run_id AND terminal_run.session_id = terminal.session_id + INNER JOIN session AS terminal_session ON terminal_session.id = terminal.session_id + WHERE terminal.id = ? + AND terminal.session_id = ? + AND terminal.run_id = ? + AND terminal.event_type = ? + AND terminal.source_event_id = ? + AND terminal.semantic_hash IS ? + AND terminal_run.status = ? + AND terminal_run.status_seq = ? + AND terminal_run.status_operation_id IS ? + AND terminal_run.status_source = ? + AND terminal_run.error_code IS ? + AND terminal_run.error_details_json IS ? + AND terminal_run.error_message IS ? + AND terminal_run.error_retryable IS ? + AND terminal_session.archived_at IS ? + AND terminal_session.cleanup_operation_kind IS ? + AND terminal_session.last_run_id IS ? + AND terminal_session.status = ? + AND terminal_session.status_operation_id IS ? + AND terminal_session.status_seq = ? + AND terminal_session.message_seq_cursor = ? + AND terminal_session.runtime_event_seq_cursor = ? + AND ( + ? = 0 OR EXISTS ( + SELECT 1 + FROM driver_instance AS terminal_driver + WHERE terminal_driver.id = ? + AND terminal_driver.generation = ? + AND terminal_driver.connection_id IS ? + AND terminal_driver.status_operation_id = ? + AND terminal_run.driver_instance_id = terminal_driver.id + ) + ) + AND NOT EXISTS ( + SELECT 1 FROM session_permission_request AS permission + WHERE permission.session_id = ? AND permission.run_id = ? + ) + AND NOT EXISTS ( + SELECT 1 FROM session_model_call AS model_call + WHERE model_call.session_id = ? + AND model_call.session_run_id = ? + AND ( + model_call.completed_at IS NULL + OR model_call.status <> ? + ) + ) + AND ? = ( + SELECT COUNT(*) + FROM session_message AS message + WHERE message.session_run_id = ? AND message.role = 'assistant' + ) + AND NOT EXISTS ( + SELECT 1 + FROM session_message AS message + WHERE message.session_run_id = ? + AND message.role = 'assistant' + AND NOT EXISTS ( + SELECT 1 + FROM json_each(?) AS expected + WHERE message.id = json_extract(expected.value, '$.id') + AND message.session_id = json_extract(expected.value, '$.sessionId') + AND message.content_text = json_extract(expected.value, '$.contentText') + AND message.created_at = json_extract(expected.value, '$.createdAt') + AND message.plan_json IS json_extract(expected.value, '$.planJson') + AND message.projection_format = json_extract( + expected.value, + '$.projectionFormat' + ) + AND message.segments_json IS json_extract(expected.value, '$.segmentsJson') + AND message.created_by_account_id = json_extract( + expected.value, + '$.createdByAccountId' + ) + AND message.seq = json_extract(expected.value, '$.seq') + ) + ) + )`, + ) + .bind( + input.event.id, + input.terminalEventId, + input.sessionId, + input.runId, + terminalEventKind(input.targetStatus), + input.terminalSourceEventId, + input.terminalSemanticHash, + input.targetStatus, + expectedRunStatusSeq, + input.statusOperationId, + input.source, + input.error?.code ?? null, + expectedErrorDetailsJson, + input.error?.message ?? null, + input.error === null ? null : Number(input.error.retryable), + input.state.archived_at, + input.state.cleanup_operation_kind, + input.state.last_run_id, + expectedSessionStatus, + expectedSessionOperationId, + expectedSessionStatusSeq, + input.state.message_seq_cursor + input.messageCursorIncrement, + input.state.runtime_event_seq_cursor + input.runtimeEventCursorIncrement, + Number(input.driverRelease !== null), + input.driverRelease?.driverInstanceId ?? "", + input.driverRelease?.generation ?? -1, + input.driverRelease === null ? "" : input.driverRelease.connectionId, + input.runId, + input.sessionId, + input.runId, + input.sessionId, + input.runId, + input.targetStatus === "completed" ? "completed" : "failed", + input.assistantMessages.length, + input.runId, + input.runId, + assistantMessagesJson, + ); +} + +async function classifyAfterBatch( + database: D1Database, + input: { + assistantMessage: PreparedAssistantMessageProjection | null; + error: RunError | null; + event: PreparedTerminalEvent; + expectedDriverObservation?: ExpectedTerminalDriverObservation; + expectedRunStatus?: SessionRunStatus; + expectedSessionObservation?: ExpectedTerminalSessionObservation; + expectedSessionOperationId?: RuntimeOperationId | null; + runId: SessionRunId; + runStatusOperationId: RuntimeOperationId | null; + sessionId: SessionId; + source: TerminalRunProjectionSource; + targetStatus: HostTerminalRunStatus; + terminalEventFence?: PersistedTerminalAuthorityRow; + }, +): Promise { + const snapshot = await readCompletionSnapshot(database, { + finalMessageId: input.assistantMessage?.id ?? null, + runId: input.runId, + sessionId: input.sessionId, + sourceEventId: input.event.sourceEventId, + }); + if (input.terminalEventFence !== undefined) { + assertSnapshotTerminalAuthority(snapshot, input.terminalEventFence, input.runId); + await assertDurableTerminalAuthority(database, input.terminalEventFence, input.runId); + } + const assistantMessages = prepareTerminalAssistantMessages(snapshot, { + finalMessage: input.assistantMessage, + runId: input.runId, + sessionId: input.sessionId, + }); + + return classifyTerminalSnapshot(snapshot, { + assistantMessage: input.assistantMessage, + assistantMessages, + createdAt: input.event.createdAt, + error: input.error, + explicitRunView: input.event.explicitRunView, + ...(input.expectedDriverObservation === undefined + ? {} + : { expectedDriverObservation: input.expectedDriverObservation }), + ...(input.expectedRunStatus === undefined + ? {} + : { expectedRunStatus: input.expectedRunStatus }), + ...(input.expectedSessionObservation === undefined + ? {} + : { expectedSessionObservation: input.expectedSessionObservation }), + ...(input.expectedSessionOperationId === undefined + ? {} + : { expectedSessionOperationId: input.expectedSessionOperationId }), + eventId: input.event.id, + runId: input.runId, + runStatusOperationId: input.runStatusOperationId, + semanticHash: input.event.semanticHash, + sessionId: input.sessionId, + sessionStatus: input.event.sessionStatus, + sourceEventId: input.event.sourceEventId, + source: input.source, + targetStatus: input.targetStatus, + }); +} + +function readPersistedTerminalStatus( + status: SessionRunStatus, + runId: SessionRunId, +): HostTerminalRunStatus { + switch (status) { + case "cancelled": + case "completed": + case "expired": + case "failed": + return status; + case "booting": + case "queued": + case "running": + case "waiting_input": + throw new Error(`Session run ${runId} has a terminal receipt without a terminal status.`); + } +} + +function readPersistedTerminalSource( + source: string, + runId: SessionRunId, +): TerminalRunProjectionSource { + switch (source) { + case "api": + case "driver": + case "maintenance": + case "runtime_operation": + case "system": + case "viewer": + return source; + default: + throw new Error(`Session run ${runId} has an invalid terminal status source.`); + } +} + +function readPersistedTerminalError( + state: CompletionStateRow, + runId: SessionRunId, +): RunError | null { + if ( + state.error_code === null && + state.error_details_json === null && + state.error_message === null && + state.error_retryable === null + ) { + return null; + } + if ( + state.error_code === null || + state.error_details_json === null || + state.error_message === null || + (state.error_retryable !== 0 && state.error_retryable !== 1) + ) { + throw new Error(`Session run ${runId} has an invalid durable terminal error.`); + } + + try { + return parseSchemaValue(DurableRunError, { + code: state.error_code, + details: JSON.parse(state.error_details_json), + message: state.error_message, + retryable: state.error_retryable === 1, + }); + } catch { + throw new Error(`Session run ${runId} has an invalid durable terminal error.`); + } +} + +function assertAdoptableTerminalReceipt( + row: PersistedTerminalAuthorityRow, + input: { + runId: SessionRunId; + sessionId: SessionId; + targetStatus: HostTerminalRunStatus; + }, +): void { + const eventType = terminalEventKind(input.targetStatus); + const hasArtifact = row.artifact_attempt_id !== null; + if ( + row.session_id !== input.sessionId || + row.run_id !== input.runId || + row.event_type !== eventType || + row.source_event_id !== createSessionRunTerminalSourceId(input.runId, eventType) || + row.visibility !== "all_consumers" || + row.mcp_command_id !== null || + row.tool_call_id !== null || + row.tool_input_delta_json !== null || + row.tool_input_json !== null || + row.tool_name !== null || + row.tool_output_delta_text !== null || + row.tool_output_text !== null || + row.tool_parent_message_id !== null || + row.tool_result_message_id !== null || + row.tool_status !== null || + (row.semantic_hash !== null && !/^[0-9a-f]{64}$/.test(row.semantic_hash)) || + (row.semantic_hash === null) !== (row.terminal_event_json === null) || + (eventType !== "run.completed" && row.stream_id !== null) || + hasArtifact !== (row.artifact_manifest_json !== null) || + hasArtifact !== (row.artifact_manifest_sha256 !== null) || + (hasArtifact && eventType !== "run.completed") + ) { + throw new Error(`Session run ${input.runId} has an invalid canonical terminal receipt.`); + } +} + +function assertPreparedTerminalEventMatchesAuthority( + event: PreparedTerminalEvent, + row: PersistedTerminalAuthorityRow, + runId: SessionRunId, +): void { + if ( + event.artifactAttemptId !== row.artifact_attempt_id || + event.artifactManifestJson !== row.artifact_manifest_json || + event.artifactManifestSha256 !== row.artifact_manifest_sha256 || + event.contentText !== row.content_text || + event.createdAt !== row.created_at || + event.endedAt !== row.ended_at || + event.eventType !== row.event_type || + event.family !== row.family || + event.occurredAt !== row.occurred_at || + event.processStatus !== row.process_status || + event.processType !== row.process_type || + event.semanticHash !== row.semantic_hash || + event.source !== row.source || + event.sourceEventId !== row.source_event_id || + event.streamId !== row.stream_id || + event.terminalEventJson !== row.terminal_event_json || + event.toolCallId !== row.tool_call_id || + event.toolInputJson !== row.tool_input_json || + event.toolName !== row.tool_name || + event.tokens !== row.tokens || + event.traceId !== row.trace_id || + event.visibility !== row.visibility + ) { + throw new Error(`Session run ${runId} terminal semantic authority conflicts with its receipt.`); + } +} + +function prepareAdoptedTerminalEvent( + row: PersistedTerminalAuthorityRow, + state: CompletionStateRow, +): PreparedTerminalEvent { + return { + artifactAttemptId: row.artifact_attempt_id, + artifactManifestJson: row.artifact_manifest_json, + artifactManifestSha256: row.artifact_manifest_sha256, + contentText: row.content_text, + createdAt: row.created_at, + endedAt: row.ended_at ?? row.occurred_at, + eventType: row.event_type, + explicitRunView: null, + family: row.family, + id: createPlatformId(), + occurredAt: row.occurred_at, + processStatus: row.process_status, + processType: row.process_type, + semanticHash: row.semantic_hash ?? "", + sessionStatus: state.session_status === "TERMINATED" ? "TERMINATED" : "IDLE", + source: row.source, + sourceEventId: row.source_event_id, + streamId: row.stream_id, + terminalEventJson: row.terminal_event_json ?? "", + toolCallId: row.tool_call_id, + toolInputJson: row.tool_input_json, + toolName: row.tool_name, + tokens: row.tokens, + traceId: row.trace_id, + visibility: row.visibility, + }; +} + +export async function adoptTerminalRunProjection( + database: D1Database, + input: { + expectedDriverObservation?: AtomicTerminalDriverReleaseObservation; + expectedTargetStatus?: DriverTerminalRunStatus; + runId: SessionRunId; + sessionId: SessionId; + }, +): Promise { + const authority = await readTerminalAuthorityReceipt(database, input.runId); + if (authority === null) { + return { kind: "missing", persistedSourceEventIds: [], runDurationMs: null }; + } + if (authority.session_id !== input.sessionId) { + throw new Error(`Session run ${input.runId} terminal receipt belongs to another session.`); + } + const semanticAuthority = + authority.semantic_hash === null + ? null + : await readTerminalEventSemanticAuthority({ + eventJson: authority.terminal_event_json, + eventType: authority.event_type, + runId: input.runId, + semanticHash: authority.semantic_hash, + sessionId: input.sessionId, + sourceEventId: authority.source_event_id, + streamId: authority.stream_id, + }); + const persistedEvent = semanticAuthority?.event ?? null; + const persistedFinalMessageId = semanticAuthority?.finalMessageId ?? null; + + const snapshot = await readCompletionSnapshot(database, { + finalMessageId: persistedFinalMessageId, + runId: input.runId, + sessionId: input.sessionId, + sourceEventId: authority.source_event_id, + }); + const { state } = snapshot; + if (state === null) { + throw new Error(`Session run ${input.runId} was not found for terminal adoption.`); + } + assertSnapshotTerminalAuthority(snapshot, authority, input.runId); + + const targetStatus = readPersistedTerminalStatus(state.run_status, input.runId); + const error = readPersistedTerminalError(state, input.runId); + assertAdoptableTerminalReceipt(authority, { + runId: input.runId, + sessionId: input.sessionId, + targetStatus, + }); + if ( + input.expectedTargetStatus !== undefined && + terminalEventKind(input.expectedTargetStatus) !== terminalEventKind(targetStatus) + ) { + return staleTerminalResult(state); + } + + const source = readPersistedTerminalSource(state.run_status_source, input.runId); + const expectedDriverObservation = + input.expectedDriverObservation === undefined + ? undefined + : { + connectionId: input.expectedDriverObservation.connectionId, + driverInstanceId: input.expectedDriverObservation.driverInstanceId, + generation: input.expectedDriverObservation.generation, + }; + const assistantMessage = + persistedEvent === null || persistedFinalMessageId === null + ? null + : prepareAssistantMessageProjection({ + createdByAccountId: state.run_created_by_account_id, + messageId: persistedFinalMessageId, + sessionId: input.sessionId, + sessionRunId: input.runId, + }); + const event = + persistedEvent === null + ? prepareAdoptedTerminalEvent(authority, state) + : await prepareTerminalEvent( + { + artifactAttemptId: authority.artifact_attempt_id, + artifactManifestJson: authority.artifact_manifest_json, + artifactManifestSha256: authority.artifact_manifest_sha256, + event: persistedEvent, + occurredAt: authority.occurred_at, + sourceEventId: authority.source_event_id, + }, + { + assistantMessage, + error, + runId: input.runId, + sessionId: input.sessionId, + targetStatus, + timestampMs: authority.created_at, + }, + ); + if (persistedEvent !== null) { + assertPreparedTerminalEventMatchesAuthority(event, authority, input.runId); + } + + return commitPreparedTerminalRunProjection(database, { + assistantMessage, + error, + event, + ...(expectedDriverObservation === undefined ? {} : { expectedDriverObservation }), + runId: input.runId, + runStatusOperationId: state.run_status_operation_id as RuntimeOperationId | null, + sessionId: input.sessionId, + source, + targetStatus, + terminalEventFence: authority, + }); +} + +export async function commitTerminalRunProjection( + database: D1Database, + input: { + assistantMessage: PreparedAssistantMessageProjection | null; + error: RunError | null; + expectedDriverObservation?: ExpectedTerminalDriverObservation; + expectedRunStatus?: SessionRunStatus; + expectedSessionObservation?: ExpectedTerminalSessionObservation; + expectedSessionOperationId?: RuntimeOperationId | null; + runId: SessionRunId; + sessionId: SessionId; + source: TerminalRunProjectionSource; + targetStatus: HostTerminalRunStatus; + terminalEvent: SessionRuntimeEventInput; + timestampMs?: number; + }, +): Promise { + if (input.targetStatus !== "completed" && input.assistantMessage !== null) { + throw new Error("Only a completed run can project a final assistant message."); + } + + const runStatusOperationId = readTerminalRunOperationId(input); + const event = await prepareTerminalEvent(input.terminalEvent, input); + + return commitPreparedTerminalRunProjection(database, { + ...input, + event, + runStatusOperationId, + }); +} + +async function commitPreparedTerminalRunProjection( + database: D1Database, + input: { + assistantMessage: PreparedAssistantMessageProjection | null; + error: RunError | null; + event: PreparedTerminalEvent; + expectedDriverObservation?: ExpectedTerminalDriverObservation; + expectedRunStatus?: SessionRunStatus; + expectedSessionObservation?: ExpectedTerminalSessionObservation; + expectedSessionOperationId?: RuntimeOperationId | null; + runId: SessionRunId; + runStatusOperationId: RuntimeOperationId | null; + sessionId: SessionId; + source: TerminalRunProjectionSource; + targetStatus: HostTerminalRunStatus; + terminalEventFence?: PersistedTerminalAuthorityRow; + }, +): Promise { + const { event, runStatusOperationId } = input; + const snapshot = await readCompletionSnapshot(database, { + finalMessageId: input.assistantMessage?.id ?? null, + runId: input.runId, + sessionId: input.sessionId, + sourceEventId: event.sourceEventId, + }); + if (input.terminalEventFence !== undefined) { + assertSnapshotTerminalAuthority(snapshot, input.terminalEventFence, input.runId); + await assertDurableTerminalAuthority(database, input.terminalEventFence, input.runId); + } + const assistantMessages = prepareTerminalAssistantMessages(snapshot, { + finalMessage: input.assistantMessage, + runId: input.runId, + sessionId: input.sessionId, + }); + const terminal = classifyTerminalSnapshot(snapshot, { + assistantMessage: input.assistantMessage, + assistantMessages, + createdAt: event.createdAt, + error: input.error, + explicitRunView: event.explicitRunView, + ...(input.expectedDriverObservation === undefined + ? {} + : { expectedDriverObservation: input.expectedDriverObservation }), + ...(input.expectedRunStatus === undefined + ? {} + : { expectedRunStatus: input.expectedRunStatus }), + ...(input.expectedSessionObservation === undefined + ? {} + : { expectedSessionObservation: input.expectedSessionObservation }), + ...(input.expectedSessionOperationId === undefined + ? {} + : { expectedSessionOperationId: input.expectedSessionOperationId }), + eventId: event.id, + runId: input.runId, + runStatusOperationId, + semanticHash: event.semanticHash, + sessionId: input.sessionId, + sessionStatus: event.sessionStatus, + sourceEventId: event.sourceEventId, + source: input.source, + targetStatus: input.targetStatus, + }); + const { state } = snapshot; + + if (terminal !== null) { + const driverRelease = readAtomicTerminalDriverReleaseObservation( + input.expectedDriverObservation, + ); + if ( + (terminal.kind === "applied" || terminal.kind === "duplicate") && + input.terminalEventFence !== undefined && + state !== null && + driverRelease !== null && + state.driver_status_operation_id === null + ) { + return claimTerminalDriverForAdoption(database, { + fence: input.terminalEventFence, + observation: driverRelease, + result: terminal, + runId: input.runId, + sessionId: input.sessionId, + state, + targetStatus: input.targetStatus, + }); + } + return terminal; + } + + if (state === null) { + throw new Error(`Session run ${input.runId} was not found for atomic completion.`); + } + + const repairingExistingTerminal = state.run_status === input.targetStatus; + const observedDriverRelease = readAtomicTerminalDriverReleaseObservation( + input.expectedDriverObservation, + ); + const driverRelease = + observedDriverRelease !== null && state.driver_status_operation_id === null + ? observedDriverRelease + : null; + + if (!repairingExistingTerminal) { + const decision = decideSessionRunTransition({ + currentStatus: state.run_status, + targetStatus: input.targetStatus, + }); + + if (decision.kind !== "accepted") { + throw new Error(`Driver terminal run projection was rejected: ${decision.kind}.`); + } + } + + if ( + state.session_status === "TERMINATED" && + (!repairingExistingTerminal || event.sessionStatus !== "TERMINATED") + ) { + throw new Error("Session is not writable for an atomic terminal run projection."); + } + + if ( + (!repairingExistingTerminal && snapshot.terminalEvents.length > 0) || + (!repairingExistingTerminal && snapshot.assistantMessages.length > 0) + ) { + throw new Error("Atomic terminal run projection found a partial canonical projection."); + } + + if (!repairingExistingTerminal && state.last_run_id !== input.runId) { + throw new Error("Active terminal run is no longer the Session's current run."); + } + + const persistedTerminalEvent = repairingExistingTerminal + ? (snapshot.terminalEvents[0] ?? null) + : null; + const terminalEventId = persistedTerminalEvent?.id ?? event.id; + const terminalSemanticHash = + persistedTerminalEvent === null ? event.semanticHash : persistedTerminalEvent.semantic_hash; + const terminalSourceEventId = persistedTerminalEvent?.source_event_id ?? event.sourceEventId; + const repairingLegacyTerminal = persistedTerminalEvent?.semantic_hash === null; + const guardedRunSource = repairingLegacyTerminal ? state.run_status_source : input.source; + const guardedRunStatusOperationId = repairingLegacyTerminal + ? state.run_status_operation_id + : runStatusOperationId; + const sessionOperationId = projectedSessionOperationId(state, event.sessionStatus); + const runtimeEventCursorIncrement = (() => { + if (persistedTerminalEvent === null) { + return 1; + } + if (persistedTerminalEvent.seq <= state.runtime_event_seq_cursor) { + return 0; + } + if (persistedTerminalEvent.seq === state.runtime_event_seq_cursor + 1) { + return 1; + } + throw new Error("Canonical terminal event is separated from the Session cursor by a gap."); + })() satisfies 0 | 1; + const assistantMessageInserts: { + message: PreparedTerminalAssistantMessage; + seqOffset: number; + }[] = []; + const guardedAssistantMessages: GuardedAssistantMessage[] = []; + let messageCursorIncrement = 0; + + if (repairingLegacyTerminal) { + for (const persisted of snapshot.assistantMessages) { + if (persisted.seq > state.message_seq_cursor) { + messageCursorIncrement += 1; + if (persisted.seq !== state.message_seq_cursor + messageCursorIncrement) { + throw new Error( + "Legacy assistant message is separated from the Session cursor by a gap.", + ); + } + } + guardedAssistantMessages.push({ + contentText: persisted.content_text, + createdAt: persisted.created_at, + createdByAccountId: persisted.created_by_account_id, + id: persisted.id, + planJson: persisted.plan_json, + projectionFormat: persisted.projection_format, + segmentsJson: persisted.segments_json, + seq: persisted.seq, + sessionId: input.sessionId, + }); + } + } else { + assertRepairableAssistantMessages(snapshot.assistantMessages, assistantMessages, input.runId); + for (const [index, message] of assistantMessages.entries()) { + const persisted = snapshot.assistantMessages[index]; + let seq: number; + if (persisted === undefined) { + messageCursorIncrement += 1; + seq = state.message_seq_cursor + messageCursorIncrement; + assistantMessageInserts.push({ + message, + seqOffset: messageCursorIncrement, + }); + } else { + seq = persisted.seq; + if (seq > state.message_seq_cursor) { + messageCursorIncrement += 1; + if (seq !== state.message_seq_cursor + messageCursorIncrement) { + throw new Error( + "Canonical assistant messages are separated from the Session cursor by a gap.", + ); + } + } + } + guardedAssistantMessages.push({ + contentText: message.message.contentText, + createdAt: message.createdAt, + createdByAccountId: message.message.createdByAccountId, + id: message.message.id, + planJson: message.message.planJson, + projectionFormat: message.message.projectionFormat, + segmentsJson: message.message.segmentsJson, + seq, + sessionId: message.message.sessionId, + }); + } + } + const lastMessageAt = guardedAssistantMessages.at(-1)?.createdAt ?? event.createdAt; + + const statements = [ + input.terminalEventFence === undefined + ? prepareTerminalEventInsert(database, { + assistantMessageCount: snapshot.assistantMessages.length, + event, + ...(input.expectedDriverObservation === undefined + ? {} + : { expectedDriverObservation: input.expectedDriverObservation }), + runId: input.runId, + sessionId: input.sessionId, + state, + }) + : prepareTerminalEventAdoptionFence(database, input.terminalEventFence), + ...(!repairingExistingTerminal && + event.artifactAttemptId !== null && + event.artifactManifestJson !== null && + event.artifactManifestSha256 !== null + ? prepareRuntimeArtifactPromotion(database, { + attemptId: event.artifactAttemptId, + eventId: event.id, + manifestJson: event.artifactManifestJson, + manifestSha256: event.artifactManifestSha256, + timestampMs: event.createdAt, + }) + : []), + ...assistantMessageInserts.map(({ message, seqOffset }) => + prepareAssistantMessageInsert(database, { + eventId: terminalEventId, + message, + seqOffset, + }), + ), + ...(repairingExistingTerminal + ? [ + prepareSessionTerminalRepair(database, { + eventId: terminalEventId, + lastMessageAt, + messageCursorIncrement, + runId: input.runId, + runtimeEventCursorIncrement, + sessionId: input.sessionId, + sessionOperationId, + sessionStatus: event.sessionStatus, + state, + targetStatus: input.targetStatus, + timestampMs: event.createdAt, + }), + ] + : [ + prepareRunTerminalUpdate(database, { + error: input.error, + eventId: event.id, + runId: input.runId, + sessionId: input.sessionId, + source: input.source, + state, + statusOperationId: runStatusOperationId, + targetStatus: input.targetStatus, + timestampMs: event.createdAt, + }), + prepareSessionTerminalUpdate(database, { + assistantMessageCount: assistantMessages.length, + eventId: event.id, + lastMessageAt, + runId: input.runId, + sessionId: input.sessionId, + sessionOperationId, + sessionStatus: event.sessionStatus, + state, + targetStatus: input.targetStatus, + timestampMs: event.createdAt, + }), + ]), + prepareSessionModelCallsTerminalUpdate(database, { + eventId: terminalEventId, + runId: input.runId, + semanticHash: terminalSemanticHash, + sessionId: input.sessionId, + targetStatus: input.targetStatus, + timestampMs: event.createdAt, + }), + ...(driverRelease === null + ? [] + : [ + prepareTerminalDriverReleaseClaim(database, { + eventId: terminalEventId, + observation: driverRelease, + runId: input.runId, + sessionId: input.sessionId, + targetStatus: input.targetStatus, + }), + ]), + preparePermissionRequestDelete(database, { + eventId: terminalEventId, + runId: input.runId, + sessionId: input.sessionId, + targetStatus: input.targetStatus, + }), + prepareTerminalCommitGuard(database, { + assistantMessages: guardedAssistantMessages, + driverRelease, + event, + error: input.error, + messageCursorIncrement, + repairingExistingTerminal, + runId: input.runId, + source: guardedRunSource, + statusOperationId: guardedRunStatusOperationId, + runtimeEventCursorIncrement, + sessionId: input.sessionId, + state, + targetStatus: input.targetStatus, + terminalEventId, + terminalSemanticHash, + terminalSourceEventId, + }), + ]; + + try { + const results = await database.batch(statements); + + if (input.terminalEventFence === undefined && getD1ChangeCount(results[0]) === 0) { + const raced = await classifyAfterBatch(database, { + ...input, + event, + runStatusOperationId, + }); + + if (raced !== null) { + return raced; + } + + throw new Error("Atomic terminal run projection lost a concurrent session mutation."); + } + } catch (error) { + try { + const committed = await classifyAfterBatch(database, { + ...input, + event, + runStatusOperationId, + }); + + if (committed !== null) { + return committed; + } + } catch { + // Preserve the batch failure when its commit outcome cannot be proven. + } + + throw error; + } + + const committed = await classifyAfterBatch(database, { + ...input, + event, + runStatusOperationId, + }); + + if (committed === null) { + throw new Error("Atomic terminal run projection did not commit its canonical projection."); + } + + return committed; +} diff --git a/apps/api/src/modules/runtime/infrastructure/driver-instance/connections.ts b/apps/api/src/modules/runtime/infrastructure/driver-instance/connections.ts index 1c970e78..8dde0ee4 100644 --- a/apps/api/src/modules/runtime/infrastructure/driver-instance/connections.ts +++ b/apps/api/src/modules/runtime/infrastructure/driver-instance/connections.ts @@ -17,6 +17,7 @@ function isDriverInstanceConflictMessage(message: string): boolean { message === "Driver hello is required before pushLogs." || message === "Driver instance id does not match the active Durable Object." || message === "Driver instance id mismatch." || + message === "Driver generation is no longer current." || message.includes("already closed.") || message.includes("closed before hello.") ); diff --git a/apps/api/src/modules/runtime/infrastructure/driver-instance/debug-resume-snapshot.ts b/apps/api/src/modules/runtime/infrastructure/driver-instance/debug-resume-snapshot.ts index 5a936c41..5bdb6213 100644 --- a/apps/api/src/modules/runtime/infrastructure/driver-instance/debug-resume-snapshot.ts +++ b/apps/api/src/modules/runtime/infrastructure/driver-instance/debug-resume-snapshot.ts @@ -3,7 +3,6 @@ import type { SandboxId } from "@mosoo/id"; export type DriverDebugRecoveryMode = "fresh" | "ready" | "disconnected" | "turn_interrupted"; export interface DriverDebugResumeSnapshot { - readonly lastEventSeq: number; readonly recoveryMode: DriverDebugRecoveryMode; readonly sandboxId: SandboxId | null; } @@ -11,9 +10,5 @@ export interface DriverDebugResumeSnapshot { export function createDriverDebugResumeSnapshot( input: DriverDebugResumeSnapshot, ): DriverDebugResumeSnapshot { - if (!Number.isInteger(input.lastEventSeq) || input.lastEventSeq < 0) { - throw new Error("Driver debug resume lastEventSeq must be a non-negative integer."); - } - return input; } diff --git a/apps/api/src/modules/runtime/infrastructure/driver-instance/do.ts b/apps/api/src/modules/runtime/infrastructure/driver-instance/do.ts index e956128d..2a43b68d 100644 --- a/apps/api/src/modules/runtime/infrastructure/driver-instance/do.ts +++ b/apps/api/src/modules/runtime/infrastructure/driver-instance/do.ts @@ -24,11 +24,14 @@ import { import { decodeAndHashBootToken } from "../runtime-boot-token"; import { toDriverInstanceRequestErrorStatus } from "./connections"; import { json, toErrorMessage } from "./driver-instance-support"; -import { claimDriverInstanceByBootTokenHash } from "./driver-instance-token.repository"; +import { + claimDriverInstanceByBootTokenHash, + validateDriverInstanceBootTokenHash, +} from "./driver-instance-token.repository"; import { runtimeSessionLinkNeedsRefresh } from "./event-types"; import { handleDriverInstanceRequest } from "./http"; import type { DriverInstanceHttpHandler } from "./http"; -import { getDriverInstanceStatus, markDriverInstanceConnected } from "./lifecycle"; +import { getDriverInstanceLifecycleIdentity, markDriverInstanceConnected } from "./lifecycle"; import { createDriverInstanceRpcContext } from "./rpc"; import type { DriverInstanceRpcContext } from "./rpc"; import { DriverInstanceRpcController } from "./rpc-controller"; @@ -39,8 +42,7 @@ import { SessionViewerEventDeliveryBuffer } from "./session-viewer-event-deliver import { DriverInstanceSocketRegistry } from "./sockets"; import type { DriverInstanceCloseSnapshot, - DriverInstanceHeartbeatResult, - DriverInstanceHelloResult, + DriverInstanceConnectionEpoch, DriverInstanceReadyResult, DriverInstanceSnapshot, DriverInstanceWaitForCloseResult, @@ -48,7 +50,8 @@ import type { import { DriverInstanceTerminalStateCoordinator } from "./terminal-state-coordinator"; export class DriverInstance extends DurableObject implements DriverInstanceHttpHandler { - #destroyed = false; + #destroyedGeneration: number | null = null; + #destroyTask: Promise | null = null; readonly #identity = new DurableObjectIdentity({ mismatchMessage: "Driver instance id does not match the active Durable Object.", requiredMessage: "Driver instance id is required.", @@ -87,7 +90,7 @@ export class DriverInstance extends DurableObject implements DriverInstanceHttpH }); this.#rpcController = new DriverInstanceRpcController({ env, - finalizeTerminalState: async () => this.#terminalState.finalize(), + finalizeTerminalState: async (epoch) => this.#terminalState.finalize(epoch), sockets: this.#sockets, state: this.#state, viewCache: this.#viewCache, @@ -95,16 +98,40 @@ export class DriverInstance extends DurableObject implements DriverInstanceHttpH waitUntil: (task) => this.ctx.waitUntil(task), withRuntimeLogContext: (fn) => this.#withRuntimeLogContext(fn), }); - void this.ctx.blockConcurrencyWhile(async () => this.#state.load()); + void this.ctx.blockConcurrencyWhile(async () => { + await this.#state.load(); + + if (this.#state.driverInstanceId !== null && this.#state.driverGeneration === null) { + const identity = await getDriverInstanceLifecycleIdentity( + this.env, + this.#state.driverInstanceId, + ); + + if (identity !== null) { + await this.#state.setDriverGeneration(identity.generation); + } + } + + if ( + (this.#state.terminalized || this.#state.errorMessage !== null) && + !this.#state.terminalCleanupComplete + ) { + const epoch = this.#state.connectionEpoch(); + + if (epoch !== null) { + await this.#finalizeTerminalState(epoch); + } + } + }); } override async fetch(request: Request): Promise { try { const url = new URL(request.url); - if (this.#destroyed) { + if (this.#destroyedGeneration !== null) { if (request.method === "POST" && url.pathname === "/control/destroy") { - return json({ ok: true }); + return handleDriverInstanceRequest(this, request); } return json({ error: "Driver instance Durable Object was destroyed." }, { status: 410 }); @@ -127,28 +154,28 @@ export class DriverInstance extends DurableObject implements DriverInstanceHttpH } override async webSocketClose(ws: WebSocket, code: number, reason: string): Promise { - if (this.#destroyed) { + if (this.#destroyedGeneration !== null) { return; } this.#rpcHandler?.close(ws); + const epoch = this.#sockets.getSocketEpoch(ws); - // A socket replaced by a newer accepted connection must not finalize the - // state that now belongs to its successor. - if (this.#sockets.isSupersededDriverSocket(ws)) { - this.#sockets.releaseDriverSocket(ws); + if (epoch === null || !this.#isCurrentSocketEpoch(ws, epoch)) { return; } - this.#sockets.releaseDriverSocket(ws); - const close: DriverInstanceCloseSnapshot = { at: new Date().toISOString(), code, reason, }; - await this.#state.persistClose(close); + await this.#state.persistClose(close, epoch); + + if (!this.#isCurrentSocketEpoch(ws, epoch)) { + return; + } this.#withRuntimeLogContext(() => { logInfo("runtime.socket.closed", { @@ -157,48 +184,54 @@ export class DriverInstance extends DurableObject implements DriverInstanceHttpH driverInstanceId: this.#state.driverInstanceId, }); }); - await this.#appendTransportWsDisconnectedEvent(close); + await this.#appendTransportWsDisconnectedEvent(close, epoch); - if (!this.#state.hello) { - this.#state.rejectHelloWaiters( - new Error(`Driver instance socket closed before hello: ${reason || code}.`), - ); + if (!this.#isCurrentSocketEpoch(ws, epoch)) { + return; } - await this.#terminalState.finalize(); + await this.#finalizeTerminalState(epoch); } override async webSocketMessage(ws: WebSocket, message: ArrayBuffer | string): Promise { - if (this.#destroyed) { + if (this.#destroyedGeneration !== null) { return; } - if (!this.#sockets.isActiveDriverSocket(ws)) { + const epoch = this.#sockets.getSocketEpoch(ws); + + if (epoch === null || !this.#isCurrentSocketEpoch(ws, epoch)) { return; } try { const rpcHandler = await this.#getRpcHandler(); - if (!this.#sockets.isActiveDriverSocket(ws)) { + if (!this.#isCurrentSocketEpoch(ws, epoch)) { return; } - const connectionId = this.#state.requireConnectionId(); await rpcHandler.message(ws, message, { context: createDriverInstanceRpcContext(this.#rpcController, { assertActiveConnection: () => { - if ( - this.#state.connectionId !== connectionId || - !this.#sockets.isActiveDriverSocket(ws) - ) { + if (!this.#isCurrentSocketEpoch(ws, epoch)) { throw new Error("Driver connection is no longer current."); } }, - connectionId, + connectionId: epoch.connectionId, + epoch, }), }); + + if (!this.#isCurrentSocketEpoch(ws, epoch)) { + this.#closeSocket(ws, 1000, "runtime.socket.superseded"); + } } catch (error) { + if (!this.#isCurrentSocketEpoch(ws, epoch)) { + this.#closeSocket(ws, 1000, "runtime.socket.superseded"); + return; + } + this.#withRuntimeLogContext(() => { logError("runtime.socket.message.failed", { ...createErrorLogContext(error), @@ -206,15 +239,27 @@ export class DriverInstance extends DurableObject implements DriverInstanceHttpH }); }); - await this.#state.setErrorMessage( + await this.#state.setConnectionErrorMessage( + epoch, toErrorMessage(error, "Driver instance WebSocket message failed."), ); - await this.#appendTransportRpcErrorEvent(error); + + if (!this.#isCurrentSocketEpoch(ws, epoch)) { + this.#closeSocket(ws, 1000, "runtime.socket.superseded"); + return; + } + + await this.#appendTransportRpcErrorEvent(error, epoch); + + if (!this.#isCurrentSocketEpoch(ws, epoch)) { + this.#closeSocket(ws, 1000, "runtime.socket.superseded"); + return; + } if (ws.readyState === WebSocket.OPEN) { ws.close(1003, "runtime.invalid-message"); } else { - await this.#terminalState.finalize(); + await this.#finalizeTerminalState(epoch); } } } @@ -236,11 +281,14 @@ export class DriverInstance extends DurableObject implements DriverInstanceHttpH } override async webSocketError(ws: WebSocket, _error: unknown): Promise { - if (this.#destroyed) { + if (this.#destroyedGeneration !== null) { return; } - if (this.#sockets.isSupersededDriverSocket(ws)) { + const epoch = this.#sockets.getSocketEpoch(ws); + + if (epoch === null || !this.#isCurrentSocketEpoch(ws, epoch)) { + this.#closeSocket(ws, 1000, "runtime.socket.superseded"); return; } @@ -250,15 +298,24 @@ export class DriverInstance extends DurableObject implements DriverInstanceHttpH }); }); - await this.#state.setErrorMessage("Driver instance WebSocket error."); - await this.#appendTransportRpcErrorEvent("Driver instance WebSocket error."); + await this.#state.setConnectionErrorMessage(epoch, "Driver instance WebSocket error."); + + if (!this.#isCurrentSocketEpoch(ws, epoch)) { + this.#closeSocket(ws, 1000, "runtime.socket.superseded"); + return; + } + + await this.#appendTransportRpcErrorEvent("Driver instance WebSocket error.", epoch); - const socket = this.#sockets.getDriverSocket(); + if (!this.#isCurrentSocketEpoch(ws, epoch)) { + this.#closeSocket(ws, 1000, "runtime.socket.superseded"); + return; + } - if (socket && socket.readyState === WebSocket.OPEN) { - socket.close(1011, "runtime.socket.error"); + if (ws.readyState === WebSocket.OPEN) { + ws.close(1011, "runtime.socket.error"); } else { - await this.#terminalState.finalize(); + await this.#finalizeTerminalState(epoch); } } @@ -282,59 +339,97 @@ export class DriverInstance extends DurableObject implements DriverInstanceHttpH return json({ error: "Boot token is invalid." }, { status: 401 }); } - if (this.#state.terminalized) { - await this.#terminalState.resetForReuse(); - } + return this.ctx.blockConcurrencyWhile(async () => { + const validation = await validateDriverInstanceBootTokenHash(this.env, bootTokenHash); + const driverInstanceId = this.#state.requireDriverInstanceId(); - const claim = await claimDriverInstanceByBootTokenHash(this.env, bootTokenHash); + if ( + validation.driverInstanceId === null || + validation.generation === null || + validation.driverInstanceId !== driverInstanceId + ) { + return json({ error: validation.error ?? "Boot token is invalid." }, { status: 401 }); + } - if ( - claim.driverInstanceId === null || - claim.generation === null || - claim.driverInstanceId !== this.#state.requireDriverInstanceId() - ) { - return json({ error: claim.error ?? "Boot token is invalid." }, { status: 401 }); - } + let shouldReset = this.#state.terminalized; - const connectedAt = Date.now(); - const connectionId = createPlatformId(); - const connected = await markDriverInstanceConnected(this.env, { - bootTokenHash, - connectedAt, - connectionId, - driverInstanceId: this.#state.requireDriverInstanceId(), - generation: claim.generation, - }); + if (shouldReset) { + if (!this.#state.terminalCleanupComplete) { + const epoch = this.#state.connectionEpoch(); - if (!connected) { - return json({ error: "Driver connection is no longer current." }, { status: 409 }); - } + if (epoch !== null) { + await this.#finalizeTerminalState(epoch); + } + } - const traceparent = url.searchParams.get("traceparent"); - const parsedTraceparent = isTruthy(traceparent) ? parseTraceparent(traceparent) : null; - const pair = new WebSocketPair(); - const [clientSocket, serverSocket] = [pair[0], pair[1]]; + if (validation.generation <= this.#state.requireDriverGeneration()) { + return json({ error: "Driver generation is no longer current." }, { status: 409 }); + } - this.#sockets.replaceDriverSockets(); - this.#sockets.acceptDriverSocket(serverSocket); + await this.#terminalState.prepareForReuse(); + } else if (this.#state.requireDriverGeneration() !== validation.generation) { + if (this.#state.connectionId !== null) { + return json({ error: "Driver generation is no longer current." }, { status: 409 }); + } - await this.#state.recordAcceptedConnection({ - connectedAt, - connectionId, - driverGeneration: claim.generation, - traceId: parsedTraceparent?.traceId ?? null, - }); + shouldReset = true; + await this.#terminalState.prepareForReuse(); + } - this.#withRuntimeLogContext(() => { - logInfo("runtime.socket.accepted", { + const claim = await claimDriverInstanceByBootTokenHash(this.env, bootTokenHash); + + if ( + claim.driverInstanceId !== driverInstanceId || + claim.generation !== validation.generation + ) { + return json({ error: claim.error ?? "Boot token is invalid." }, { status: 401 }); + } + + if (shouldReset) { + await this.#terminalState.resetForReuse(validation.generation); + } + + const connectedAt = Date.now(); + const connectionId = createPlatformId(); + const connected = await markDriverInstanceConnected(this.env, { + bootTokenHash, + connectedAt, connectionId, - driverInstanceId: this.#state.requireDriverInstanceId(), + driverInstanceId, + generation: validation.generation, }); - }); - return new Response(null, { - status: 101, - webSocket: clientSocket, + if (!connected) { + return json({ error: "Driver connection is no longer current." }, { status: 409 }); + } + + const traceparent = url.searchParams.get("traceparent"); + const parsedTraceparent = isTruthy(traceparent) ? parseTraceparent(traceparent) : null; + const pair = new WebSocketPair(); + const [clientSocket, serverSocket] = [pair[0], pair[1]]; + const epoch = { connectionId, generation: validation.generation }; + + this.#sockets.replaceDriverSockets(); + this.#sockets.acceptDriverSocket(serverSocket, epoch); + + await this.#state.recordAcceptedConnection({ + connectedAt, + connectionId, + driverGeneration: validation.generation, + traceId: parsedTraceparent?.traceId ?? null, + }); + + this.#withRuntimeLogContext(() => { + logInfo("runtime.socket.accepted", { + connectionId, + driverInstanceId, + }); + }); + + return new Response(null, { + status: 101, + webSocket: clientSocket, + }); }); } @@ -346,12 +441,17 @@ export class DriverInstance extends DurableObject implements DriverInstanceHttpH this.#identity.ensure(candidate); } - if (this.#state.terminalized) { - const status = await getDriverInstanceStatus(this.env, this.#state.driverInstanceId); + if (this.#state.driverGeneration === null) { + const identity = await getDriverInstanceLifecycleIdentity( + this.env, + this.#state.driverInstanceId, + ); - if (status === "provisioning" || status === "connecting" || status === "ready") { - await this.#terminalState.resetForReuse(); + if (identity === null) { + throw new Error("Driver instance record was not found."); } + + await this.#state.setDriverGeneration(identity.generation); } return this.#state.driverInstanceId; @@ -361,77 +461,117 @@ export class DriverInstance extends DurableObject implements DriverInstanceHttpH this.#identity.ensure(candidate), "driver instance id", ); - await this.#state.setDriverInstanceId(driverInstanceId); + const identity = await getDriverInstanceLifecycleIdentity(this.env, driverInstanceId); + + if (identity === null) { + throw new Error("Driver instance record was not found."); + } + + await this.#state.initializeDriverInstance(driverInstanceId, identity.generation); return driverInstanceId; } - async sendControlCommand(command: RuntimeCommand): Promise { - const socket = this.#sockets.getDriverSocket(); - - if (!socket || socket.readyState !== WebSocket.OPEN) { - const message = "Runtime driver control socket is not connected."; - await this.#state.setErrorMessage(message); - await this.#terminalState.finalize(); - throw new Error(message); - } + async sendControlCommand(generation: number, command: RuntimeCommand): Promise { + return this.ctx.blockConcurrencyWhile(async () => { + await this.#assertCurrentGeneration(generation); + const epoch = this.#state.requireConnectionEpoch(); + const socket = this.#sockets.getDriverSocket(epoch); + + if (!socket || socket.readyState !== WebSocket.OPEN) { + const message = "Runtime driver control socket is not connected."; + await this.#state.setConnectionErrorMessage(epoch, message); + await this.#finalizeTerminalState(epoch); + throw new Error(message); + } - await this.#rpcController.enqueueCommand(command); + await this.#rpcController.enqueueCommand(generation, command); + }); } - async destroy(reason: string): Promise { - if (this.#destroyed) { - return; - } + async destroy(generation: number, reason: string): Promise { + return this.ctx.blockConcurrencyWhile(async () => { + if (this.#destroyedGeneration !== null) { + if (this.#destroyedGeneration !== generation) { + throw new Error("Driver generation is no longer current."); + } - this.#destroyed = true; - this.#identity.clear(); - const socket = this.#sockets.getDriverSocket(); + return; + } + + await this.#assertCurrentGeneration(generation); + + return (this.#destroyTask ??= this.#destroy(generation, reason).finally(() => { + if (this.#destroyedGeneration === null) { + this.#destroyTask = null; + } + })); + }); + } + + async #destroy(generation: number, reason: string): Promise { + const socket = this.#sockets.getDriverSocket(this.#state.connectionEpoch()); if (socket?.readyState === WebSocket.OPEN) { socket.close(1000, reason); } - await this.#terminalState.destroy(reason); + await this.#rpcController.runAfterPendingEvents(() => this.#terminalState.destroy(reason)); + this.#identity.clear(); + this.#destroyedGeneration = generation; } - async fail(message: string): Promise { - await this.#state.setErrorMessage(message); + async fail(generation: number, message: string): Promise { + return this.ctx.blockConcurrencyWhile(async () => { + await this.#assertCurrentGeneration(generation); + const epoch = this.#state.requireConnectionEpoch(); + await this.#state.setConnectionErrorMessage(epoch, message); - const socket = this.#sockets.getDriverSocket(); + const socket = this.#sockets.getDriverSocket(epoch); - if (socket && socket.readyState === WebSocket.OPEN) { - socket.close(1011, "runtime.failed"); - return; - } + if (socket && socket.readyState === WebSocket.OPEN) { + socket.close(1011, "runtime.failed"); + return; + } - await this.#terminalState.finalize(); + await this.#finalizeTerminalState(epoch); + }); } - snapshot(): DriverInstanceSnapshot { - const socket = this.#sockets.getDriverSocket(); - return this.#state.snapshot(Boolean(socket && socket.readyState === WebSocket.OPEN)); + async #finalizeTerminalState(epoch: DriverInstanceConnectionEpoch): Promise { + await this.#rpcController.runAfterPendingEvents(() => this.#terminalState.finalize(epoch)); } - async waitForClose(timeoutMs: number): Promise { - return this.#state.waitForClose(timeoutMs); + async #assertCurrentGeneration(generation: number): Promise { + if (this.#state.requireDriverGeneration() !== generation) { + throw new Error("Driver generation is no longer current."); + } + + const driverInstanceId = this.#state.requireDriverInstanceId(); + const identity = await getDriverInstanceLifecycleIdentity(this.env, driverInstanceId); + + if (identity === null || identity.generation !== generation) { + throw new Error("Driver generation is no longer current."); + } } - async waitForHeartbeat( - afterCount: number, - timeoutMs: number, - ): Promise { - return this.#state.waitForHeartbeat(afterCount, timeoutMs); + snapshot(): DriverInstanceSnapshot { + const socket = this.#sockets.getDriverSocket(this.#state.connectionEpoch()); + return this.#state.snapshot(Boolean(socket && socket.readyState === WebSocket.OPEN)); } - async waitForHello(timeoutMs: number): Promise { - return this.#state.waitForHello(timeoutMs); + async waitForClose( + generation: number, + timeoutMs: number, + ): Promise { + return this.#state.waitForClose(generation, timeoutMs); } - async waitForReady(timeoutMs: number): Promise { - return this.#state.waitForReady(timeoutMs); + async waitForReady(generation: number, timeoutMs: number): Promise { + return this.#state.waitForReady(generation, timeoutMs); } - async #getRuntimeSessionLink() { + async #getRuntimeSessionLink(epoch: DriverInstanceConnectionEpoch) { + this.#state.assertConnectionEpoch(epoch); const existing = this.#state.runtimeSessionLink; if (existing !== null && !runtimeSessionLinkNeedsRefresh(existing)) { @@ -439,13 +579,18 @@ export class DriverInstance extends DurableObject implements DriverInstanceHttpH } const link = await getRuntimeSessionLink(this.env.DB, this.#state.requireDriverInstanceId()); + this.#state.assertConnectionEpoch(epoch); this.#state.setRuntimeSessionLink(link); return link; } - async #appendTransportRpcErrorEvent(error: unknown): Promise { + async #appendTransportRpcErrorEvent( + error: unknown, + epoch: DriverInstanceConnectionEpoch, + ): Promise { try { - const link = await this.#getRuntimeSessionLink(); + const link = await this.#getRuntimeSessionLink(epoch); + this.#state.assertConnectionEpoch(epoch); if (!isTruthy(link.agentId) || !isTruthy(link.sessionId)) { return; @@ -475,9 +620,13 @@ export class DriverInstance extends DurableObject implements DriverInstanceHttpH } } - async #appendTransportWsDisconnectedEvent(close: DriverInstanceCloseSnapshot): Promise { + async #appendTransportWsDisconnectedEvent( + close: DriverInstanceCloseSnapshot, + epoch: DriverInstanceConnectionEpoch, + ): Promise { try { - const link = await this.#getRuntimeSessionLink(); + const link = await this.#getRuntimeSessionLink(epoch); + this.#state.assertConnectionEpoch(epoch); if (!isTruthy(link.agentId) || !isTruthy(link.sessionId)) { return; @@ -507,6 +656,16 @@ export class DriverInstance extends DurableObject implements DriverInstanceHttpH } } + #isCurrentSocketEpoch(ws: WebSocket, epoch: DriverInstanceConnectionEpoch): boolean { + return this.#sockets.isCurrentDriverSocket(ws, epoch, this.#state.connectionEpoch()); + } + + #closeSocket(ws: WebSocket, code: number, reason: string): void { + if (ws.readyState === WebSocket.OPEN) { + ws.close(code, reason); + } + } + #withRuntimeLogContext(fn: () => T): T { return runWithApiLogContext( { diff --git a/apps/api/src/modules/runtime/infrastructure/driver-instance/driver-event-canonicalization.ts b/apps/api/src/modules/runtime/infrastructure/driver-instance/driver-event-canonicalization.ts new file mode 100644 index 00000000..a2098107 --- /dev/null +++ b/apps/api/src/modules/runtime/infrastructure/driver-instance/driver-event-canonicalization.ts @@ -0,0 +1,127 @@ +import { DurableRunError } from "@mosoo/contracts/session-run"; +import { parseSchemaValue } from "@mosoo/contracts/validation"; +import type { DriverInstanceId, RuntimeEventId, SessionId, SessionRunId } from "@mosoo/id"; +import { + createRuntimeEvent, + parseRuntimeEventEnvelope, + readRuntimeEventPayload, +} from "@mosoo/runtime-events"; +import type { RuntimeEventEnvelope } from "@mosoo/runtime-events"; + +import { createSessionRunTerminalSourceId } from "../../domain/session-run-terminal-event-id"; +import type { CanonicalDriverEventEnvelope } from "./event-types"; + +interface DriverEventEnvelopeInput { + readonly event: unknown; + readonly eventId: string; + readonly occurredAt?: string | null | undefined; +} + +function isTerminalRunEventKind( + kind: RuntimeEventEnvelope["kind"], +): kind is "run.cancelled" | "run.completed" | "run.failed" { + return kind === "run.cancelled" || kind === "run.completed" || kind === "run.failed"; +} + +export function createCanonicalDriverRunFailedEvent(input: { + driverInstanceId: DriverInstanceId; + error: typeof DurableRunError.infer; + id: RuntimeEventId; + occurredAt: string; + runId: SessionRunId; + runtimeId: string; + sessionId: SessionId; + traceId: string; +}): RuntimeEventEnvelope { + return createRuntimeEvent({ + actor: "driver", + delivery: "lossless", + driverInstanceId: input.driverInstanceId, + id: input.id, + kind: "run.failed", + occurredAt: input.occurredAt, + origin: "driver", + payload: { + error: input.error, + lifecycle: "IDLE", + recoverable: input.error.retryable, + }, + runId: input.runId, + runtimeId: input.runtimeId, + sessionId: input.sessionId, + sourceEventId: createSessionRunTerminalSourceId(input.runId, "run.failed"), + traceId: input.traceId, + visibility: "participant", + }); +} + +export function canonicalizeDriverEventEnvelope( + envelope: DriverEventEnvelopeInput, + input: { traceId: string | null }, +): CanonicalDriverEventEnvelope { + const parsedEvent = parseRuntimeEventEnvelope(envelope.event); + + if (parsedEvent.sourceEventId !== undefined && parsedEvent.sourceEventId !== envelope.eventId) { + throw new Error("Runtime driver event source id does not match the driver envelope."); + } + + const event = (() => { + if (parsedEvent.runId === undefined) { + return parsedEvent; + } + + if (input.traceId === null) { + throw new Error("Runtime driver event run is missing its authoritative trace identity."); + } + + if (parsedEvent.traceId !== undefined && parsedEvent.traceId !== input.traceId) { + throw new Error("Runtime driver event trace id does not match its Session Run."); + } + + return parsedEvent.traceId === undefined + ? { ...parsedEvent, traceId: input.traceId } + : parsedEvent; + })(); + + if (!isTerminalRunEventKind(event.kind) || event.runId === undefined) { + return { ...envelope, event }; + } + + if (event.kind !== "run.failed") { + return { + ...envelope, + event: { + ...event, + payload: { + ...readRuntimeEventPayload(event), + lifecycle: "IDLE", + }, + sourceEventId: createSessionRunTerminalSourceId(event.runId, event.kind), + }, + }; + } + + if ( + event.driverInstanceId === undefined || + event.runtimeId === undefined || + event.traceId === undefined + ) { + throw new Error("Runtime driver failure event is missing its canonical execution identity."); + } + + const error = parseSchemaValue(DurableRunError, readRuntimeEventPayload(event)["error"]); + + return { + ...envelope, + event: createCanonicalDriverRunFailedEvent({ + driverInstanceId: event.driverInstanceId, + error, + id: event.id, + occurredAt: event.occurredAt, + runId: event.runId, + runtimeId: event.runtimeId, + sessionId: event.sessionId, + traceId: event.traceId, + }), + }; +} diff --git a/apps/api/src/modules/runtime/infrastructure/driver-instance/driver-event-receipts.ts b/apps/api/src/modules/runtime/infrastructure/driver-instance/driver-event-receipts.ts deleted file mode 100644 index 014184ce..00000000 --- a/apps/api/src/modules/runtime/infrastructure/driver-instance/driver-event-receipts.ts +++ /dev/null @@ -1,90 +0,0 @@ -import type { DriverEventEnvelope } from "@mosoo/agent-driver/events"; -import type { DriverEventReceipt } from "@mosoo/agent-driver/orpc"; - -const MAX_PROCESSED_DRIVER_EVENT_RECEIPTS = 8192; - -export function filterNewDriverEvents(input: { - events: readonly DriverEventEnvelope[]; - processedReceipts: Map; -}): DriverEventEnvelope[] { - const seenEventIds = new Set(); - - return input.events.filter((envelope) => { - if (envelope.eventId.length === 0) { - return true; - } - - if (seenEventIds.has(envelope.eventId)) { - return false; - } - - seenEventIds.add(envelope.eventId); - - return !input.processedReceipts.has(envelope.eventId); - }); -} - -export function createReceiptsForDriverEvents(input: { - events: readonly DriverEventEnvelope[]; - nextSeq: number; -}): { - nextSeq: number; - receipts: DriverEventReceipt[]; -} { - let nextSeq = input.nextSeq; - const receipts = input.events.map((envelope) => { - nextSeq += 1; - - return { - ...(envelope.eventId.length > 0 ? { eventId: envelope.eventId } : {}), - seq: nextSeq, - type: envelope.event.kind, - }; - }); - - return { nextSeq, receipts }; -} - -export function readReceiptsForProcessedDriverEvents(input: { - events: readonly DriverEventEnvelope[]; - processedReceipts: Map; -}): DriverEventReceipt[] { - const receipts: DriverEventReceipt[] = []; - - for (const envelope of input.events) { - if (envelope.eventId.length === 0) { - continue; - } - - const receipt = input.processedReceipts.get(envelope.eventId); - - if (receipt !== undefined) { - receipts.push(receipt); - } - } - - return receipts; -} - -export function rememberDriverEventReceipts(input: { - processedReceipts: Map; - receipts: DriverEventReceipt[]; -}): void { - for (const receipt of input.receipts) { - if (typeof receipt.eventId !== "string" || receipt.eventId.length === 0) { - continue; - } - - input.processedReceipts.set(receipt.eventId, receipt); - } - - while (input.processedReceipts.size > MAX_PROCESSED_DRIVER_EVENT_RECEIPTS) { - const oldest = input.processedReceipts.keys().next().value; - - if (typeof oldest !== "string") { - return; - } - - input.processedReceipts.delete(oldest); - } -} diff --git a/apps/api/src/modules/runtime/infrastructure/driver-instance/driver-instance-record.repository.ts b/apps/api/src/modules/runtime/infrastructure/driver-instance/driver-instance-record.repository.ts index 900438ae..6add8cbf 100644 --- a/apps/api/src/modules/runtime/infrastructure/driver-instance/driver-instance-record.repository.ts +++ b/apps/api/src/modules/runtime/infrastructure/driver-instance/driver-instance-record.repository.ts @@ -1,12 +1,37 @@ import { DRIVER_PROTOCOL_VERSION } from "@mosoo/agent-driver/boot"; import type { DriverRuntime } from "@mosoo/agent-driver/runtime"; -import { driverCommandsTable, driverInstanceMcpGrantsTable, driverInstancesTable } from "@mosoo/db"; +import { + driverCommandsTable, + driverInstanceMcpGrantsTable, + driverInstancesTable, + externalToolEffectsTable, + sandboxesTable, + sandboxSessionsTable, + sessionRunsTable, + sessionsTable, +} from "@mosoo/db"; import type { DriverInstanceId, SandboxId, SessionId } from "@mosoo/id"; -import { and, desc, eq, gt, inArray, isNotNull, notInArray, or, sql } from "drizzle-orm"; +import { + and, + desc, + eq, + exists, + gt, + inArray, + isNotNull, + isNull, + notExists, + or, + sql, +} from "drizzle-orm"; import type { SQL } from "drizzle-orm"; import type { ApiBindings } from "../../../../platform/cloudflare/worker-types"; -import { getAppDatabase, runAppDatabaseBatch } from "../../../../platform/db/drizzle"; +import { + getAppDatabase, + getD1ChangeCount, + runAppDatabaseBatch, +} from "../../../../platform/db/drizzle"; import { currentTimestampMs } from "../../../../time"; import { REUSABLE_DRIVER_INSTANCE_STATUSES, @@ -18,6 +43,8 @@ import { DRIVER_COLD_READY_TIMEOUT_MS, RUNTIME_SOCKET_TIMEOUT_MS, } from "../../domain/runtime-config"; +import { ACTIVE_SESSION_RUN_STATUSES } from "../../domain/session-run-lifecycle.machine"; +import type { RuntimeRunProvisioningLease } from "../runtime-subject-lifecycle/runtime-provisioning-lease-store"; import type { DriverInstanceMcpGrantRecord } from "./mcp-grants.repository"; import { driverInstanceExpiresAt } from "./status"; import type { DriverInstanceStatus } from "./status"; @@ -35,6 +62,186 @@ export type CreateDriverInstanceRecordResult = status: "skipped"; }; +function selectedValue(value: Value, alias: string) { + return sql`${value}`.as(alias); +} + +async function insertProvisioningOwnedDriverRecord( + database: D1Database, + input: { + readonly driverRecord: typeof driverInstancesTable.$inferInsert; + readonly lease: RuntimeRunProvisioningLease; + }, +): Promise<{ bootTokenExpiresAt: number; generation: number } | null> { + const { driverRecord: record, lease } = input; + if ( + lease.sandboxIncarnation === null || + lease.sandboxSessionId === null || + record.bootTokenExpiresAt === undefined || + record.generation === undefined || + record.sandboxId !== lease.sandboxId || + record.sandboxIncarnation !== lease.sandboxIncarnation || + record.sandboxSessionId !== lease.sessionId + ) { + throw new Error("Driver provisioning requires a complete immutable sandbox target."); + } + + const inserted = await database + .prepare( + ` + INSERT INTO driver_instance ( + boot_token_expires_at, boot_token_hash, created_at, expires_at, + generation, heartbeat_count, id, protocol, protocol_version, + restart_count, runtime, sandbox_id, sandbox_incarnation, + sandbox_session_id, status, status_changed_at, status_event, + status_seq, status_source, updated_at + ) + SELECT ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ? + FROM session AS provisioning + INNER JOIN session_run AS run + ON run.id = provisioning.runtime_provisioning_run_id + AND run.session_id = provisioning.id + INNER JOIN sandbox AS subject + ON subject.id = provisioning.runtime_provisioning_sandbox_id + INNER JOIN sandbox_session AS conversation + ON conversation.session_id = provisioning.id + AND conversation.sandbox_id = subject.id + WHERE provisioning.id = ? + AND provisioning.runtime_provisioning_operation_id = ? + AND provisioning.runtime_provisioning_run_id = ? + AND provisioning.runtime_provisioning_sandbox_id = ? + AND provisioning.runtime_provisioning_sandbox_incarnation = ? + AND provisioning.runtime_provisioning_sandbox_session_id = ? + AND provisioning.last_run_id = run.id + AND provisioning.status = 'RUNNING' + AND provisioning.archived_at IS NULL + AND provisioning.cleanup_operation_kind IS NULL + AND provisioning.status_operation_id IS NULL + AND run.status IN ('queued', 'booting', 'running', 'waiting_input') + AND subject.incarnation = ? + AND subject.status = 'active' + AND subject.claim_owner IS NULL + AND subject.operation_kind IS NULL + AND subject.status_operation_id IS NULL + AND conversation.sandbox_incarnation = ? + AND conversation.cloudflare_session_id = ? + AND conversation.status = 'active' + ON CONFLICT DO NOTHING + `, + ) + .bind( + record.bootTokenExpiresAt, + record.bootTokenHash, + record.createdAt, + record.expiresAt, + record.generation, + record.heartbeatCount, + record.id, + record.protocol, + record.protocolVersion, + record.restartCount, + record.runtime, + record.sandboxId, + record.sandboxIncarnation, + record.sandboxSessionId, + record.status, + record.statusChangedAt, + record.statusEvent, + record.statusSeq, + record.statusSource, + record.updatedAt, + lease.sessionId, + lease.operationId, + lease.runId, + lease.sandboxId, + lease.sandboxIncarnation, + lease.sandboxSessionId, + lease.sandboxIncarnation, + lease.sandboxIncarnation, + lease.sandboxSessionId, + ) + .run(); + + return getD1ChangeCount(inserted) === 1 + ? { bootTokenExpiresAt: record.bootTokenExpiresAt, generation: record.generation } + : null; +} + +export async function runtimeProvisioningDriverLaunchIsOwned( + database: D1Database, + input: { + readonly bootTokenHash: Uint8Array; + readonly driverGeneration: number; + readonly driverInstanceId: DriverInstanceId; + readonly lease: RuntimeRunProvisioningLease; + }, +): Promise { + const { lease } = input; + if (lease.sandboxIncarnation === null || lease.sandboxSessionId === null) { + return false; + } + + const row = await getAppDatabase(database) + .select({ id: driverInstancesTable.id }) + .from(sessionsTable) + .innerJoin( + sessionRunsTable, + and( + eq(sessionRunsTable.id, sessionsTable.runtimeProvisioningRunId), + eq(sessionRunsTable.sessionId, sessionsTable.id), + ), + ) + .innerJoin(sandboxesTable, eq(sandboxesTable.id, sessionsTable.runtimeProvisioningSandboxId)) + .innerJoin( + sandboxSessionsTable, + and( + eq(sandboxSessionsTable.sessionId, sessionsTable.id), + eq(sandboxSessionsTable.sandboxId, sandboxesTable.id), + ), + ) + .innerJoin( + driverInstancesTable, + and( + eq(driverInstancesTable.id, input.driverInstanceId), + eq(driverInstancesTable.sandboxSessionId, sessionsTable.id), + ), + ) + .where( + and( + eq(sessionsTable.id, lease.sessionId), + eq(sessionsTable.runtimeProvisioningOperationId, lease.operationId), + eq(sessionsTable.runtimeProvisioningRunId, lease.runId), + eq(sessionsTable.runtimeProvisioningSandboxId, lease.sandboxId), + eq(sessionsTable.runtimeProvisioningSandboxIncarnation, lease.sandboxIncarnation), + eq(sessionsTable.runtimeProvisioningSandboxSessionId, lease.sandboxSessionId), + eq(sessionsTable.lastRunId, lease.runId), + eq(sessionsTable.status, "RUNNING"), + isNull(sessionsTable.archivedAt), + isNull(sessionsTable.cleanupOperationKind), + isNull(sessionsTable.statusOperationId), + inArray(sessionRunsTable.status, ACTIVE_SESSION_RUN_STATUSES), + eq(sandboxesTable.incarnation, lease.sandboxIncarnation), + eq(sandboxesTable.status, "active"), + isNull(sandboxesTable.claimOwner), + isNull(sandboxesTable.operationKind), + isNull(sandboxesTable.statusOperationId), + eq(sandboxSessionsTable.sandboxIncarnation, lease.sandboxIncarnation), + eq(sandboxSessionsTable.sandboxSessionId, lease.sandboxSessionId), + eq(sandboxSessionsTable.status, "active"), + eq(driverInstancesTable.generation, input.driverGeneration), + eq(driverInstancesTable.bootTokenHash, input.bootTokenHash), + eq(driverInstancesTable.sandboxId, lease.sandboxId), + eq(driverInstancesTable.sandboxIncarnation, lease.sandboxIncarnation), + inArray(driverInstancesTable.status, REUSABLE_DRIVER_INSTANCE_STATUSES), + isNull(driverInstancesTable.statusOperationId), + ), + ) + .limit(1) + .get(); + + return row !== undefined; +} + export async function createDriverInstanceRecord( bindings: ApiBindings, input: { @@ -43,8 +250,10 @@ export async function createDriverInstanceRecord( driverInstanceId: DriverInstanceId; runtime: DriverRuntime; sandboxId: SandboxId; + sandboxIncarnation: number; sandboxSessionId: SessionId; mcpGrants?: DriverInstanceMcpGrantRecord[]; + runtimeProvisioningLease?: RuntimeRunProvisioningLease; }, ): Promise { const now = currentTimestampMs(); @@ -84,6 +293,7 @@ export async function createDriverInstanceRecord( restartCount: 0, runtime: input.runtime, sandboxId: input.sandboxId, + sandboxIncarnation: input.sandboxIncarnation, sandboxSessionId: input.sandboxSessionId, status: "provisioning", statusChangedAt: now, @@ -96,16 +306,20 @@ export async function createDriverInstanceRecord( if (input.conflictStrategy === "insert-only") { const database = getAppDatabase(bindings.DB); - const inserted = - (await database - .insert(driverInstancesTable) - .values(driverRecord) - .onConflictDoNothing() - .returning({ - bootTokenExpiresAt: driverInstancesTable.bootTokenExpiresAt, - generation: driverInstancesTable.generation, + const inserted = input.runtimeProvisioningLease + ? await insertProvisioningOwnedDriverRecord(bindings.DB, { + driverRecord, + lease: input.runtimeProvisioningLease, }) - .get()) ?? null; + : ((await database + .insert(driverInstancesTable) + .values(driverRecord) + .onConflictDoNothing() + .returning({ + bootTokenExpiresAt: driverInstancesTable.bootTokenExpiresAt, + generation: driverInstancesTable.generation, + }) + .get()) ?? null); if (inserted === null) { return { @@ -127,65 +341,141 @@ export async function createDriverInstanceRecord( }; } + const [replacement] = await runAppDatabaseBatch(bindings.DB, (batchDb) => { + const acceptedCommand = batchDb + .select({ id: driverCommandsTable.id }) + .from(driverCommandsTable) + .where( + and( + eq(driverCommandsTable.id, externalToolEffectsTable.commandId), + eq(driverCommandsTable.status, "accepted"), + ), + ); + const protectedEffect = batchDb + .select({ id: externalToolEffectsTable.id }) + .from(externalToolEffectsTable) + .where( + and( + eq(externalToolEffectsTable.driverInstanceId, input.driverInstanceId), + or( + inArray(externalToolEffectsTable.status, ["claimed", "unknown"]), + exists(acceptedCommand), + ), + ), + ); + const replacementAllowed = and(sql`status_operation_id IS NULL`, notExists(protectedEffect))!; + const replacementDriver = and( + eq(driverInstancesTable.id, input.driverInstanceId), + eq(driverInstancesTable.bootTokenHash, input.bootTokenHash), + ); + const replacementCommitted = exists( + batchDb + .select({ id: driverInstancesTable.id }) + .from(driverInstancesTable) + .where(replacementDriver), + ); + + return [ + batchDb + .insert(driverInstancesTable) + .values(driverRecord) + .onConflictDoUpdate({ + set: { + bootTokenExpiresAt: sql`excluded.boot_token_expires_at`, + bootTokenHash: sql`excluded.boot_token_hash`, + bootTokenUsedAt: null, + closeCode: null, + closeReason: null, + connectionId: null, + createdAt: sql`excluded.created_at`, + driverPid: null, + driverStartedAt: null, + driverVersion: null, + errorMessage: null, + expiresAt: sql`excluded.expires_at`, + generation: sql`${driverInstancesTable.generation} + 1`, + heartbeatCount: 0, + lastHeartbeatAt: null, + processId: null, + protocol: sql`excluded.protocol`, + protocolVersion: sql`excluded.protocol_version`, + restartCount: sql`${driverInstancesTable.restartCount} + 1`, + runtime: sql`excluded.runtime`, + sandboxId: sql`excluded.sandbox_id`, + sandboxIncarnation: sql`excluded.sandbox_incarnation`, + sandboxSessionId: sql`excluded.sandbox_session_id`, + status: sql`excluded.status`, + statusChangedAt: sql`excluded.status_changed_at`, + statusEvent: sql`excluded.status_event`, + statusOperationId: null, + statusSeq: sql`${driverInstancesTable.statusSeq} + 1`, + statusSource: sql`excluded.status_source`, + updatedAt: sql`excluded.updated_at`, + }, + setWhere: replacementAllowed, + target: driverInstancesTable.id, + }), + batchDb + .delete(driverCommandsTable) + .where( + and( + eq(driverCommandsTable.driverInstanceId, input.driverInstanceId), + replacementCommitted, + ), + ), + batchDb + .delete(driverInstanceMcpGrantsTable) + .where( + and( + eq(driverInstanceMcpGrantsTable.driverInstanceId, input.driverInstanceId), + replacementCommitted, + ), + ), + ...mcpGrantRows.map((grant) => + batchDb.insert(driverInstanceMcpGrantsTable).select( + batchDb + .select({ + authType: selectedValue(grant.authType, "auth_type"), + authorizationState: selectedValue(grant.authorizationState, "authorization_state"), + canInvalidate: selectedValue(grant.canInvalidate, "can_invalidate"), + canRefresh: selectedValue(grant.canRefresh, "can_refresh"), + createdAt: selectedValue(grant.createdAt, "created_at"), + credentialId: selectedValue(grant.credentialId, "credential_id"), + driverInstanceId: driverInstancesTable.id, + appId: selectedValue(grant.appId, "app_id"), + serverId: selectedValue(grant.serverId, "server_id"), + updatedAt: selectedValue(grant.updatedAt, "updated_at"), + }) + .from(driverInstancesTable) + .where(replacementDriver), + ), + ), + ]; + }); + + if (getD1ChangeCount(replacement) === 0) { + throw new Error("Driver instance replacement is blocked by a protected external effect."); + } + const database = getAppDatabase(bindings.DB); - await runAppDatabaseBatch(bindings.DB, (batchDb) => [ - batchDb - .delete(driverCommandsTable) - .where(eq(driverCommandsTable.driverInstanceId, input.driverInstanceId)), - batchDb - .delete(driverInstanceMcpGrantsTable) - .where(eq(driverInstanceMcpGrantsTable.driverInstanceId, input.driverInstanceId)), - ]); const upserted = (await database - .insert(driverInstancesTable) - .values(driverRecord) - .onConflictDoUpdate({ - set: { - bootTokenExpiresAt: sql`excluded.boot_token_expires_at`, - bootTokenHash: sql`excluded.boot_token_hash`, - bootTokenUsedAt: null, - closeCode: null, - closeReason: null, - connectionId: null, - createdAt: sql`excluded.created_at`, - driverPid: null, - driverStartedAt: null, - driverVersion: null, - errorMessage: null, - expiresAt: sql`excluded.expires_at`, - generation: sql`${driverInstancesTable.generation} + 1`, - heartbeatCount: 0, - lastHeartbeatAt: null, - processId: null, - protocol: sql`excluded.protocol`, - protocolVersion: sql`excluded.protocol_version`, - restartCount: sql`${driverInstancesTable.restartCount} + 1`, - runtime: sql`excluded.runtime`, - sandboxId: sql`excluded.sandbox_id`, - sandboxSessionId: sql`excluded.sandbox_session_id`, - status: sql`excluded.status`, - statusChangedAt: sql`excluded.status_changed_at`, - statusEvent: sql`excluded.status_event`, - statusOperationId: null, - statusSeq: sql`${driverInstancesTable.statusSeq} + 1`, - statusSource: sql`excluded.status_source`, - updatedAt: sql`excluded.updated_at`, - }, - target: driverInstancesTable.id, - }) - .returning({ + .select({ bootTokenExpiresAt: driverInstancesTable.bootTokenExpiresAt, generation: driverInstancesTable.generation, }) + .from(driverInstancesTable) + .where( + and( + eq(driverInstancesTable.id, input.driverInstanceId), + eq(driverInstancesTable.bootTokenHash, input.bootTokenHash), + ), + ) + .limit(1) .get()) ?? null; - if (mcpGrantRows.length > 0) { - await database.insert(driverInstanceMcpGrantsTable).values(mcpGrantRows).run(); - } - if (upserted === null) { - throw new Error("Driver instance record was not created."); + throw new Error("Driver instance record was replaced before provisioning could claim it."); } return { @@ -229,6 +519,7 @@ export async function getDriverInstanceRecord( ): Promise<{ generation: number; sandboxId: SandboxId; + sandboxIncarnation: number; sandboxSessionId: SessionId; status: DriverInstanceStatus; } | null> { @@ -237,6 +528,7 @@ export async function getDriverInstanceRecord( .select({ generation: driverInstancesTable.generation, sandboxId: driverInstancesTable.sandboxId, + sandboxIncarnation: driverInstancesTable.sandboxIncarnation, sandboxSessionId: driverInstancesTable.sandboxSessionId, status: driverInstancesTable.status, }) @@ -303,6 +595,7 @@ export async function getReusableDriverInstanceRecord( database: D1Database, input: { sandboxId: SandboxId; + sandboxIncarnation: number; sandboxSessionId: SessionId; }, ): Promise<{ @@ -321,6 +614,7 @@ export async function getReusableDriverInstanceRecord( .where( and( eq(driverInstancesTable.sandboxId, input.sandboxId), + eq(driverInstancesTable.sandboxIncarnation, input.sandboxIncarnation), eq(driverInstancesTable.sandboxSessionId, input.sandboxSessionId), inArray(driverInstancesTable.status, REUSABLE_DRIVER_INSTANCE_STATUSES), ), @@ -343,7 +637,8 @@ export async function recordRuntimeProcessStarted( const now = currentTimestampMs(); const conditions: SQL[] = [ eq(driverInstancesTable.id, driverInstanceId), - notInArray(driverInstancesTable.status, ["stopped", "failed"]), + inArray(driverInstancesTable.status, REUSABLE_DRIVER_INSTANCE_STATUSES), + isNull(driverInstancesTable.statusOperationId), ]; if (options.expectedBootTokenHash !== undefined) { diff --git a/apps/api/src/modules/runtime/infrastructure/driver-instance/driver-instance-token.repository.ts b/apps/api/src/modules/runtime/infrastructure/driver-instance/driver-instance-token.repository.ts index 06ad29a8..c3db90f3 100644 --- a/apps/api/src/modules/runtime/infrastructure/driver-instance/driver-instance-token.repository.ts +++ b/apps/api/src/modules/runtime/infrastructure/driver-instance/driver-instance-token.repository.ts @@ -11,19 +11,32 @@ import type { DriverInstanceStatus } from "./status"; interface DriverInstanceTokenRow { boot_token_expires_at: number; boot_token_used_at: number | null; + connection_id: string | null; generation: number; id: DriverInstanceId; status: DriverInstanceStatus; } -export async function claimDriverInstanceByBootTokenHash( - bindings: ApiBindings, - bootTokenHash: Uint8Array, -): Promise<{ +interface DriverInstanceTokenValidation { driverInstanceId: DriverInstanceId | null; error: string | null; generation: number | null; -}> { +} + +export async function validateDriverInstanceBootTokenHash( + bindings: ApiBindings, + bootTokenHash: Uint8Array, +): Promise { + return validateDriverInstanceTokenRow( + await readDriverInstanceTokenRow(bindings, bootTokenHash), + currentTimestampMs(), + ); +} + +export async function claimDriverInstanceByBootTokenHash( + bindings: ApiBindings, + bootTokenHash: Uint8Array, +): Promise { const now = currentTimestampMs(); const claimed = (await getAppDatabase(bindings.DB) @@ -59,8 +72,16 @@ export async function claimDriverInstanceByBootTokenHash( }; } - const row = await readDriverInstanceTokenRow(bindings, bootTokenHash); + return validateDriverInstanceTokenRow( + await readDriverInstanceTokenRow(bindings, bootTokenHash), + now, + ); +} +function validateDriverInstanceTokenRow( + row: DriverInstanceTokenRow | null, + now: number, +): DriverInstanceTokenValidation { if (!row) { return { driverInstanceId: null, @@ -77,6 +98,14 @@ export async function claimDriverInstanceByBootTokenHash( }; } + if (row.status === "connecting" && row.boot_token_used_at !== null) { + return { + driverInstanceId: row.id, + error: null, + generation: row.generation, + }; + } + if (row.boot_token_used_at !== null || row.status !== "provisioning") { return { driverInstanceId: null, @@ -86,9 +115,9 @@ export async function claimDriverInstanceByBootTokenHash( } return { - driverInstanceId: null, - error: "Boot token is invalid.", - generation: null, + driverInstanceId: row.id, + error: null, + generation: row.generation, }; } @@ -109,6 +138,7 @@ async function readDriverInstanceTokenRow( .select({ bootTokenExpiresAt: driverInstancesTable.bootTokenExpiresAt, bootTokenUsedAt: driverInstancesTable.bootTokenUsedAt, + connectionId: driverInstancesTable.connectionId, generation: driverInstancesTable.generation, id: driverInstancesTable.id, status: driverInstancesTable.status, @@ -125,6 +155,7 @@ async function readDriverInstanceTokenRow( return { boot_token_expires_at: row.bootTokenExpiresAt, boot_token_used_at: row.bootTokenUsedAt, + connection_id: row.connectionId, generation: row.generation, id: row.id, status: row.status, diff --git a/apps/api/src/modules/runtime/infrastructure/driver-instance/event-link-assertion.ts b/apps/api/src/modules/runtime/infrastructure/driver-instance/event-link-assertion.ts index b8abef21..06a4a48c 100644 --- a/apps/api/src/modules/runtime/infrastructure/driver-instance/event-link-assertion.ts +++ b/apps/api/src/modules/runtime/infrastructure/driver-instance/event-link-assertion.ts @@ -26,6 +26,8 @@ const runBoundRuntimeEventKinds = new Set([ "permission.resolved", "permission.review.completed", "permission.review.started", + "runtime.resume.updated", + "usage.updated", ]); function runtimeEventRequiresRunLink(kind: RuntimeEventKind): boolean { @@ -53,6 +55,14 @@ export function assertRuntimeEventMatchesDriverLink( throw new Error("Runtime driver event driver instance id does not match the request."); } + if ((event.runtimeId ?? null) !== input.link.runtimeId) { + throw new Error("Runtime driver event runtime id does not match the driver session link."); + } + + if (event.runId !== undefined && (event.traceId ?? null) !== input.link.traceId) { + throw new Error("Runtime driver event trace id does not match the driver session link."); + } + if (event.runId === undefined && !runtimeEventRequiresRunLink(event.kind)) { return; } @@ -69,14 +79,3 @@ export function assertRuntimeEventMatchesDriverLink( throw new Error("Runtime agent task snapshot requires an active session run."); } } - -export function assertRuntimeEventMatchesDriverEnvelope( - event: RuntimeEventEnvelope, - input: { - eventId: string; - }, -): void { - if (event.sourceEventId !== undefined && event.sourceEventId !== input.eventId) { - throw new Error("Runtime driver event source id does not match the driver envelope."); - } -} diff --git a/apps/api/src/modules/runtime/infrastructure/driver-instance/event-persistence.ts b/apps/api/src/modules/runtime/infrastructure/driver-instance/event-persistence.ts index f2d5dae3..6ea29f15 100644 --- a/apps/api/src/modules/runtime/infrastructure/driver-instance/event-persistence.ts +++ b/apps/api/src/modules/runtime/infrastructure/driver-instance/event-persistence.ts @@ -1,20 +1,21 @@ -import { sessionEventsTable, sessionsTable } from "@mosoo/db"; import type { DriverInstanceId } from "@mosoo/id"; -import { and, eq, isNull } from "drizzle-orm"; import { captureServerProductEvent, SERVER_PRODUCT_ANALYTICS_EVENTS, } from "../../../../platform/analytics/product-analytics"; import type { ApiBindings } from "../../../../platform/cloudflare/worker-types"; -import { getAppDatabase } from "../../../../platform/db/drizzle"; -import { currentTimestampMs } from "../../../../time"; -import { createSessionRuntimeEvent } from "../../../sessions/application/session-event-write.service"; -import { upsertSessionModelCallUsage } from "../../../sessions/application/session-model-call.service"; +import { reduceMessageStreamLifecycle } from "../../../sessions/domain/session-event-stream-fold"; +import { createSessionRuntimeEventProjection } from "../../../sessions/domain/session-runtime-event-projection"; +import { + isSealedPublicSessionMessageStream, + readPublicSessionMessageStreamSealState, +} from "../../../sessions/infrastructure/session-message-event-stream.repository"; import { persistSessionRuntimeEvents } from "../../../sessions/infrastructure/session-runtime-event-store.repository"; -import { setSessionRunStatus } from "../session-runs/session-run-store.repository"; -import type { SessionRunTransitionOutcome } from "../session-runs/session-run-store.repository"; -import { persistAssistantMessageProjection } from "./assistant-message-projection"; +import { prepareAssistantMessageProjection } from "./assistant-message-projection"; +import type { PreparedAssistantMessageProjection } from "./assistant-message-projection"; +import { commitTerminalRunProjection } from "./completed-run-commit.repository"; +import type { DriverTerminalRunStatus } from "./completed-run-commit.repository"; import { compactRuntimeDriverRunTransitions } from "./event-projection"; import type { AppRuntimeDriverEventsResult, @@ -22,122 +23,181 @@ import type { RuntimeSessionLink, SessionLiveState, } from "./event-types"; -import { hasTerminalRuntimeDriverRunTransition } from "./run-transitions"; - -async function loadTerminalRunRelease() { - return import("./terminal-run-release"); -} -function getModelCallStatus( - transitions: ReturnType, -): "completed" | "failed" | "started" { - for (const transition of transitions) { - if (transition.status === "completed") { - return "completed"; - } +async function resolveFinalAssistantMessage( + database: D1Database, + input: { + messageId: string; + runId: NonNullable; + sessionId: NonNullable; + }, +): Promise<{ id: string }> { + const sealed = await isSealedPublicSessionMessageStream(database, { + processType: "agent.message.delta", + runId: input.runId, + sessionId: input.sessionId, + streamId: input.messageId, + }); - if (transition.status === "cancelled" || transition.status === "failed") { - return "failed"; - } + if (!sealed) { + throw new Error( + `Completed run final message ${input.messageId} has no sealed authoritative snapshot.`, + ); } - return "started"; + return { id: input.messageId }; } -type DriverProjectedSessionRunStatusInput = Parameters[1]; +async function isFinalAssistantMessageSealedAfterProjection( + database: D1Database, + input: { + messageId: string; + projection: AppRuntimeDriverEventsResult; + runId: NonNullable; + sessionId: NonNullable; + }, +): Promise { + let state = await readPublicSessionMessageStreamSealState(database, { + processType: "agent.message.delta", + runId: input.runId, + sessionId: input.sessionId, + streamId: input.messageId, + }); -function assertDriverProjectedSessionRunTransition(outcome: SessionRunTransitionOutcome): void { - switch (outcome.kind) { - case "applied": - case "duplicate": { - return; + for (const { event } of input.projection.runtimeEvents) { + if ( + event.kind !== "message.added" && + event.kind !== "message.cancelled" && + event.kind !== "message.completed" && + event.kind !== "message.delta" && + event.kind !== "message.failed" && + event.kind !== "message.started" + ) { + continue; } - case "stale": { - if (outcome.reason === "terminal_run") { - return; - } - throw new Error("Driver run transition lost a concurrent status race."); + const row = createSessionRuntimeEventProjection(event); + if (row.streamId !== input.messageId) { + continue; } - case "repair_needed": { - throw new Error("Driver run transition left the session lifecycle projection stale."); + if ( + event.sessionId !== input.sessionId || + row.runId !== input.runId || + row.processType !== "agent.message.delta" + ) { + throw new Error(`Session message stream ${input.messageId} has conflicting identity rows.`); } - case "rejected": { - throw new Error(`Driver run transition was rejected: ${outcome.reason}.`); + if (row.visibility !== "all_consumers") { + throw new Error(`Session message stream ${input.messageId} has mixed visibility.`); } + + state = reduceMessageStreamLifecycle(state, event.kind); } -} -async function setDriverProjectedSessionRunStatus( - database: D1Database, - input: DriverProjectedSessionRunStatusInput, -): Promise { - const outcome = await setSessionRunStatus(database, input); - assertDriverProjectedSessionRunTransition(outcome); - return outcome; + return state.sealed; } -function isStaleTerminalRunTransition(outcome: SessionRunTransitionOutcome | null): boolean { - return outcome?.kind === "stale" && outcome.reason === "terminal_run"; +function isTerminalRunTransition( + transition: RuntimeDriverRunTransition | undefined, +): transition is RuntimeDriverRunTransition & { status: DriverTerminalRunStatus } { + return transition !== undefined && transition.status !== "running"; } -function isMatchingStaleTerminalRunTransition(input: { - readonly outcome: SessionRunTransitionOutcome | null; - readonly transition: RuntimeDriverRunTransition | undefined; -}): boolean { - if (input.outcome?.kind !== "stale" || input.outcome.reason !== "terminal_run") { - return true; - } +function terminalRuntimeEventKind(status: DriverTerminalRunStatus) { + return `run.${status}` as const; +} - return input.transition !== undefined && input.outcome.currentStatus === input.transition.status; +export interface PersistProjectedRuntimeDriverEventsResult { + liveState: SessionLiveState | null; + persistedSourceEventIds: readonly string[]; } -function getRunDurationMs(outcome: SessionRunTransitionOutcome): number | null { +export async function preflightProjectedRuntimeDriverEvents( + database: D1Database, + projection: AppRuntimeDriverEventsResult, +): Promise { + const { link } = projection; + if (link.sessionId === null) { + return; + } + const [runTransition] = compactRuntimeDriverRunTransitions(projection.transitions); + const terminalRunTransition = isTerminalRunTransition(runTransition) ? runTransition : undefined; + + if (terminalRunTransition !== undefined && link.sessionRunId === null) { + throw new Error("Terminal run projection is missing its run scope."); + } if ( - outcome.kind !== "applied" || - outcome.run.startedAt === null || - outcome.run.completedAt === null + terminalRunTransition?.status === "completed" && + projection.finalAssistantMessageId !== null ) { - return null; + if (link.sessionRunId === null) { + throw new Error("Completed run final message is missing its run scope."); + } + if ( + !(await isFinalAssistantMessageSealedAfterProjection(database, { + messageId: projection.finalAssistantMessageId, + projection, + runId: link.sessionRunId, + sessionId: link.sessionId, + })) + ) { + throw new Error( + `Completed run final message ${projection.finalAssistantMessageId} has no sealed authoritative snapshot.`, + ); + } + } + if (terminalRunTransition !== undefined) { + const terminalKind = terminalRuntimeEventKind(terminalRunTransition.status); + if (projection.runtimeEvents.filter(({ event }) => event.kind === terminalKind).length !== 1) { + throw new Error("Terminal run projection requires exactly one terminal runtime event."); + } } - - const durationMs = Date.parse(outcome.run.completedAt) - Date.parse(outcome.run.startedAt); - return Number.isFinite(durationMs) ? Math.max(0, durationMs) : null; } -async function autoTitleRuntimeSession( +export async function persistProjectedRuntimeDriverEventPrerequisites( database: D1Database, - link: RuntimeSessionLink, - title: string, + input: { + driverConnectionId: string; + driverGeneration: number; + driverInstanceId: DriverInstanceId; + projection: AppRuntimeDriverEventsResult; + }, ): Promise { - if (link.creatorId === null || link.sessionId === null) { + const { link, runtimeEvents } = input.projection; + if (link.sessionId === null) { return; } - - await getAppDatabase(database) - .update(sessionsTable) - .set({ - title, - updatedAt: currentTimestampMs(), - }) - .where( - and( - eq(sessionsTable.id, link.sessionId), - eq(sessionsTable.creatorAccountId, link.creatorId), - isNull(sessionsTable.title), - eq(sessionsTable.renamed, false), - ), - ) - .run(); -} - -export interface PersistProjectedRuntimeDriverEventsResult { - liveState: SessionLiveState | null; - persistedSourceEventIds: readonly string[]; + const [runTransition] = compactRuntimeDriverRunTransitions(input.projection.transitions); + const terminalRunTransition = isTerminalRunTransition(runTransition) ? runTransition : undefined; + if (terminalRunTransition === undefined) { + return; + } + const terminalKind = terminalRuntimeEventKind(terminalRunTransition.status); + const prerequisites = runtimeEvents.filter( + ({ event }) => + event.kind !== terminalKind && + event.kind !== "file.change.updated" && + event.kind !== "file.changed", + ); + if (prerequisites.length === 0) { + return; + } + await persistSessionRuntimeEvents(database, { + driverFence: { + connectionId: input.driverConnectionId, + driverInstanceId: input.driverInstanceId, + generation: input.driverGeneration, + sessionRunId: link.sessionRunId, + }, + records: prerequisites, + sessionId: link.sessionId, + }); } export async function persistProjectedRuntimeDriverEvents( bindings: ApiBindings, input: { + driverConnectionId: string; + driverGeneration: number; driverInstanceId: DriverInstanceId; projection: AppRuntimeDriverEventsResult; }, @@ -150,8 +210,7 @@ export async function persistProjectedRuntimeDriverEvents( }; const transitions = compactRuntimeDriverRunTransitions(projection.transitions); const [runTransition] = transitions; - const deferCompletedRunTransition = runTransition?.status === "completed"; - let runTransitionOutcome: SessionRunTransitionOutcome | null = null; + const terminalRunTransition = isTerminalRunTransition(runTransition) ? runTransition : undefined; if (link.sessionId === null) { return { @@ -159,119 +218,88 @@ export async function persistProjectedRuntimeDriverEvents( persistedSourceEventIds: [], }; } + const driverFence = { + connectionId: input.driverConnectionId, + driverInstanceId: input.driverInstanceId, + generation: input.driverGeneration, + sessionRunId: link.sessionRunId, + }; - if (runTransition !== undefined && link.sessionRunId !== null && !deferCompletedRunTransition) { - if (runTransition.status === "running") { - runTransitionOutcome = await setDriverProjectedSessionRunStatus(database, { - runId: link.sessionRunId, - source: "driver", - status: "running", - }); - } else if (runTransition.status === "cancelled") { - runTransitionOutcome = await setDriverProjectedSessionRunStatus(database, { - runId: link.sessionRunId, - source: "driver", - status: "cancelled", - }); - } else { - runTransitionOutcome = await setDriverProjectedSessionRunStatus(database, { - error: runTransition.error ?? null, - runId: link.sessionRunId, - source: "driver", - status: "failed", - }); - } + const completedTransition = terminalRunTransition?.status === "completed"; + const terminalEventKind = + terminalRunTransition === undefined + ? null + : terminalRuntimeEventKind(terminalRunTransition.status); + const preTerminalRuntimeEvents = + terminalEventKind === null + ? [] + : projection.runtimeEvents.filter((record) => record.event.kind !== terminalEventKind); + const terminalRuntimeEvents = + terminalEventKind === null + ? projection.runtimeEvents + : projection.runtimeEvents.filter((record) => record.event.kind === terminalEventKind); + const persistedSourceEventIds: string[] = []; + + if (preTerminalRuntimeEvents.length > 0) { + const persisted = await persistSessionRuntimeEvents(database, { + driverFence, + records: preTerminalRuntimeEvents, + sessionId: link.sessionId, + }); + persistedSourceEventIds.push(...persisted.persistedSourceEventIds); } - const shouldReleaseDriverRun = hasTerminalRuntimeDriverRunTransition(transitions); - let staleTerminalRunTransition = isStaleTerminalRunTransition(runTransitionOutcome); + let finalAssistantMessage: { id: string } | null = null; + let preparedAssistantMessage: PreparedAssistantMessageProjection | null = null; - if ( - staleTerminalRunTransition && - !isMatchingStaleTerminalRunTransition({ - outcome: runTransitionOutcome, - transition: runTransition, - }) - ) { - if (shouldReleaseDriverRun && link.sessionRunId !== null) { - const { releaseTerminalDriverInstanceSessionRun } = await loadTerminalRunRelease(); - await releaseTerminalDriverInstanceSessionRun(bindings, { - driverInstanceId: input.driverInstanceId, - sessionRunId: link.sessionRunId, - }); + if (completedTransition && projection.finalAssistantMessageId !== null) { + if (link.sessionRunId === null) { + throw new Error("Completed run final message is missing its run scope."); } - return { - liveState: null, - persistedSourceEventIds: [], - }; - } - - if (projection.sessionTitle !== null && projection.sessionTitle.length > 0) { - await autoTitleRuntimeSession(database, link, projection.sessionTitle); - } - - const traceId = link.traceId ?? link.sessionRunId ?? link.sessionId; + finalAssistantMessage = await resolveFinalAssistantMessage(database, { + messageId: projection.finalAssistantMessageId, + runId: link.sessionRunId, + sessionId: link.sessionId, + }); - if (projection.usage && link.sessionRunId !== null) { - await upsertSessionModelCallUsage(database, { - driverInstanceId: input.driverInstanceId, + preparedAssistantMessage = prepareAssistantMessageProjection({ + createdByAccountId: link.callerId ?? link.creatorId ?? input.driverInstanceId, + messageId: finalAssistantMessage.id, sessionId: link.sessionId, sessionRunId: link.sessionRunId, - status: getModelCallStatus(transitions), - traceId, - usage: projection.usage, }); } - const completedTransition = transitions.find((transition) => transition.status === "completed"); - const preCompletionRuntimeEvents = - completedTransition === undefined - ? [] - : projection.runtimeEvents.filter((record) => record.event.kind !== "run.completed"); - const terminalRuntimeEvents = - completedTransition === undefined - ? projection.runtimeEvents - : projection.runtimeEvents.filter((record) => record.event.kind === "run.completed"); - const finalAssistantRuntimeEvents: typeof terminalRuntimeEvents = []; - const persistedSourceEventIds: string[] = []; + let terminalRunCommit: Awaited> | null = null; - if (preCompletionRuntimeEvents.length > 0) { - const persisted = await persistSessionRuntimeEvents(database, { - records: preCompletionRuntimeEvents, - sessionId: link.sessionId, - }); - persistedSourceEventIds.push(...persisted.persistedSourceEventIds); - } + if (terminalRunTransition !== undefined) { + if (link.sessionRunId === null) { + throw new Error("Terminal run projection is missing its run scope."); + } - // Completion is a retry boundary. Arbitrate the terminal Run status before - // writing canonical output, so a concurrent failure/cancellation cannot - // leave a final assistant row on a non-completed Run. The terminal receipt - // remains last: a crash after the CAS is repaired by replay against the exact - // completed Run link. - if (deferCompletedRunTransition && link.sessionRunId !== null) { - runTransitionOutcome = await setDriverProjectedSessionRunStatus(database, { + const [terminalEvent] = terminalRuntimeEvents; + + if (terminalRuntimeEvents.length !== 1 || terminalEvent === undefined) { + throw new Error("Terminal run projection requires exactly one terminal runtime event."); + } + + terminalRunCommit = await commitTerminalRunProjection(database, { + assistantMessage: preparedAssistantMessage, + error: terminalRunTransition.error ?? null, + expectedDriverObservation: { + connectionId: input.driverConnectionId, + driverInstanceId: input.driverInstanceId, + generation: input.driverGeneration, + }, runId: link.sessionRunId, + sessionId: link.sessionId, source: "driver", - status: "completed", + targetStatus: terminalRunTransition.status, + terminalEvent, }); - staleTerminalRunTransition = isStaleTerminalRunTransition(runTransitionOutcome); - - if ( - staleTerminalRunTransition && - !isMatchingStaleTerminalRunTransition({ - outcome: runTransitionOutcome, - transition: runTransition, - }) - ) { - if (shouldReleaseDriverRun) { - const { releaseTerminalDriverInstanceSessionRun } = await loadTerminalRunRelease(); - await releaseTerminalDriverInstanceSessionRun(bindings, { - driverInstanceId: input.driverInstanceId, - sessionRunId: link.sessionRunId, - }); - } - + persistedSourceEventIds.push(...terminalRunCommit.persistedSourceEventIds); + if (terminalRunCommit.kind === "stale") { return { liveState: null, persistedSourceEventIds: [], @@ -279,73 +307,15 @@ export async function persistProjectedRuntimeDriverEvents( } } - if ( - completedTransition !== undefined && - projection.finalAssistantMessage !== null && - nextLiveState !== null && - link.sessionRunId !== null && - nextLiveState.run.id === link.sessionRunId - ) { - await persistAssistantMessageProjection(database, { - createdByAccountId: link.callerId ?? link.creatorId ?? input.driverInstanceId, - driverInstanceId: input.driverInstanceId, - messageId: projection.finalAssistantMessage.id, - messageText: projection.finalAssistantMessage.text, + if (terminalRunTransition === undefined) { + const persistedTerminalEvents = await persistSessionRuntimeEvents(database, { + driverFence, + records: terminalRuntimeEvents, sessionId: link.sessionId, - sessionRunId: link.sessionRunId, - state: nextLiveState, }); - - const terminalRuntimeEvent = terminalRuntimeEvents[0]; - const finalSnapshotAlreadyPersisted = - (await getAppDatabase(database) - .select({ id: sessionEventsTable.id }) - .from(sessionEventsTable) - .where( - and( - eq(sessionEventsTable.sessionId, link.sessionId), - eq(sessionEventsTable.runId, link.sessionRunId), - eq(sessionEventsTable.eventType, "message.added"), - eq(sessionEventsTable.processType, "agent.message.delta"), - eq(sessionEventsTable.contentText, projection.finalAssistantMessage.text), - ), - ) - .limit(1) - .get()) !== undefined; - - if (terminalRuntimeEvent !== undefined && !finalSnapshotAlreadyPersisted) { - const sourceEventId = `session-run:${link.sessionRunId}:final-assistant`; - finalAssistantRuntimeEvents.push({ - event: createSessionRuntimeEvent({ - actor: terminalRuntimeEvent.event.actor, - kind: "message.added", - ...(terminalRuntimeEvent.occurredAt === null - ? {} - : { occurredAtMs: terminalRuntimeEvent.occurredAt }), - origin: terminalRuntimeEvent.event.origin, - payload: { - content: projection.finalAssistantMessage.text, - messageId: projection.finalAssistantMessage.id, - role: "agent", - }, - runId: link.sessionRunId, - sessionId: link.sessionId, - sourceEventId, - traceId: terminalRuntimeEvent.event.traceId ?? link.traceId, - visibility: terminalRuntimeEvent.event.visibility, - }), - occurredAt: terminalRuntimeEvent.occurredAt, - sourceEventId, - }); - } + persistedSourceEventIds.push(...persistedTerminalEvents.persistedSourceEventIds); } - const persistedTerminalEvents = await persistSessionRuntimeEvents(database, { - records: [...finalAssistantRuntimeEvents, ...terminalRuntimeEvents], - sessionId: link.sessionId, - }); - persistedSourceEventIds.push(...persistedTerminalEvents.persistedSourceEventIds); - let committedLiveState: SessionLiveState | null = null; if (projection.liveStateChanged && nextLiveState !== null) { @@ -353,28 +323,10 @@ export async function persistProjectedRuntimeDriverEvents( } if ( - runTransition === undefined && - !staleTerminalRunTransition && - link.sessionRunId !== null && - projection.liveStateChanged && - nextLiveState !== null && - nextLiveState.run.id === link.sessionRunId && - nextLiveState.run.status === "waiting_input" - ) { - await setDriverProjectedSessionRunStatus(database, { - runId: link.sessionRunId, - source: "driver", - status: "waiting_input", - }); - } - - if ( - completedTransition !== undefined && - runTransitionOutcome?.kind === "applied" && + completedTransition && + terminalRunCommit?.kind === "applied" && link.executionOwnerId !== null ) { - const runDurationMs = getRunDurationMs(runTransitionOutcome); - await captureServerProductEvent(bindings, { distinctId: link.executionOwnerId, event: SERVER_PRODUCT_ANALYTICS_EVENTS.taskSucceeded, @@ -382,7 +334,7 @@ export async function persistProjectedRuntimeDriverEvents( agent_id: link.agentId, app_id: link.appId, run_id: link.sessionRunId, - run_duration_ms: runDurationMs, + run_duration_ms: terminalRunCommit.runDurationMs, sandbox_id: link.sandboxId, sandbox_kind: link.sandboxKind, sandbox_subject_kind: link.sandboxSubjectKind, @@ -392,14 +344,6 @@ export async function persistProjectedRuntimeDriverEvents( }); } - if (shouldReleaseDriverRun && link.sessionRunId !== null) { - const { releaseTerminalDriverInstanceSessionRun } = await loadTerminalRunRelease(); - await releaseTerminalDriverInstanceSessionRun(bindings, { - driverInstanceId: input.driverInstanceId, - sessionRunId: link.sessionRunId, - }); - } - return { liveState: committedLiveState, persistedSourceEventIds, diff --git a/apps/api/src/modules/runtime/infrastructure/driver-instance/event-projection.ts b/apps/api/src/modules/runtime/infrastructure/driver-instance/event-projection.ts index bd76d6a1..997433e0 100644 --- a/apps/api/src/modules/runtime/infrastructure/driver-instance/event-projection.ts +++ b/apps/api/src/modules/runtime/infrastructure/driver-instance/event-projection.ts @@ -6,7 +6,6 @@ import type { SessionLiveState, SessionPermissionRequestView, } from "../../../sessions/application/session-live-state.service"; -import { normalizeSessionTitle } from "../../../sessions/domain/session-title"; import type { RuntimeDriverRunTransition, RuntimeSessionLink } from "./event-types"; export function compactRuntimeDriverRunTransitions( @@ -92,23 +91,6 @@ export function createBaseLiveState( }); } -export function normalizeRuntimeSessionInfoTitle(title: string | null | undefined): string | null { - if (typeof title !== "string" || title.trim().length === 0) { - return null; - } - - return normalizeSessionTitle(title); -} - -export function upsertPermissionRequest( - current: SessionPermissionRequestView[], - next: SessionPermissionRequestView, -): SessionPermissionRequestView[] { - const requests = current.filter((request) => request.requestId !== next.requestId); - requests.push(next); - return requests; -} - export function removePermissionRequest( current: SessionPermissionRequestView[], requestId: string, diff --git a/apps/api/src/modules/runtime/infrastructure/driver-instance/event-types.ts b/apps/api/src/modules/runtime/infrastructure/driver-instance/event-types.ts index af005f42..41c787c4 100644 --- a/apps/api/src/modules/runtime/infrastructure/driver-instance/event-types.ts +++ b/apps/api/src/modules/runtime/infrastructure/driver-instance/event-types.ts @@ -1,4 +1,3 @@ -import type { SessionUsageSummary } from "@mosoo/ag-ui-session"; import type { AgentKind } from "@mosoo/contracts/agent"; import type { SandboxSubjectKind } from "@mosoo/contracts/sandbox"; import type { SessionType } from "@mosoo/contracts/session"; @@ -7,6 +6,7 @@ import type { AccountId, AgentId, AppId, + DriverCommandId, PlatformId, SandboxId, SessionId, @@ -21,6 +21,17 @@ import type { export type { SessionLiveState }; +export interface CanonicalDriverEventEnvelope { + readonly event: RuntimeEventEnvelope; + readonly eventId: string; + readonly occurredAt?: string | null | undefined; +} + +export interface HostDriverEventBatchInput { + readonly driverInstanceId: string; + readonly events: readonly CanonicalDriverEventEnvelope[]; +} + export interface RuntimeSessionLink { agentId: AgentId | null; appId: AppId | null; @@ -29,6 +40,7 @@ export interface RuntimeSessionLink { executionOwnerId: AccountId | null; sandboxId: SandboxId | null; sandboxKind: AgentKind | null; + runtimeId: string | null; sessionId: SessionId | null; sessionRunId: SessionRunId | null; sessionRunStatus: SessionRunStatus | null; @@ -42,8 +54,12 @@ export function runtimeSessionLinkNeedsRefresh(link: RuntimeSessionLink | null): } export interface ProjectedRuntimeEventRecord { + artifactAttemptId?: string | null; + artifactManifestJson?: string | null; + artifactManifestSha256?: string | null; event: RuntimeEventEnvelope; occurredAt: number | null; + provenMcpCommandId?: DriverCommandId | null; sourceEventId: string | null; } @@ -64,13 +80,11 @@ export interface RuntimeDriverRunTransition { } export interface AppRuntimeDriverEventsResult { - finalAssistantMessage: { id: string; text: string } | null; + finalAssistantMessageId: string | null; link: RuntimeSessionLink; liveStateChanged: boolean; nextLiveState: SessionLiveState | null; - sessionTitle: string | null; transitions: RuntimeDriverRunTransition[]; - usage: SessionUsageSummary | null; runtimeEvents: ProjectedRuntimeEventRecord[]; sessionDeliveryEvents: ProjectedSessionDeliveryEvent[]; } diff --git a/apps/api/src/modules/runtime/infrastructure/driver-instance/events.ts b/apps/api/src/modules/runtime/infrastructure/driver-instance/events.ts index 9e5a504d..621692ff 100644 --- a/apps/api/src/modules/runtime/infrastructure/driver-instance/events.ts +++ b/apps/api/src/modules/runtime/infrastructure/driver-instance/events.ts @@ -1,32 +1,13 @@ -import { - EventType, - MOSOO_CUSTOM_EVENT, - createServerCustomEvent, - parseNullableSessionUsageSummary, -} from "@mosoo/ag-ui-session"; -import type { DriverEventEnvelope } from "@mosoo/agent-driver/events"; -import { parsePlatformId } from "@mosoo/id"; -import type { DriverInstanceId } from "@mosoo/id"; -import type { AccountId, SessionId } from "@mosoo/id"; +import type { DriverCommandId, DriverInstanceId } from "@mosoo/id"; import { parseRuntimeEventEnvelope, - readRuntimeEventFileChanges, readRuntimeEventPayload, - readRuntimeEventPermissionRequest, readRuntimeEventString, - readRuntimeRunPayload, } from "@mosoo/runtime-events"; import type { RuntimeEventEnvelope } from "@mosoo/runtime-events"; -import { createErrorLogContext, logInfo, logWarn } from "../../../../platform/cloudflare/logger"; -import { withDisposedRpcResource } from "../../../../platform/cloudflare/rpc-disposal"; import type { ApiBindings } from "../../../../platform/cloudflare/worker-types"; import { isTruthy } from "../../../../shared/truthiness"; -import { - createRuntimeOutputContentSha256, - createRuntimeOutputParentPath, - fileStore, -} from "../../../files/application/file-store"; import { applyAgUiEventToSessionLiveState, loadSessionViewerState, @@ -36,364 +17,43 @@ import type { SessionDeliveryEvent, SessionLiveState, } from "../../../sessions/application/session-live-state.service"; -import { getRuntimeKindPolicy } from "../../domain/runtime-kind-policy"; -import { createSessionRunTerminalFailureSourceId } from "../../domain/session-run-terminal-event-id"; -import { upsertNativeResumeRef } from "../native-resume-ref.repository"; -import { getRuntimeSubjectKeepAliveHandle } from "../runtime-subject-lifecycle/runtime-subject-lifecycle.service"; -import { getRuntimeConversationSession } from "../runtime-subject-lifecycle/runtime-subject-store"; -import { readSandboxFileBytes } from "../sandbox-file-bytes"; -import type { ExecutionSessionHandle } from "../sandbox-handles"; -import { - assertRuntimeEventMatchesDriverEnvelope, - assertRuntimeEventMatchesDriverLink, -} from "./event-link-assertion"; -import { - createBaseLiveState, - normalizeRuntimeSessionInfoTitle, - readPermissionRequestViews, - readRuntimeDriverRunTransition, - removePermissionRequest, - upsertPermissionRequest, -} from "./event-projection"; +import { createSessionRunTerminalSourceId } from "../../domain/session-run-terminal-event-id"; +import { assertRuntimeEventMatchesDriverLink } from "./event-link-assertion"; +import { createBaseLiveState, readRuntimeDriverRunTransition } from "./event-projection"; import type { - ProjectedRuntimeEventRecord, AppRuntimeDriverEventsResult, + CanonicalDriverEventEnvelope, + ProjectedRuntimeEventRecord, RuntimeDriverRunTransition, RuntimeSessionLink, } from "./event-types"; -import { readNativeResumeRef } from "./native-resume-ref-event"; -import { - RUNTIME_SESSION_OUTPUT_DIR_NAME, - RUNTIME_SESSION_OUTPUT_SCAN_MAX_FILES, - getRuntimeSessionOutputDirectory, - guessRuntimeSessionOutputContentType, - readRuntimeSessionOutputListing, - toRuntimeSessionOutputArtifactPath, - toRuntimeSessionOutputFile, -} from "./runtime-session-outputs"; import { getRuntimeSessionLink } from "./session-link.repository"; export type { AppRuntimeDriverEventsResult, RuntimeDriverRunTransition, RuntimeSessionLink, } from "./event-types"; -export { persistProjectedRuntimeDriverEvents } from "./event-persistence"; +export { + persistProjectedRuntimeDriverEventPrerequisites, + persistProjectedRuntimeDriverEvents, + preflightProjectedRuntimeDriverEvents, +} from "./event-persistence"; export { getRuntimeSessionLink } from "./session-link.repository"; export { recordDriverInstanceCompletion, recordDriverInstanceFailure, } from "./terminal-driver-events"; -function quoteShellArg(value: string): string { - return `'${value.replaceAll("'", `'"'"'`)}'`; -} - -function resolveRuntimeOutputCreator(link: RuntimeSessionLink): AccountId | null { - const actorId = link.executionOwnerId ?? link.callerId ?? link.creatorId; - - if (!isTruthy(actorId)) { - return null; - } - - return parsePlatformId(actorId, "runtime output creator account ID"); -} - -function readRuntimeFileChangeContentType( - metadata: Record | undefined, -): string | null { - const contentType = metadata?.["contentType"] ?? metadata?.["mimeType"]; - return typeof contentType === "string" && contentType.trim().length > 0 ? contentType : null; -} - -function createRuntimeSessionOutputListCommand(outputDir: string): string { - const quotedOutputDir = quoteShellArg(outputDir); - const command = [ - `if [ ! -d ${quotedOutputDir} ]; then exit 0; fi`, - `cd ${quotedOutputDir}`, - `find . -type f -print | sed 's#^\\./##' | sort | head -n ${RUNTIME_SESSION_OUTPUT_SCAN_MAX_FILES}`, - ].join(" && "); - - return `sh -lc ${quoteShellArg(command)}`; -} - -async function listRuntimeSessionOutputFiles( - handle: ExecutionSessionHandle, - outputDir: string, -): Promise { - const result = await handle.exec(createRuntimeSessionOutputListCommand(outputDir)); - - if (!result.success || result.exitCode !== 0) { - throw new Error( - result.stderr.trim() || - result.stdout.trim() || - `Failed to list runtime session outputs in ${outputDir}.`, - ); - } - - return readRuntimeSessionOutputListing(result.stdout); -} - -async function recordRuntimeSessionOutputFile(input: { - bindings: ApiBindings; - body: Uint8Array; - contentType: string | null; - createdBy: AccountId; - existingArtifacts: Set; - path: string; - recordedArtifacts: Set; - sessionId: SessionId; -}): Promise { - const contentSha256 = await createRuntimeOutputContentSha256(input.body); - const artifactKey = createRuntimeOutputParentPath(input.path, contentSha256); - - if (input.recordedArtifacts.has(artifactKey) || input.existingArtifacts.has(artifactKey)) { - return; - } - - await fileStore.recordRuntimeOutput({ - bindings: input.bindings, - body: input.body, - contentSha256, - contentType: input.contentType, - createdBy: input.createdBy, - path: input.path, - sessionId: input.sessionId, - }); - input.recordedArtifacts.add(artifactKey); - input.existingArtifacts.add(artifactKey); -} - -function readTerminalPendingToolResult(event: RuntimeEventEnvelope): string | null { - if (event.kind === "run.failed") { - const run = readRuntimeRunPayload(event).run; - const message = run?.error?.message ?? "Run failed before the tool returned a result."; - return `Tool failed before returning a result: ${message}`; - } - - if (event.kind === "run.cancelled") { - return "Tool was cancelled before returning a result."; - } - - return null; -} - -function createPendingToolResultEvents( - state: SessionLiveState, - event: RuntimeEventEnvelope, -): SessionDeliveryEvent[] { - const content = readTerminalPendingToolResult(event); - - if (content === null) { - return []; - } - - return state.messages.flatMap((message) => { - const completedToolCallIds = new Set( - message.segments.flatMap((segment) => - segment.kind === "tool_result" ? [segment.toolCallId] : [], - ), - ); - - return message.segments.flatMap((segment) => { - if (segment.kind !== "tool_use" || completedToolCallIds.has(segment.toolCallId)) { - return []; - } - - return [ - { - content, - messageId: message.id, - toolCallId: segment.toolCallId, - type: EventType.TOOL_CALL_RESULT, - }, - { - toolCallId: segment.toolCallId, - type: EventType.TOOL_CALL_END, - }, - ]; - }); - }); -} - -async function recordRuntimeFileChanges(input: { - bindings: ApiBindings; - event: RuntimeEventEnvelope; - link: RuntimeSessionLink; -}): Promise { - const sessionId = input.link.sessionId; - const sandboxId = input.link.sandboxId; - const createdBy = resolveRuntimeOutputCreator(input.link); - const changes = readRuntimeEventFileChanges(input.event).filter( - (change) => change.change === "upsert", - ); - - if (changes.length === 0) { - return; - } - - if (sessionId === null || sandboxId === null || createdBy === null) { - logWarn("runtime.file_artifact.record_skipped", { - driverInstanceId: input.event.driverInstanceId ?? null, - hasCreatedBy: createdBy !== null, - sandboxId, - sessionId, - }); - return; - } - - const conversation = await getRuntimeConversationSession(input.bindings.DB, sessionId); - - if (conversation === null) { - logWarn("runtime.file_artifact.record_skipped.missing_session", { - sandboxId, - sessionId, - }); - return; - } - - const outputChanges = changes.flatMap((change) => { - const outputFile = toRuntimeSessionOutputFile({ - contentType: readRuntimeFileChangeContentType(change.metadata), - cwd: conversation.cwd, - path: change.path, - }); - - return outputFile === null ? [] : [outputFile]; - }); - - if (outputChanges.length === 0) { - return; - } - - const parsedSessionId = parsePlatformId(sessionId, "runtime output session ID"); - const existingArtifacts = new Set( - await fileStore.listReadySessionArtifactKeys(input.bindings.DB, parsedSessionId), - ); - const recordedArtifacts = new Set(); - - await withDisposedRpcResource( - await getRuntimeSubjectKeepAliveHandle(input.bindings, sandboxId), - async (sandbox) => { - const sandboxSession = await sandbox.getSession(conversation.sandboxSessionId); - - for (const outputFile of outputChanges) { - try { - await recordRuntimeSessionOutputFile({ - bindings: input.bindings, - body: await readSandboxFileBytes(sandboxSession, outputFile.readPath), - contentType: outputFile.contentType, - createdBy, - existingArtifacts, - path: outputFile.artifactPath, - recordedArtifacts, - sessionId: parsedSessionId, - }); - } catch (error) { - logWarn("runtime.file_artifact.record_failed", { - ...createErrorLogContext(error), - path: outputFile.artifactPath, - sandboxId, - sessionId, - }); - } - } - }, - ); -} - -async function recordRuntimeSessionOutputDirectory(input: { - bindings: ApiBindings; - event: RuntimeEventEnvelope; - link: RuntimeSessionLink; -}): Promise { - const sessionId = input.link.sessionId; - const sandboxId = input.link.sandboxId; - const createdBy = resolveRuntimeOutputCreator(input.link); - - if (sessionId === null || sandboxId === null || createdBy === null) { - return; - } - - const parsedSessionId = parsePlatformId(sessionId, "runtime output session ID"); - let conversation; - - try { - conversation = await getRuntimeConversationSession(input.bindings.DB, parsedSessionId); - } catch (error) { - logWarn("runtime.file_artifact.output_scan_session_lookup_failed", { - ...createErrorLogContext(error), - driverInstanceId: input.event.driverInstanceId ?? null, - sandboxId, - sessionId, - }); - return; - } - - if (conversation === null) { - return; - } - - try { - await withDisposedRpcResource( - await getRuntimeSubjectKeepAliveHandle(input.bindings, sandboxId), - async (sandbox) => { - const sandboxSession = await sandbox.getSession(conversation.sandboxSessionId); - const outputDir = getRuntimeSessionOutputDirectory(conversation.cwd); - const outputPaths = await listRuntimeSessionOutputFiles(sandboxSession, outputDir); - - if (outputPaths.length === 0) { - return; - } - - const existingArtifacts = new Set( - await fileStore.listReadySessionArtifactKeys(input.bindings.DB, parsedSessionId), - ); - const recordedArtifacts = new Set(); - - for (const outputPath of outputPaths) { - const artifactPath = toRuntimeSessionOutputArtifactPath(outputPath); - - try { - await recordRuntimeSessionOutputFile({ - bindings: input.bindings, - body: await readSandboxFileBytes(sandboxSession, `${outputDir}/${outputPath}`), - contentType: guessRuntimeSessionOutputContentType(outputPath), - createdBy, - existingArtifacts, - path: artifactPath, - recordedArtifacts, - sessionId: parsedSessionId, - }); - } catch (error) { - logWarn("runtime.file_artifact.output_record_failed", { - ...createErrorLogContext(error), - path: artifactPath, - sandboxId, - sessionId, - }); - } - } - }, - ); - } catch (error) { - logWarn("runtime.file_artifact.output_scan_failed", { - ...createErrorLogContext(error), - driverInstanceId: input.event.driverInstanceId ?? null, - outputDir: `${RUNTIME_SESSION_OUTPUT_DIR_NAME}/`, - sandboxId, - sessionId, - }); - } -} - export async function appRuntimeDriverEvents( bindings: ApiBindings, input: { assertCurrentConnection?: () => void; currentLiveState?: SessionLiveState | null; - events: readonly DriverEventEnvelope[]; + projectLiveState?: boolean; + events: readonly CanonicalDriverEventEnvelope[]; driverInstanceId: DriverInstanceId; link?: RuntimeSessionLink | null; + provenMcpCommandIds?: ReadonlyMap; }, ): Promise { const database = bindings.DB; @@ -410,32 +70,41 @@ export async function appRuntimeDriverEvents( throw new Error("Runtime driver run event is missing a session run id."); } - const currentLiveState = - input.currentLiveState ?? - (await loadStoredRuntimeLiveState(database, { - driverInstanceId: input.driverInstanceId, - link, - })); + const projectLiveState = input.projectLiveState !== false; + const currentLiveState = !projectLiveState + ? createBaseLiveState({ + callerId: link.callerId, + creatorId: link.creatorId, + driverInstanceId: input.driverInstanceId, + sessionId: link.sessionId, + }) + : (input.currentLiveState ?? + (await loadStoredRuntimeLiveState(database, { + driverInstanceId: input.driverInstanceId, + link, + }))); let nextLiveState = currentLiveState; let liveStateChanged = false; - let finalAssistantMessage: AppRuntimeDriverEventsResult["finalAssistantMessage"] = null; - let sessionTitle: string | null = null; - let usage: AppRuntimeDriverEventsResult["usage"] = null; + let finalAssistantMessageId: AppRuntimeDriverEventsResult["finalAssistantMessageId"] = null; const runtimeEvents: ProjectedRuntimeEventRecord[] = []; const sessionDeliveryEvents: AppRuntimeDriverEventsResult["sessionDeliveryEvents"] = []; const transitions: RuntimeDriverRunTransition[] = []; - function appendCanonicalEvent(source: DriverEventEnvelope, event: RuntimeEventEnvelope): void { + function appendCanonicalEvent( + source: CanonicalDriverEventEnvelope, + event: RuntimeEventEnvelope, + ): void { runtimeEvents.push({ event, occurredAt: toDriverEventOccurredAtMs(source.occurredAt), + provenMcpCommandId: input.provenMcpCommandIds?.get(source.eventId) ?? null, sourceEventId: resolveDriverEventPersistenceSourceId(source, event), }); } function appendSessionDeliveryEvent( - source: DriverEventEnvelope, + source: CanonicalDriverEventEnvelope, event: RuntimeEventEnvelope, deliveryEvent: SessionDeliveryEvent, ): void { @@ -453,145 +122,32 @@ export async function appRuntimeDriverEvents( driverInstanceId: input.driverInstanceId, link, }); - assertRuntimeEventMatchesDriverEnvelope(event, { - eventId: envelope.eventId, - }); appendCanonicalEvent(envelope, event); if (event.kind === "runtime.resume.updated") { - const nativeResumeRef = readNativeResumeRef(event); - - if (nativeResumeRef === null) { - continue; - } - - const policy = link.sandboxKind === null ? null : getRuntimeKindPolicy(link.sandboxKind); - - if (policy?.nativeResume.persistence !== "platform") { - logInfo("runtime.native_resume_ref.ignored", { - driverInstanceId: input.driverInstanceId, - kind: nativeResumeRef.kind, - runtimeId: nativeResumeRef.runtimeId, - sandboxKind: link.sandboxKind, - sandboxSubjectKind: link.sandboxSubjectKind, - sessionId: link.sessionId, - sessionRunId: link.sessionRunId, - }); - continue; - } - - if (link.sessionRunId === null) { - logInfo("runtime.native_resume_ref.deferred", { - driverInstanceId: input.driverInstanceId, - kind: nativeResumeRef.kind, - runtimeId: nativeResumeRef.runtimeId, - sessionId: link.sessionId, - }); - continue; - } - - input.assertCurrentConnection?.(); - await upsertNativeResumeRef(database, { - driverInstanceId: input.driverInstanceId, - nativeResumeRef, - sessionId: link.sessionId, - sessionRunId: link.sessionRunId, - }); - logInfo("runtime.native_resume_ref.observed", { - driverInstanceId: input.driverInstanceId, - kind: nativeResumeRef.kind, - runtimeId: nativeResumeRef.runtimeId, - sessionId: link.sessionId, - sessionRunId: link.sessionRunId, - }); continue; } if (event.kind === "file.change.updated" || event.kind === "file.changed") { - await recordRuntimeFileChanges({ - bindings, - event, - link, - }); continue; } if (event.kind === "run.completed") { const payload = readRuntimeEventPayload(event); - const finalMessageId = readRuntimeEventString(payload, "finalMessageId"); - const finalMessageText = readRuntimeEventString(payload, "finalMessageText"); - finalAssistantMessage = - finalMessageId === null || finalMessageText === null - ? null - : { id: finalMessageId, text: finalMessageText }; - await recordRuntimeSessionOutputDirectory({ - bindings, - event, - link, - }); + finalAssistantMessageId = readRuntimeEventString(payload, "finalMessageId"); } - if (event.kind === "permission.requested") { - const request = readRuntimeEventPermissionRequest(event); - - if (request) { - const permissionsUpdatedEvent = createServerCustomEvent( - MOSOO_CUSTOM_EVENT.sessionPermissionsUpdated.name, - { - permissionRequests: upsertPermissionRequest(nextLiveState.permissionRequests, request), - }, - ); - - nextLiveState = applyAgUiEventToSessionLiveState(nextLiveState, permissionsUpdatedEvent); - appendSessionDeliveryEvent(envelope, event, permissionsUpdatedEvent); - liveStateChanged = true; - } + const transition = readRuntimeDriverRunTransition(event); - continue; + if (transition !== null) { + transitions.push(transition); } - if (event.kind === "permission.resolved") { - const payload = readRuntimeEventPayload(event); - const requestId = readRuntimeEventString(payload, "requestId"); - const permissionRequests = - readPermissionRequestViews(payload["permissionRequests"]) ?? - (requestId === null - ? null - : removePermissionRequest(nextLiveState.permissionRequests, requestId)); - - if (permissionRequests !== null) { - const permissionsUpdatedEvent = createServerCustomEvent( - MOSOO_CUSTOM_EVENT.sessionPermissionsUpdated.name, - { - permissionRequests, - }, - ); - - nextLiveState = applyAgUiEventToSessionLiveState(nextLiveState, permissionsUpdatedEvent); - appendSessionDeliveryEvent(envelope, event, permissionsUpdatedEvent); - liveStateChanged = true; - } - + if (!projectLiveState) { continue; } - const liveEvents = [ - ...createPendingToolResultEvents(nextLiveState, event), - ...appRuntimeEventToSessionDeliveryEvents(event), - ]; - - const setSessionTitle = (title: string | null): void => { - sessionTitle = title; - }; - const setUsage = (nextUsage: AppRuntimeDriverEventsResult["usage"]): void => { - usage = nextUsage; - }; - - appendRuntimeDriverCanonicalSideEffects(event, { - setSessionTitle, - setUsage, - transitions, - }); + const liveEvents = appRuntimeEventToSessionDeliveryEvents(event); for (const liveEvent of liveEvents) { nextLiveState = applyAgUiEventToSessionLiveState(nextLiveState, liveEvent); @@ -601,21 +157,21 @@ export async function appRuntimeDriverEvents( } return { - finalAssistantMessage, + finalAssistantMessageId, link, liveStateChanged, nextLiveState, runtimeEvents, - sessionTitle, sessionDeliveryEvents, transitions, - usage, }; } -// Driver Contract v2 carries envelope occurredAt as an ISO 8601 string; +// Driver Contract v3 carries envelope occurredAt as an ISO 8601 string; // persistence keeps epoch milliseconds. Unparsable or absent values stay null. -function toDriverEventOccurredAtMs(occurredAt: DriverEventEnvelope["occurredAt"]): number | null { +function toDriverEventOccurredAtMs( + occurredAt: CanonicalDriverEventEnvelope["occurredAt"], +): number | null { if (typeof occurredAt !== "string") { return null; } @@ -625,45 +181,21 @@ function toDriverEventOccurredAtMs(occurredAt: DriverEventEnvelope["occurredAt"] } function resolveDriverEventPersistenceSourceId( - source: DriverEventEnvelope, + source: CanonicalDriverEventEnvelope, event: RuntimeEventEnvelope, ): string | null { - if (event.kind === "run.failed" && event.runId !== undefined) { - return createSessionRunTerminalFailureSourceId(event.runId); + if ( + (event.kind === "run.cancelled" || + event.kind === "run.completed" || + event.kind === "run.failed") && + event.runId !== undefined + ) { + return createSessionRunTerminalSourceId(event.runId, event.kind); } return source.eventId.trim().length > 0 ? source.eventId : null; } -function appendRuntimeDriverCanonicalSideEffects( - event: RuntimeEventEnvelope, - output: { - setSessionTitle: (title: string | null) => void; - setUsage: (usage: AppRuntimeDriverEventsResult["usage"]) => void; - transitions: RuntimeDriverRunTransition[]; - }, -): void { - if (event.kind === "session.info.updated") { - output.setSessionTitle( - normalizeRuntimeSessionInfoTitle( - readRuntimeEventString(readRuntimeEventPayload(event), "title"), - ), - ); - return; - } - - if (event.kind === "usage.updated") { - output.setUsage(parseNullableSessionUsageSummary(event.payload)); - return; - } - - const transition = readRuntimeDriverRunTransition(event); - - if (transition !== null) { - output.transitions.push(transition); - } -} - async function loadStoredRuntimeLiveState( database: D1Database, input: { diff --git a/apps/api/src/modules/runtime/infrastructure/driver-instance/http.ts b/apps/api/src/modules/runtime/infrastructure/driver-instance/http.ts index ef7df8ec..6da81594 100644 --- a/apps/api/src/modules/runtime/infrastructure/driver-instance/http.ts +++ b/apps/api/src/modules/runtime/infrastructure/driver-instance/http.ts @@ -1,9 +1,8 @@ +import { parseRuntimeCommand } from "@mosoo/contracts/runtime-command"; import type { RuntimeCommand } from "@mosoo/contracts/runtime-command"; import { json, readPositiveTimeout, toErrorMessage } from "./driver-instance-support"; import type { - DriverInstanceHeartbeatResult, - DriverInstanceHelloResult, DriverInstanceReadyResult, DriverInstanceSnapshot, DriverInstanceWaitForCloseResult, @@ -11,24 +10,29 @@ import type { export interface DriverInstanceHttpHandler { acceptDriverSocket(request: Request): Promise; - destroy(reason: string): Promise; - fail(message: string): Promise; - sendControlCommand(command: RuntimeCommand): Promise; + destroy(generation: number, reason: string): Promise; + fail(generation: number, message: string): Promise; + sendControlCommand(generation: number, command: RuntimeCommand): Promise; snapshot(): DriverInstanceSnapshot; - waitForClose(timeoutMs: number): Promise; - waitForHeartbeat(afterCount: number, timeoutMs: number): Promise; - waitForHello(timeoutMs: number): Promise; - waitForReady(timeoutMs: number): Promise; + waitForClose(generation: number, timeoutMs: number): Promise; + waitForReady(generation: number, timeoutMs: number): Promise; } interface RuntimeFailRequest { + generation: number; message?: string; } interface RuntimeCloseRequest { + generation: number; reason?: string; } +interface RuntimeSendRequest { + command: RuntimeCommand; + generation: number; +} + function isRecord(value: unknown): value is Record { return typeof value === "object" && value !== null && !Array.isArray(value); } @@ -62,25 +66,46 @@ function readOptionalString( function parseFailRequest(value: unknown): RuntimeFailRequest { const requestName = "RuntimeFailRequest"; const record = readObject(value, requestName); + const generation = record["generation"]; const message = readOptionalString(record, "message", requestName); - return message === undefined ? {} : { message }; + if (!Number.isSafeInteger(generation) || (generation as number) < 0) { + throw new TypeError(`${requestName}.generation must be a non-negative safe integer.`); + } + + return message === undefined + ? { generation: generation as number } + : { generation: generation as number, message }; } function parseCloseRequest(value: unknown): RuntimeCloseRequest { const requestName = "RuntimeCloseRequest"; const record = readObject(value, requestName); + const generation = record["generation"]; const reason = readOptionalString(record, "reason", requestName); - return reason === undefined ? {} : { reason }; + if (!Number.isSafeInteger(generation) || (generation as number) < 0) { + throw new TypeError(`${requestName}.generation must be a non-negative safe integer.`); + } + + return reason === undefined + ? { generation: generation as number } + : { generation: generation as number, reason }; } -function parseRuntimeCommand(value: unknown): RuntimeCommand { - if (!isRecord(value) || typeof value["kind"] !== "string") { - throw new TypeError("Runtime command must be an object with a string kind."); +function parseSendRequest(value: unknown): RuntimeSendRequest { + const requestName = "RuntimeSendRequest"; + const record = readObject(value, requestName); + const generation = record["generation"]; + + if (!Number.isSafeInteger(generation) || (generation as number) < 0) { + throw new TypeError(`${requestName}.generation must be a non-negative safe integer.`); } - return value as RuntimeCommand; + return { + command: parseRuntimeCommand(record["command"]), + generation: generation as number, + }; } async function readOptionalJsonBody(request: Request): Promise { @@ -88,6 +113,17 @@ async function readOptionalJsonBody(request: Request): Promise { return text.trim().length === 0 ? {} : JSON.parse(text); } +function readGeneration(url: URL): number { + const value = url.searchParams.get("generation"); + const generation = value === null || value.trim().length === 0 ? Number.NaN : Number(value); + + if (!Number.isSafeInteger(generation) || generation < 0) { + throw new TypeError("generation must be a non-negative safe integer."); + } + + return generation; +} + export async function handleDriverInstanceRequest( handler: DriverInstanceHttpHandler, request: Request, @@ -98,27 +134,12 @@ export async function handleDriverInstanceRequest( return handler.acceptDriverSocket(request); } - if (request.method === "GET" && url.pathname === "/wait/hello") { - return json(await handler.waitForHello(readPositiveTimeout(url, "hello"))); - } - if (request.method === "GET" && url.pathname === "/wait/ready") { - return json(await handler.waitForReady(readPositiveTimeout(url, "ready"))); - } - - if (request.method === "GET" && url.pathname === "/wait/heartbeat") { - const timeoutMs = readPositiveTimeout(url, "heartbeat"); - const afterCount = Number(url.searchParams.get("afterCount") ?? "0"); - - if (!Number.isInteger(afterCount) || afterCount < 0) { - return json({ error: "afterCount must be a non-negative integer." }, { status: 400 }); - } - - return json(await handler.waitForHeartbeat(afterCount, timeoutMs)); + return json(await handler.waitForReady(readGeneration(url), readPositiveTimeout(url, "ready"))); } if (request.method === "GET" && url.pathname === "/wait/close") { - return json(await handler.waitForClose(readPositiveTimeout(url, "close"))); + return json(await handler.waitForClose(readGeneration(url), readPositiveTimeout(url, "close"))); } if (request.method === "GET" && url.pathname === "/snapshot") { @@ -126,10 +147,10 @@ export async function handleDriverInstanceRequest( } if (request.method === "POST" && url.pathname === "/control/send") { - let command: RuntimeCommand; + let body: RuntimeSendRequest; try { - command = parseRuntimeCommand(await request.json()); + body = parseSendRequest(await request.json()); } catch (error) { return json( { @@ -139,7 +160,7 @@ export async function handleDriverInstanceRequest( ); } - await handler.sendControlCommand(command); + await handler.sendControlCommand(body.generation, body.command); return json({ ok: true }); } @@ -162,7 +183,7 @@ export async function handleDriverInstanceRequest( ? body.message : "Driver instance failed."; - await handler.fail(message); + await handler.fail(body.generation, message); return json({ ok: true }); } @@ -181,6 +202,7 @@ export async function handleDriverInstanceRequest( } await handler.destroy( + body.generation, typeof body.reason === "string" && body.reason.trim().length > 0 ? body.reason : "runtime.driver_instance.destroyed", diff --git a/apps/api/src/modules/runtime/infrastructure/driver-instance/lifecycle.ts b/apps/api/src/modules/runtime/infrastructure/driver-instance/lifecycle.ts index 4ac96984..f3cafa5e 100644 --- a/apps/api/src/modules/runtime/infrastructure/driver-instance/lifecycle.ts +++ b/apps/api/src/modules/runtime/infrastructure/driver-instance/lifecycle.ts @@ -5,7 +5,7 @@ import type { } from "@mosoo/agent-driver/orpc"; import { driverInstancesTable } from "@mosoo/db"; import type { DriverInstanceId } from "@mosoo/id"; -import { and, eq, inArray, sql } from "drizzle-orm"; +import { and, eq, inArray, isNull, sql } from "drizzle-orm"; import type { ApiBindings } from "../../../../platform/cloudflare/worker-types"; import { getAppDatabase } from "../../../../platform/db/drizzle"; @@ -17,6 +17,8 @@ import { import { parseDriverTimestampMs, driverInstanceExpiresAt } from "./status"; import type { DriverInstanceStatus } from "./status"; +export type DriverInstanceProjectionOutcome = "applied" | "replay" | "conflict"; + export async function markDriverInstanceConnected( bindings: ApiBindings, input: { @@ -58,7 +60,7 @@ export async function recordDriverInstanceHello( generation: number; hello: DriverHelloInput; }, -): Promise { +): Promise { const now = currentTimestampMs(); if (input.hello === undefined) { @@ -81,12 +83,40 @@ export async function recordDriverInstanceHello( eq(driverInstancesTable.connectionId, input.connectionId), eq(driverInstancesTable.generation, input.generation), eq(driverInstancesTable.status, "connecting"), + isNull(driverInstancesTable.driverPid), + isNull(driverInstancesTable.driverStartedAt), + isNull(driverInstancesTable.driverVersion), ), ) .returning({ id: driverInstancesTable.id }) .get()) ?? null; - return row !== null; + if (row !== null) { + return "applied"; + } + + const startedAt = parseDriverTimestampMs(input.hello.startedAt, "Driver hello startedAt"); + const existing = + (await getAppDatabase(bindings.DB) + .select({ id: driverInstancesTable.id }) + .from(driverInstancesTable) + .where( + and( + eq(driverInstancesTable.id, input.driverInstanceId), + eq(driverInstancesTable.connectionId, input.connectionId), + eq(driverInstancesTable.generation, input.generation), + inArray(driverInstancesTable.status, ["connecting", "ready"]), + eq(driverInstancesTable.driverPid, input.hello.pid), + eq(driverInstancesTable.driverStartedAt, startedAt), + eq(driverInstancesTable.driverVersion, input.hello.driverVersion), + eq(driverInstancesTable.protocolVersion, input.hello.protocolVersion), + eq(driverInstancesTable.runtime, input.hello.runtime), + ), + ) + .limit(1) + .get()) ?? null; + + return existing === null ? "conflict" : "replay"; } export async function markDriverInstanceReady( @@ -96,7 +126,7 @@ export async function markDriverInstanceReady( driverInstanceId: DriverInstanceId; generation: number; }, -): Promise { +): Promise { const now = currentTimestampMs(); const row = @@ -122,7 +152,27 @@ export async function markDriverInstanceReady( .returning({ id: driverInstancesTable.id }) .get()) ?? null; - return row !== null; + if (row !== null) { + return "applied"; + } + + const existing = + (await getAppDatabase(bindings.DB) + .select({ id: driverInstancesTable.id }) + .from(driverInstancesTable) + .where( + and( + eq(driverInstancesTable.id, input.driverInstanceId), + eq(driverInstancesTable.connectionId, input.connectionId), + eq(driverInstancesTable.generation, input.generation), + eq(driverInstancesTable.status, "ready"), + eq(driverInstancesTable.driverPid, input.pid), + ), + ) + .limit(1) + .get()) ?? null; + + return existing === null ? "conflict" : "replay"; } export async function recordDriverInstanceHeartbeat( @@ -165,7 +215,7 @@ export async function finalizeDriverInstance( input: { closeCode?: number | null; closeReason?: string | null; - connectionId: string; + connectionId: string | null; connectedAt?: number | null; driverPid?: number | null; driverStartedAt?: string | null; @@ -175,8 +225,10 @@ export async function finalizeDriverInstance( lastHeartbeatAt?: string | null; status: Extract; }, -): Promise { +): Promise | null> { const completedAt = currentTimestampMs(); + const connectionMatches = + input.connectionId === null ? [] : [eq(driverInstancesTable.connectionId, input.connectionId)]; const driverStartedAt = typeof input.driverStartedAt === "string" && input.driverStartedAt.length > 0 @@ -209,7 +261,7 @@ export async function finalizeDriverInstance( .where( and( eq(driverInstancesTable.id, driverInstanceId), - eq(driverInstancesTable.connectionId, input.connectionId), + ...connectionMatches, eq(driverInstancesTable.generation, input.generation), inArray(driverInstancesTable.status, LIVE_DRIVER_INSTANCE_STATUSES), ), @@ -217,20 +269,42 @@ export async function finalizeDriverInstance( .returning({ id: driverInstancesTable.id }) .get()) ?? null; - return row !== null; + if (row !== null) { + return input.status; + } + + const existing = + (await getAppDatabase(bindings.DB) + .select({ status: driverInstancesTable.status }) + .from(driverInstancesTable) + .where( + and( + eq(driverInstancesTable.id, driverInstanceId), + ...connectionMatches, + eq(driverInstancesTable.generation, input.generation), + inArray(driverInstancesTable.status, ["stopped", "failed"]), + ), + ) + .limit(1) + .get()) ?? null; + + return existing?.status === "stopped" || existing?.status === "failed" ? existing.status : null; } -export async function getDriverInstanceStatus( +export async function getDriverInstanceLifecycleIdentity( bindings: ApiBindings, driverInstanceId: DriverInstanceId, -): Promise { +): Promise<{ generation: number; status: DriverInstanceStatus } | null> { const row = (await getAppDatabase(bindings.DB) - .select({ status: driverInstancesTable.status }) + .select({ + generation: driverInstancesTable.generation, + status: driverInstancesTable.status, + }) .from(driverInstancesTable) .where(eq(driverInstancesTable.id, driverInstanceId)) .limit(1) .get()) ?? null; - return row?.status ?? null; + return row; } diff --git a/apps/api/src/modules/runtime/infrastructure/driver-instance/live-driver-instance.repository.ts b/apps/api/src/modules/runtime/infrastructure/driver-instance/live-driver-instance.repository.ts index 18973423..af11dd4e 100644 --- a/apps/api/src/modules/runtime/infrastructure/driver-instance/live-driver-instance.repository.ts +++ b/apps/api/src/modules/runtime/infrastructure/driver-instance/live-driver-instance.repository.ts @@ -10,6 +10,7 @@ export async function listLiveDriverInstanceRefsForSandboxSessions( sandboxSessionIds: readonly SessionId[], ): Promise< { + generation: number; id: DriverInstanceId; sandboxSessionId: SessionId; }[] @@ -22,6 +23,7 @@ export async function listLiveDriverInstanceRefsForSandboxSessions( return getAppDatabase(database) .select({ + generation: driverInstancesTable.generation, id: driverInstancesTable.id, sandboxSessionId: driverInstancesTable.sandboxSessionId, }) @@ -34,11 +36,3 @@ export async function listLiveDriverInstanceRefsForSandboxSessions( ) .all(); } - -export async function listLiveDriverInstanceIdsForSandboxSessions( - database: D1Database, - sandboxSessionIds: readonly SessionId[], -): Promise { - const rows = await listLiveDriverInstanceRefsForSandboxSessions(database, sandboxSessionIds); - return rows.map((row) => row.id); -} diff --git a/apps/api/src/modules/runtime/infrastructure/driver-instance/maintenance.ts b/apps/api/src/modules/runtime/infrastructure/driver-instance/maintenance.ts index c9e4d52a..444556e8 100644 --- a/apps/api/src/modules/runtime/infrastructure/driver-instance/maintenance.ts +++ b/apps/api/src/modules/runtime/infrastructure/driver-instance/maintenance.ts @@ -1,5 +1,5 @@ -import { driverInstancesTable, externalToolEffectsTable } from "@mosoo/db"; -import { and, eq, inArray, isNull, lte, notExists, sql } from "drizzle-orm"; +import { driverInstancesTable, externalToolEffectsTable, sessionsTable } from "@mosoo/db"; +import { and, eq, inArray, isNotNull, isNull, lte, notExists, sql } from "drizzle-orm"; import type { ApiBindings } from "../../../../platform/cloudflare/worker-types"; import { getAppDatabase } from "../../../../platform/db/drizzle"; @@ -9,7 +9,9 @@ import { DRIVER_COLD_READY_TIMEOUT_MS, RUNTIME_SOCKET_TIMEOUT_MS, } from "../../domain/runtime-config"; +import { repairClaimedDriverStopsGlobally } from "../driver-session-stop.service"; import { driverInstanceExpiresAt } from "./status"; +import { repairTerminalDriverRuntimeCommandsGlobally } from "./terminal-run-release"; export async function cleanupDriverInstances(bindings: ApiBindings): Promise { const database = getAppDatabase(bindings.DB); @@ -77,11 +79,33 @@ export async function cleanupDriverInstances(bindings: ApiBindings): Promise["result"] + >, ): RuntimeCommandResult { return parseSchemaValue(RuntimeCommandResult, result); } @@ -51,11 +54,16 @@ export class DriverInstanceRpcCommandController { this.#dependencies = dependencies; } - async enqueueCommand(command: RuntimeCommand): Promise { + async enqueueCommand(driverGeneration: number, command: RuntimeCommand): Promise { const { env, state } = this.#dependencies; + if (state.requireDriverGeneration() !== driverGeneration) { + throw new Error("Driver generation is no longer current."); + } + await createRuntimeCommandRecord(env.DB, { command, + driverGeneration, driverInstanceId: state.requireDriverInstanceId(), expiresAt: currentTimestampPlus(COMMAND_LEASE_MS), }); @@ -76,19 +84,31 @@ export class DriverInstanceRpcCommandController { throw new Error("Driver instance id mismatch."); } const driverInstanceId = state.requireDriverInstanceId(); + const driverGeneration = state.requireDriverGeneration(); context.assertActiveConnection(); const commandId = parsePlatformId(input.commandId, "driver command id"); - const command = await getRuntimeCommandRecord(env.DB, driverInstanceId, commandId); + const command = await getRuntimeCommandRecord( + env.DB, + driverInstanceId, + driverGeneration, + commandId, + ); context.assertActiveConnection(); + const terminalPayload = + input.status === "failed" + ? { error: input.error } + : input.status === "completed" && input.result !== undefined + ? { result: toStoredRuntimeCommandResult(input.result) } + : {}; const updateOutcome = await updateRuntimeCommandRecord(env.DB, { commandId, deliveryConnectionId: context.connectionId, + driverGeneration, driverInstanceId, - ...(input.error === undefined ? {} : { error: input.error }), + ...terminalPayload, status: input.status, - ...(input.result === undefined ? {} : { result: toStoredRuntimeCommandResult(input.result) }), }); context.assertActiveConnection(); @@ -96,24 +116,9 @@ export class DriverInstanceRpcCommandController { throw new Error(`Runtime command status update rejected: ${updateOutcome.reason}.`); } - if ( - command?.payload.kind === "input.start" && - (input.status === "completed" || - input.status === "failed" || - input.status === "cancelled" || - input.status === "expired") - ) { - const release = releaseLinkedTerminalDriverInstanceSessionRun(env, driverInstanceId).catch( - (error: unknown) => { - this.#dependencies.withRuntimeLogContext(() => { - logError("runtime.terminal.lease_release.failed", { - ...createErrorLogContext(error), - driverInstanceId, - }); - }); - }, - ); - this.#dependencies.waitUntil(release); + if (command?.payload.kind === "input.start" && input.status !== "accepted") { + await releaseLinkedTerminalDriverInstanceSessionRun(env, driverInstanceId, driverGeneration); + context.assertActiveConnection(); } return { ok: true }; @@ -130,50 +135,57 @@ export class DriverInstanceRpcCommandController { } context.assertActiveConnection(); - return claimExternalToolEffect(env.DB, { + const claim = await claimExternalToolEffect(env.DB, { + claimToken: input.claimToken, commandId: parsePlatformId(input.commandId, "driver command id"), + driverGeneration: state.requireDriverGeneration(), driverInstanceId: state.requireDriverInstanceId(), }); + context.assertActiveConnection(); + return claim; } - async handleCompleteExternalToolEffect( - input: DriverExternalToolEffectCompleteInput, + async handleObserveExternalToolEffect( + input: DriverExternalToolEffectObserveInput, context: DriverInstanceRpcOperationContext, - ): Promise<{ ok: true }> { + ): Promise { const { env, state } = this.#dependencies; if (input.driverInstanceId !== state.requireDriverInstanceId()) { throw new Error("Driver instance id mismatch."); } context.assertActiveConnection(); - await completeExternalToolEffect(env.DB, { + + const observation = await observeExternalToolEffect(env.DB, { commandId: parsePlatformId(input.commandId, "driver command id"), + driverGeneration: state.requireDriverGeneration(), driverInstanceId: state.requireDriverInstanceId(), - ...(input.providerReceiptJson === undefined - ? {} - : { providerReceiptJson: input.providerReceiptJson }), - result: parseSchemaValue(McpExecuteCommandResult, input.result), }); context.assertActiveConnection(); - return { ok: true }; + return observation; } - async handleMarkExternalToolEffectUnknown( - input: DriverExternalToolEffectUnknownInput, + async handleSettleExternalToolEffect( + input: DriverExternalToolEffectSettleInput, context: DriverInstanceRpcOperationContext, - ): Promise<{ ok: true }> { + ): Promise { const { env, state } = this.#dependencies; if (input.driverInstanceId !== state.requireDriverInstanceId()) { throw new Error("Driver instance id mismatch."); } context.assertActiveConnection(); - await markExternalToolEffectUnknown(env.DB, { + + const settlement = await settleExternalToolEffect(env.DB, { + claimToken: input.claimToken, commandId: parsePlatformId(input.commandId, "driver command id"), + driverGeneration: state.requireDriverGeneration(), driverInstanceId: state.requireDriverInstanceId(), + effectId: parsePlatformId(input.effectId, "external tool effect id"), + settlement: parseSchemaValue(ExternalToolEffectSettlement, input.settlement), }); context.assertActiveConnection(); - return { ok: true }; + return settlement; } async handleNextCommand( @@ -195,6 +207,7 @@ export class DriverInstanceRpcCommandController { const record = await claimNextQueuedRuntimeCommandRecord( env.DB, driverInstanceId, + state.requireDriverGeneration(), context.connectionId, ); @@ -220,7 +233,7 @@ export class DriverInstanceRpcCommandController { async #markCommandDelivered( command: RuntimeCommand, context: DriverInstanceRpcOperationContext, - ): Promise { + ): Promise<"delivered" | "discarded" | "retry"> { const { env, state } = this.#dependencies; context.assertActiveConnection(); @@ -228,11 +241,18 @@ export class DriverInstanceRpcCommandController { const deliveryOutcome = await markRuntimeCommandRecordDelivered(env.DB, { commandId, connectionId: context.connectionId, + driverGeneration: state.requireDriverGeneration(), driverInstanceId: state.requireDriverInstanceId(), }); context.assertActiveConnection(); - return deliveryOutcome.kind === "applied"; + if (deliveryOutcome.kind === "applied") { + return "delivered"; + } + + return deliveryOutcome.kind === "rejected" && deliveryOutcome.reason === "inactive_session_run" + ? "discarded" + : "retry"; } async #nextCommand(context: DriverInstanceRpcOperationContext): Promise { diff --git a/apps/api/src/modules/runtime/infrastructure/driver-instance/rpc-controller-dependencies.ts b/apps/api/src/modules/runtime/infrastructure/driver-instance/rpc-controller-dependencies.ts index 49b6c3de..451a149a 100644 --- a/apps/api/src/modules/runtime/infrastructure/driver-instance/rpc-controller-dependencies.ts +++ b/apps/api/src/modules/runtime/infrastructure/driver-instance/rpc-controller-dependencies.ts @@ -3,10 +3,11 @@ import type { RuntimeSessionViewCache } from "./runtime-session-view-cache"; import type { DriverInstanceRuntimeState } from "./runtime-state"; import type { SessionViewerEventDeliveryBuffer } from "./session-viewer-event-delivery-buffer"; import type { DriverInstanceSocketRegistry } from "./sockets"; +import type { DriverInstanceConnectionEpoch } from "./state"; export interface DriverInstanceRpcControllerDependencies { env: ApiBindings; - finalizeTerminalState: () => Promise; + finalizeTerminalState: (epoch: DriverInstanceConnectionEpoch) => Promise; sockets: DriverInstanceSocketRegistry; state: DriverInstanceRuntimeState; viewCache: RuntimeSessionViewCache; diff --git a/apps/api/src/modules/runtime/infrastructure/driver-instance/rpc-controller.ts b/apps/api/src/modules/runtime/infrastructure/driver-instance/rpc-controller.ts index ab2db5af..3cd45f3c 100644 --- a/apps/api/src/modules/runtime/infrastructure/driver-instance/rpc-controller.ts +++ b/apps/api/src/modules/runtime/infrastructure/driver-instance/rpc-controller.ts @@ -1,12 +1,12 @@ import type { DriverCommandUpdateInput, DriverCompletionInput, - DriverEventBatchInput, DriverEventBatchOutput, DriverExternalToolEffectClaimInput, DriverExternalToolEffectClaimOutput, - DriverExternalToolEffectCompleteInput, - DriverExternalToolEffectUnknownInput, + DriverExternalToolEffectObserveInput, + DriverExternalToolEffectSettleInput, + DriverExternalToolEffectState, DriverFailureInput, DriverHeartbeatInput, DriverHelloInput, @@ -19,6 +19,7 @@ import type { } from "@mosoo/agent-driver/orpc"; import type { RuntimeCommand } from "@mosoo/contracts/runtime-command"; +import type { HostDriverEventBatchInput } from "./event-types"; import type { DriverInstanceRpcHandler, DriverInstanceRpcOperationContext } from "./rpc"; import { DriverInstanceRpcCommandController } from "./rpc-command-controller"; import type { DriverInstanceRpcControllerDependencies } from "./rpc-controller-dependencies"; @@ -39,8 +40,8 @@ export class DriverInstanceRpcController implements DriverInstanceRpcHandler { this.#terminal = new DriverInstanceRpcRunTerminalController(dependencies); } - async enqueueCommand(command: RuntimeCommand): Promise { - await this.#commands.enqueueCommand(command); + async enqueueCommand(driverGeneration: number, command: RuntimeCommand): Promise { + await this.#commands.enqueueCommand(driverGeneration, command); } async handleCommandUpdate( @@ -57,11 +58,11 @@ export class DriverInstanceRpcController implements DriverInstanceRpcHandler { return this.#commands.handleClaimExternalToolEffect(input, context); } - async handleCompleteExternalToolEffect( - input: DriverExternalToolEffectCompleteInput, + async handleObserveExternalToolEffect( + input: DriverExternalToolEffectObserveInput, context: DriverInstanceRpcOperationContext, - ): Promise<{ ok: true }> { - return this.#commands.handleCompleteExternalToolEffect(input, context); + ): Promise { + return this.#commands.handleObserveExternalToolEffect(input, context); } async handleCompleteRun( @@ -106,7 +107,7 @@ export class DriverInstanceRpcController implements DriverInstanceRpcHandler { } async handlePushEvents( - input: DriverEventBatchInput, + input: HostDriverEventBatchInput, context: DriverInstanceRpcOperationContext, ): Promise { return this.#events.handlePushEvents(input, context); @@ -119,11 +120,15 @@ export class DriverInstanceRpcController implements DriverInstanceRpcHandler { return this.#events.handlePushLogs(input, context); } - async handleMarkExternalToolEffectUnknown( - input: DriverExternalToolEffectUnknownInput, + async runAfterPendingEvents(operation: () => Promise): Promise { + return this.#events.runAfterPendingEvents(operation); + } + + async handleSettleExternalToolEffect( + input: DriverExternalToolEffectSettleInput, context: DriverInstanceRpcOperationContext, - ): Promise<{ ok: true }> { - return this.#commands.handleMarkExternalToolEffectUnknown(input, context); + ): Promise { + return this.#commands.handleSettleExternalToolEffect(input, context); } async handleReady( diff --git a/apps/api/src/modules/runtime/infrastructure/driver-instance/rpc-event-ingestion-controller.ts b/apps/api/src/modules/runtime/infrastructure/driver-instance/rpc-event-ingestion-controller.ts index 9c91b197..4969e5b5 100644 --- a/apps/api/src/modules/runtime/infrastructure/driver-instance/rpc-event-ingestion-controller.ts +++ b/apps/api/src/modules/runtime/infrastructure/driver-instance/rpc-event-ingestion-controller.ts @@ -1,33 +1,70 @@ -import type { DriverEventEnvelope } from "@mosoo/agent-driver/events"; +import { createMcpExecuteFailedEventIdentity } from "@mosoo/agent-driver/events"; import type { - DriverEventBatchInput, DriverEventBatchOutput, DriverEventReceipt, DriverLogBatchInput, DriverLogBatchOutput, } from "@mosoo/agent-driver/orpc"; +import { McpExecuteCommandResult } from "@mosoo/contracts/runtime-command"; +import { parseSchemaValue } from "@mosoo/contracts/validation"; import { parsePlatformId } from "@mosoo/id"; -import type { SessionRunId } from "@mosoo/id"; +import type { DriverCommandId, DriverInstanceId, SessionRunId } from "@mosoo/id"; +import { + createRuntimeEventSemanticHash, + isRuntimeEventRecord, + readRuntimeEventToolCallUpdate, +} from "@mosoo/runtime-events"; import { createErrorLogContext, logError } from "../../../../platform/cloudflare/logger"; -import type { SessionDeliveryEvent } from "../../../sessions/application/session-live-state.service"; +import { loadSessionViewerState } from "../../../sessions/application/session-live-state.service"; +import type { + SessionDeliveryEvent, + SessionLiveState, +} from "../../../sessions/application/session-live-state.service"; import { getSessionRuntimeEventSourceReceipts } from "../../../sessions/infrastructure/session-runtime-event-store.repository"; -import { createSessionRunTerminalFailureSourceId } from "../../domain/session-run-terminal-event-id"; +import type { SessionRuntimeEventSourceReceipt } from "../../../sessions/infrastructure/session-runtime-event-store.repository"; +import { createSessionRunTerminalSourceId } from "../../domain/session-run-terminal-event-id"; +import { getExternalToolEffectForCommand } from "../session-runs/external-tool-effect-store.repository"; +import { getRuntimeCommandRecord } from "../session-runs/runtime-command-store.repository"; import { EVENT_BATCH_MAX_SIZE, LOG_BATCH_MAX_SIZE } from "./connections"; +import { canonicalizeDriverEventEnvelope } from "./driver-event-canonicalization"; import { DriverEventTerminalGate } from "./driver-event-terminal-gate"; import { publishDriverLogBatch } from "./driver-log-batch-publisher"; import { runtimeSessionLinkNeedsRefresh } from "./event-types"; +import type { CanonicalDriverEventEnvelope, HostDriverEventBatchInput } from "./event-types"; import { getRuntimeSessionLink, + persistProjectedRuntimeDriverEventPrerequisites, persistProjectedRuntimeDriverEvents, + preflightProjectedRuntimeDriverEvents, appRuntimeDriverEvents, } from "./events"; import type { RuntimeSessionLink } from "./events"; import type { DriverInstanceRpcOperationContext } from "./rpc"; import type { DriverInstanceRpcControllerDependencies } from "./rpc-controller-dependencies"; -import { filterDurablyAcceptedRuntimeStreamReplays } from "./runtime-event-replay-filter"; +import { stageRuntimeArtifactEvents } from "./runtime-artifact-staging"; +import { assertActiveRuntimeSessionRun } from "./session-link.repository"; + +interface HashedDriverEvent { + readonly envelope: CanonicalDriverEventEnvelope; + readonly persistenceSourceId: string; + readonly semanticHash: string; +} -function summarizeDriverEvents(events: readonly DriverEventEnvelope[]) { +interface PreparedDriverEventBatch { + readonly uniqueOuterEvents: readonly HashedDriverEvent[]; + readonly uniquePersistenceEvents: readonly HashedDriverEvent[]; +} + +function toRuntimeArtifactEvent(event: HashedDriverEvent) { + return { + event: event.envelope.event, + semanticHash: event.semanticHash, + sourceEventId: event.persistenceSourceId, + }; +} + +function summarizeDriverEvents(events: readonly CanonicalDriverEventEnvelope[]) { return { eventCount: events.length, eventKinds: events.map((event) => event.event.kind).slice(0, 24), @@ -36,7 +73,7 @@ function summarizeDriverEvents(events: readonly DriverEventEnvelope[]) { } function resolveEventSessionRunId( - events: readonly DriverEventEnvelope[], + events: readonly { readonly event: { readonly runId?: unknown } }[], ): SessionRunId | undefined { let eventRunId: string | undefined; @@ -46,6 +83,9 @@ function resolveEventSessionRunId( if (candidateRunId === undefined) { continue; } + if (typeof candidateRunId !== "string") { + throw new TypeError("Driver event run id must be a string."); + } if (eventRunId !== undefined && eventRunId !== candidateRunId) { throw new Error("Event batch cannot contain events from multiple runs."); @@ -59,10 +99,16 @@ function resolveEventSessionRunId( : parsePlatformId(eventRunId, "driver event run id"); } -function resolveDriverEventPersistenceSourceId(event: DriverEventEnvelope): string { - if (event.event.kind === "run.failed" && event.event.runId !== undefined) { - return createSessionRunTerminalFailureSourceId( - parsePlatformId(event.event.runId, "driver failed event run id"), +function resolveDriverEventPersistenceSourceId(event: CanonicalDriverEventEnvelope): string { + if ( + (event.event.kind === "run.cancelled" || + event.event.kind === "run.completed" || + event.event.kind === "run.failed") && + event.event.runId !== undefined + ) { + return createSessionRunTerminalSourceId( + parsePlatformId(event.event.runId, "driver terminal event run id"), + event.event.kind, ); } @@ -82,7 +128,7 @@ export class DriverInstanceRpcEventIngestionController { } public async handlePushEvents( - input: DriverEventBatchInput, + input: HostDriverEventBatchInput, context: DriverInstanceRpcOperationContext, ): Promise { const { state } = this.#dependencies; @@ -118,23 +164,68 @@ export class DriverInstanceRpcEventIngestionController { ...(eventSessionRunId === undefined ? {} : { sessionRunId: eventSessionRunId }), }); context.assertActiveConnection(); - const replayedReceipts = state.readProcessedDriverEventReceipts(input.events); - const candidateEvents = state.filterUnprocessedDriverEvents(input.events); - const durableReceipts = await this.#readPersistedEventReceipts(link, candidateEvents); + const canonicalInputEvents = input.events.map((event) => + canonicalizeDriverEventEnvelope(event, { traceId: link.traceId }), + ); + const preparedEvents = await prepareDriverEventBatch(canonicalInputEvents); + assertDriverEventBatchTerminalOrder( + preparedEvents.uniquePersistenceEvents.map((event) => event.envelope), + ); + const provenMcpCommandIds = await proveMcpDriverEvents(env.DB, { + driverGeneration: state.requireDriverGeneration(), + driverInstanceId, + events: preparedEvents.uniquePersistenceEvents, + }); + const durableReceiptsBySource = await this.#readPersistedEventReceipts( + link, + preparedEvents.uniquePersistenceEvents, + ); + const durableReceipts = projectDriverEventReceipts( + preparedEvents.uniqueOuterEvents, + durableReceiptsBySource, + ); context.assertActiveConnection(); - const durableEventIds = new Set( - durableReceipts.flatMap((receipt) => - receipt.eventId === undefined ? [] : [receipt.eventId], - ), + const pendingEvents = preparedEvents.uniquePersistenceEvents.filter( + (event) => !durableReceiptsBySource.has(event.persistenceSourceId), ); - const events = filterDurablyAcceptedRuntimeStreamReplays(candidateEvents, durableEventIds); - const replayedAccepted = [...replayedReceipts, ...durableReceipts]; - + const events = pendingEvents.map((event) => event.envelope); + const containsTerminalEvent = preparedEvents.uniquePersistenceEvents.some( + ({ envelope: { event } }) => + event.kind === "run.cancelled" || + event.kind === "run.completed" || + event.kind === "run.failed", + ); + let requiresDurableStateSync = durableReceipts.length > 0 || containsTerminalEvent; + let requiresDriverStateReload = requiresDurableStateSync && !containsTerminalEvent; if (events.length === 0) { - state.rememberProcessedDriverEventReceipts(replayedAccepted); - return { accepted: replayedAccepted }; + const replayState = await this.#readDurableStateForSync(link, requiresDriverStateReload); + context.assertActiveConnection(); + if (replayState !== null) { + viewCache.update(replayState); + } else if (containsTerminalEvent) { + // Terminal state is sealed in D1. Do not rebuild an unbounded final + // transcript in this hibernatable Driver DO merely to discard it. + viewCache.reset(); + } + if (requiresDurableStateSync) { + viewerEventDelivery.requestStateSync(link.sessionId); + } + return { accepted: durableReceipts }; } + const assertActiveRun = async (): Promise => { + if (eventSessionRunId === undefined || link.sessionId === null) { + return; + } + + await assertActiveRuntimeSessionRun(env.DB, { + driverInstanceId, + sessionId: link.sessionId, + sessionRunId: eventSessionRunId, + }); + }; + + await assertActiveRun(); const projection = await (async () => { try { return await appRuntimeDriverEvents(env, { @@ -142,7 +233,9 @@ export class DriverInstanceRpcEventIngestionController { currentLiveState: viewCache.currentState, driverInstanceId, events, + projectLiveState: !containsTerminalEvent, link, + provenMcpCommandIds, }); } catch (error) { logError("runtime.driver.events.projection_failed", { @@ -153,14 +246,50 @@ export class DriverInstanceRpcEventIngestionController { throw error; } })(); + await persistProjectedRuntimeDriverEventPrerequisites(env.DB, { + driverConnectionId: context.connectionId, + driverGeneration: state.requireDriverGeneration(), + driverInstanceId, + projection, + }); + await preflightProjectedRuntimeDriverEvents(env.DB, projection); + context.assertActiveConnection(); + const artifactProjections = await stageRuntimeArtifactEvents(env, { + driverFence: { + connectionId: context.connectionId, + driverInstanceId, + generation: state.requireDriverGeneration(), + sessionRunId: link.sessionRunId, + }, + events: pendingEvents.map(toRuntimeArtifactEvent), + link, + }); + if (artifactProjections.size > 0) { + requiresDurableStateSync = true; + requiresDriverStateReload = !containsTerminalEvent; + } + context.assertActiveConnection(); // An accepted source identity must already be durable. Buffering stream // fragments only in this hibernatable DO would acknowledge text that a // fresh instance cannot reconstruct, so persist every canonical event. - const persistenceRuntimeEvents = projection.runtimeEvents; + const persistenceRuntimeEvents = projection.runtimeEvents.map((record) => { + const artifact = + record.sourceEventId === null ? undefined : artifactProjections.get(record.sourceEventId); + return artifact === undefined + ? record + : { + ...record, + artifactAttemptId: artifact.attemptId, + artifactManifestJson: artifact.manifestJson, + artifactManifestSha256: artifact.manifestSha256, + }; + }); const commit = await (async () => { try { return await persistProjectedRuntimeDriverEvents(env, { + driverConnectionId: context.connectionId, + driverGeneration: state.requireDriverGeneration(), driverInstanceId, projection: { ...projection, @@ -184,22 +313,59 @@ export class DriverInstanceRpcEventIngestionController { })(); context.assertActiveConnection(); - if (commit.liveState) { - viewCache.update(commit.liveState); + const committedReceiptsBySource = await this.#readPersistedEventReceipts( + link, + preparedEvents.uniquePersistenceEvents, + ); + if (committedReceiptsBySource.size !== preparedEvents.uniquePersistenceEvents.length) { + throw new Error("Driver event commit did not produce every durable source receipt."); } - - viewerEventDelivery.enqueue( - projection.link.sessionId, - filterDurablyCommittedDeliveryEvents({ - persistenceEvents: persistenceRuntimeEvents, - persistedSourceEventIds: commit.persistedSourceEventIds, - sessionDeliveryEvents: projection.sessionDeliveryEvents, - }), + const persistedRuntimeEventSeqs = commit.persistedSourceEventIds.flatMap( + (sourceEventId): number[] => { + const seq = committedReceiptsBySource.get(sourceEventId)?.seq; + return seq === undefined ? [] : [seq]; + }, ); + const deliveryRuntimeEventSeqCursor = + persistedRuntimeEventSeqs.length === 0 ? null : Math.max(...persistedRuntimeEventSeqs); + const previousDeliveryRuntimeEventSeqCursor = + deliveryRuntimeEventSeqCursor === null || + persistedRuntimeEventSeqs.length === 0 || + deliveryRuntimeEventSeqCursor - Math.min(...persistedRuntimeEventSeqs) + 1 !== + persistedRuntimeEventSeqs.length + ? null + : Math.min(...persistedRuntimeEventSeqs) - 1; + + const replayState = await this.#readDurableStateForSync(link, requiresDriverStateReload); + context.assertActiveConnection(); + if (containsTerminalEvent) { + viewCache.reset(); + } else { + const committedLiveState = replayState ?? commit.liveState; + if (committedLiveState !== null) { + viewCache.update(committedLiveState); + } + } - const accepted = [...replayedAccepted, ...state.createDriverEventReceipts(events)]; + if (!requiresDurableStateSync) { + viewerEventDelivery.enqueue( + projection.link.sessionId, + filterDurablyCommittedDeliveryEvents({ + persistenceEvents: persistenceRuntimeEvents, + persistedSourceEventIds: commit.persistedSourceEventIds, + sessionDeliveryEvents: projection.sessionDeliveryEvents, + }), + deliveryRuntimeEventSeqCursor, + previousDeliveryRuntimeEventSeqCursor, + ); + } else { + viewerEventDelivery.requestStateSync(link.sessionId); + } - state.rememberProcessedDriverEventReceipts(accepted); + const accepted = projectDriverEventReceipts( + preparedEvents.uniqueOuterEvents, + committedReceiptsBySource, + ); return { accepted }; }); @@ -292,18 +458,16 @@ export class DriverInstanceRpcEventIngestionController { async #readPersistedEventReceipts( link: RuntimeSessionLink, - events: readonly DriverEventEnvelope[], - ): Promise { + events: readonly HashedDriverEvent[], + ): Promise> { if (link.sessionId === null) { - return []; + return new Map(); } - const sourceEventIds = events - .map(resolveDriverEventPersistenceSourceId) - .filter((eventId) => eventId.length > 0); + const sourceEventIds = events.map((event) => event.persistenceSourceId); if (sourceEventIds.length === 0) { - return []; + return new Map(); } const receiptsByEventId = await getSessionRuntimeEventSourceReceipts( @@ -313,27 +477,310 @@ export class DriverInstanceRpcEventIngestionController { sourceEventIds, }, ); - const receipts: DriverEventReceipt[] = []; - const seenEventIds = new Set(); - for (const event of events) { - if (event.eventId.length === 0 || seenEventIds.has(event.eventId)) { + const receipt = receiptsByEventId.get(event.persistenceSourceId); + + if (receipt === undefined) { continue; } - seenEventIds.add(event.eventId); + if ( + receipt.semanticHash === null || + receipt.semanticHash !== event.semanticHash || + receipt.type !== event.envelope.event.kind + ) { + throw new Error( + `Driver event source ${event.persistenceSourceId} conflicts with its durable receipt.`, + ); + } + } - const receipt = receiptsByEventId.get(resolveDriverEventPersistenceSourceId(event)); + return receiptsByEventId; + } - if (receipt === undefined) { - continue; + async #readDurableStateForSync( + link: RuntimeSessionLink, + required: boolean, + ): Promise { + if (link.sessionId === null || !required) { + return null; + } + const viewerId = link.callerId ?? link.creatorId; + if (viewerId === null) { + throw new Error("Durable Driver event replay is missing its Session viewer identity."); + } + + return loadSessionViewerState(this.#dependencies.env.DB, { + sessionId: link.sessionId, + viewerId, + }); + } +} + +function projectDriverEventReceipts( + events: readonly HashedDriverEvent[], + receiptsBySource: ReadonlyMap, +): DriverEventReceipt[] { + return events.flatMap((event) => { + const receipt = receiptsBySource.get(event.persistenceSourceId); + + return receipt === undefined + ? [] + : [ + { + eventId: event.envelope.eventId, + seq: receipt.seq, + type: event.envelope.event.kind, + }, + ]; + }); +} + +async function prepareDriverEventBatch( + events: readonly CanonicalDriverEventEnvelope[], +): Promise { + const hashedEvents = await Promise.all( + events.map(async (envelope) => ({ + envelope, + persistenceSourceId: resolveDriverEventPersistenceSourceId(envelope), + semanticHash: await createRuntimeEventSemanticHash(envelope.event), + })), + ); + const outerIdentities = new Map(); + const persistenceIdentities = new Map(); + + for (const event of hashedEvents) { + const outer = outerIdentities.get(event.envelope.eventId); + + if (outer !== undefined) { + assertMatchingDriverEventIdentity(outer, event, "outer"); + } else { + outerIdentities.set(event.envelope.eventId, event); + } + + const persisted = persistenceIdentities.get(event.persistenceSourceId); + + if (persisted !== undefined) { + assertMatchingDriverEventIdentity(persisted, event, "durable"); + } else { + persistenceIdentities.set(event.persistenceSourceId, event); + } + } + + return { + uniqueOuterEvents: [...outerIdentities.values()], + uniquePersistenceEvents: [...persistenceIdentities.values()], + }; +} + +function assertMatchingDriverEventIdentity( + expected: HashedDriverEvent, + actual: HashedDriverEvent, + identityKind: "durable" | "outer", +): void { + if ( + expected.persistenceSourceId !== actual.persistenceSourceId || + expected.semanticHash !== actual.semanticHash + ) { + throw new Error(`Driver event batch contains a conflicting ${identityKind} source identity.`); + } +} + +export function assertDriverEventBatchTerminalOrder( + events: readonly CanonicalDriverEventEnvelope[], +): void { + const terminalIndexes = events.flatMap((event, index) => + event.event.kind === "run.cancelled" || + event.event.kind === "run.completed" || + event.event.kind === "run.failed" + ? [index] + : [], + ); + + if (terminalIndexes.length > 1) { + throw new Error("Driver event batch cannot contain multiple run terminal events."); + } + + if (terminalIndexes[0] !== undefined && terminalIndexes[0] !== events.length - 1) { + throw new Error("Driver event batch run terminal event must be last."); + } +} + +function readMcpDriverEventIdentity(event: CanonicalDriverEventEnvelope): { + commandId: DriverCommandId; + sourceEventId: string; + status: "cancelled" | "completed" | "failed" | "running"; +} | null { + const sourceEventId = event.event.sourceEventId ?? event.eventId; + const reservedSource = sourceEventId.startsWith("mcp.execute."); + const mcpPayload = + event.event.kind === "tool.call.updated" && + readRuntimeEventToolCallUpdate(event.event).kind === "mcp"; + + if (!reservedSource && !mcpPayload) { + return null; + } + if (!reservedSource || !mcpPayload) { + throw new Error("MCP tool events require their reserved command source identity."); + } + + const commandId = parsePlatformId( + event.event.correlationId, + "MCP event command correlation ID", + ); + + for (const status of ["running", "completed", "failed", "cancelled"] as const) { + const prefix = `mcp.execute.${status}:`; + + if (!sourceEventId.startsWith(prefix)) { + continue; + } + + if (event.event.sourceEventId !== sourceEventId || event.eventId !== sourceEventId) { + throw new Error("MCP event transport identity does not match its command."); + } + + if ( + status !== "failed" && + commandId !== + parsePlatformId( + sourceEventId.slice(prefix.length), + "MCP event source command ID", + ) + ) { + throw new Error("MCP event transport identity does not match its command."); + } + + return { commandId, sourceEventId, status }; + } + + throw new Error("MCP event source identity has an unsupported terminal status."); +} + +async function proveMcpDriverEvents( + database: D1Database, + input: { + driverGeneration: number; + driverInstanceId: DriverInstanceId; + events: readonly HashedDriverEvent[]; + }, +): Promise> { + const proven = new Map(); + + for (const { envelope } of input.events) { + const identity = readMcpDriverEventIdentity(envelope); + + if (identity === null) { + continue; + } + + if (envelope.event.kind !== "tool.call.updated") { + throw new Error("MCP command source identity requires a tool.call.updated event."); + } + + const record = await getRuntimeCommandRecord( + database, + input.driverInstanceId, + input.driverGeneration, + identity.commandId, + ); + + if (record === null || record.kind !== "mcp.execute") { + throw new Error("MCP event does not have an immutable command in this Driver generation."); + } + + if ( + record.status !== "accepted" && + (identity.status === "running" || record.status !== identity.status) + ) { + throw new Error("MCP event conflicts with the command's durable lifecycle status."); + } + + const command = record.payload; + const toolCall = readRuntimeEventToolCallUpdate(envelope.event); + const effect = await getExternalToolEffectForCommand(database, { + commandId: identity.commandId, + driverGeneration: input.driverGeneration, + driverInstanceId: input.driverInstanceId, + }); + + if ( + effect === null || + envelope.event.driverInstanceId !== input.driverInstanceId || + envelope.event.runId !== command.runId || + toolCall.content !== null || + toolCall.kind !== "mcp" || + toolCall.messageId !== null || + toolCall.parentMessageId !== null || + toolCall.rawInput !== command.argumentsJson || + toolCall.rawInputDelta !== null || + toolCall.rawOutputDelta !== null || + toolCall.status !== identity.status || + toolCall.title !== command.toolName || + toolCall.toolCallId !== command.toolCallId + ) { + throw new Error("MCP event does not match its immutable command intent."); + } + + if (identity.status === "completed") { + if (effect.status !== "succeeded" || effect.resultJson === null) { + throw new Error("MCP completion event has no succeeded durable external effect."); } - receipts.push({ ...receipt, eventId: event.eventId }); + const result = parseSchemaValue(McpExecuteCommandResult, JSON.parse(effect.resultJson)); + + if ( + result.requestId !== command.requestId || + result.serverId !== command.serverId || + result.toolName !== command.toolName || + toolCall.rawOutput !== result.outputText || + (record.status === "completed" && + (record.result === null || + record.result.outputText !== result.outputText || + record.result.requestId !== result.requestId || + record.result.serverId !== result.serverId || + record.result.toolName !== result.toolName || + (record.result.isError ?? false) !== (result.isError ?? false))) + ) { + throw new Error("MCP completion event conflicts with its durable external effect result."); + } + } else if (identity.status === "failed") { + if ( + (effect.status !== "intent" && effect.status !== "unknown") || + toolCall.rawOutput === null || + toolCall.rawOutput.length === 0 || + (record.status === "failed" && record.error?.message !== toolCall.rawOutput) + ) { + throw new Error("MCP failure event conflicts with its durable external effect state."); + } + + const failedEvent = createMcpExecuteFailedEventIdentity({ + commandId: identity.commandId, + rawInput: command.argumentsJson, + rawOutput: toolCall.rawOutput, + title: command.toolName, + toolCallId: command.toolCallId, + }); + const payload = envelope.event.payload; + + if ( + identity.sourceEventId !== failedEvent.sourceEventId || + !isRuntimeEventRecord(payload) || + Object.keys(payload).length !== Object.keys(failedEvent.payload).length || + Object.entries(failedEvent.payload).some(([key, value]) => payload[key] !== value) + ) { + throw new Error("MCP failure event does not have its canonical content identity."); + } + } else if (toolCall.rawOutput !== null || effect.status !== "intent") { + throw new Error("Unsettled MCP event contains an impossible external effect output."); } - return receipts; + if (identity.status !== "running") { + proven.set(envelope.eventId, identity.commandId); + } } + + return proven; } function filterDurablyCommittedDeliveryEvents(input: { diff --git a/apps/api/src/modules/runtime/infrastructure/driver-instance/rpc-handshake-controller.ts b/apps/api/src/modules/runtime/infrastructure/driver-instance/rpc-handshake-controller.ts index 0d4b892b..d6b6e81b 100644 --- a/apps/api/src/modules/runtime/infrastructure/driver-instance/rpc-handshake-controller.ts +++ b/apps/api/src/modules/runtime/infrastructure/driver-instance/rpc-handshake-controller.ts @@ -5,7 +5,6 @@ import type { DriverReadyInput, } from "@mosoo/agent-driver/orpc"; import { SANDBOX_ORGANIZATION_ROOT } from "@mosoo/agent-driver/paths"; -import { createPlatformId } from "@mosoo/id"; import { logInfo } from "../../../../platform/cloudflare/logger"; import { DRIVER_HEARTBEAT_INTERVAL_MS } from "../../domain/runtime-config"; @@ -67,31 +66,57 @@ export class DriverInstanceRpcHandshakeController { context: DriverInstanceRpcOperationContext, ): Promise { const { env, state, withRuntimeLogContext } = this.#dependencies; + context.assertActiveConnection(); + + let link: RuntimeSessionLink | null = null; + let output = state.helloOutput ?? state.pendingHello?.output ?? null; - if (state.hello) { - throw new Error("Driver hello has already been received."); + if (output === null) { + link = await this.#getRuntimeSessionLink(context); + context.assertActiveConnection(); + output = { + acceptedCapabilities: input.capabilities, + connectionId: context.connectionId, + driverInstanceId: state.requireDriverInstanceId(), + heartbeatIntervalMs: DRIVER_HEARTBEAT_INTERVAL_MS, + runConfig: { + commandLeaseMs: COMMAND_LEASE_MS, + envPolicy: "strict", + eventBatchMaxSize: EVENT_BATCH_MAX_SIZE, + organizationPath: SANDBOX_ORGANIZATION_ROOT, + }, + runId: link.sessionRunId, + }; } + + const staged = await state.stageHello(context.epoch, input, output); context.assertActiveConnection(); - const recorded = await recordDriverInstanceHello(env, { + if (staged === "replay") { + return output; + } + + const projection = await recordDriverInstanceHello(env, { connectionId: context.connectionId, driverInstanceId: state.requireDriverInstanceId(), - generation: state.requireDriverGeneration(), + generation: context.epoch.generation, hello: input, }); - if (!recorded) { - throw new Error("Driver connection is no longer current."); + if (projection === "conflict") { + throw new Error("Driver hello conflicts with the lifecycle projection."); } context.assertActiveConnection(); - const result = await state.recordHello(input); - state.resolveHelloWaiters(result); + const committedOutput = await state.commitHello(context.epoch); + context.assertActiveConnection(); - const link = await this.#getRuntimeSessionLink(); + link ??= await this.#getRuntimeSessionLink(context); + context.assertActiveConnection(); if (state.traceId === null && link.traceId !== null) { - await state.setTraceId(link.traceId); + await state.setTraceId(link.traceId, context.epoch); + context.assertActiveConnection(); } withRuntimeLogContext(() => { @@ -105,19 +130,7 @@ export class DriverInstanceRpcHandshakeController { }); }); - return { - acceptedCapabilities: input.capabilities, - connectionId: state.connectionId ?? createPlatformId(), - driverInstanceId: state.requireDriverInstanceId(), - heartbeatIntervalMs: DRIVER_HEARTBEAT_INTERVAL_MS, - runConfig: { - commandLeaseMs: COMMAND_LEASE_MS, - envPolicy: "strict", - eventBatchMaxSize: EVENT_BATCH_MAX_SIZE, - organizationPath: SANDBOX_ORGANIZATION_ROOT, - }, - runId: link.sessionRunId, - }; + return committedOutput; } async handleReady( @@ -134,25 +147,29 @@ export class DriverInstanceRpcHandshakeController { throw new Error("Driver hello is required before ready."); } - if (state.ready) { - throw new Error("Driver ready has already been received."); - } context.assertActiveConnection(); - const markedReady = await markDriverInstanceReady(env, { + const staged = await state.stageReady(context.epoch, input); + context.assertActiveConnection(); + + if (staged === "replay") { + return { ok: true }; + } + + const projection = await markDriverInstanceReady(env, { ...input, connectionId: context.connectionId, driverInstanceId: state.requireDriverInstanceId(), - generation: state.requireDriverGeneration(), + generation: context.epoch.generation, }); - if (!markedReady) { - throw new Error("Driver connection is no longer current."); + if (projection === "conflict") { + throw new Error("Driver ready conflicts with the lifecycle projection."); } context.assertActiveConnection(); - const result = await state.recordReady(input); - state.resolveReadyWaiters(result); + const result = await state.commitReady(context.epoch); + state.resolveReadyWaiters(result, context.epoch.generation); withRuntimeLogContext(() => { logInfo("runtime.driver.ready.received", { @@ -165,7 +182,10 @@ export class DriverInstanceRpcHandshakeController { return { ok: true }; } - async #getRuntimeSessionLink(options: { refresh?: boolean } = {}): Promise { + async #getRuntimeSessionLink( + context: DriverInstanceRpcOperationContext, + options: { refresh?: boolean } = {}, + ): Promise { const { env, state } = this.#dependencies; if (options.refresh !== true && state.runtimeSessionLink !== null) { @@ -173,6 +193,7 @@ export class DriverInstanceRpcHandshakeController { } const link = await getRuntimeSessionLink(env.DB, state.requireDriverInstanceId()); + context.assertActiveConnection(); state.setRuntimeSessionLink(link); return link; } diff --git a/apps/api/src/modules/runtime/infrastructure/driver-instance/rpc-run-terminal-controller.ts b/apps/api/src/modules/runtime/infrastructure/driver-instance/rpc-run-terminal-controller.ts index 126fd5f5..b94a9b2e 100644 --- a/apps/api/src/modules/runtime/infrastructure/driver-instance/rpc-run-terminal-controller.ts +++ b/apps/api/src/modules/runtime/infrastructure/driver-instance/rpc-run-terminal-controller.ts @@ -1,7 +1,8 @@ import type { DriverCompletionInput, DriverFailureInput } from "@mosoo/agent-driver/orpc"; +import { parsePlatformId } from "@mosoo/id"; +import type { SessionRunId } from "@mosoo/id"; import { logError, logInfo } from "../../../../platform/cloudflare/logger"; -import { syncSessionViewerState } from "../../../sessions/application/session-viewer-events.service"; import { runtimeSessionLinkNeedsRefresh } from "./event-types"; import { getRuntimeSessionLink, @@ -36,14 +37,23 @@ export class DriverInstanceRpcRunTerminalController { throw new Error("Driver instance id mismatch."); } const driverInstanceId = state.requireDriverInstanceId(); + const sessionRunId = readTerminalRunId(input); context.assertActiveConnection(); - await viewerEventDelivery.flushSafely(); - context.assertActiveConnection(); await recordDriverInstanceCompletion(env, { + driverConnectionId: context.connectionId, + driverGeneration: context.epoch.generation, driverInstanceId, - driverReady: state.hello !== null, + sessionRunId, + }); + + await state.setTerminalSessionRunId(sessionRunId, context.epoch); + + const link = await this.#getRuntimeSessionLink(context, { + refresh: runtimeSessionLinkNeedsRefresh(state.runtimeSessionLink), + sessionRunId, }); + viewerEventDelivery.requestStateSync(link.sessionId); context.assertActiveConnection(); withRuntimeLogContext(() => { @@ -54,24 +64,22 @@ export class DriverInstanceRpcRunTerminalController { }); }); - const socket = sockets.getDriverSocket(); + const socket = sockets.getDriverSocket(context.epoch); if (socket && socket.readyState === WebSocket.OPEN) { - sockets.scheduleDriverSocketClose(1000, "runtime.completed"); + sockets.scheduleDriverSocketClose(context.epoch, 1000, "runtime.completed"); } else { - await state.persistClose({ - at: new Date().toISOString(), - code: 1000, - reason: "runtime.completed", - }); - await finalizeTerminalState(); + await state.persistClose( + { + at: new Date().toISOString(), + code: 1000, + reason: "runtime.completed", + }, + context.epoch, + ); + await finalizeTerminalState(context.epoch); } - const link = await this.#getRuntimeSessionLink({ - refresh: runtimeSessionLinkNeedsRefresh(state.runtimeSessionLink), - }); - await syncSessionViewerState(env, link.sessionId); - return { ok: true }; } @@ -92,18 +100,25 @@ export class DriverInstanceRpcRunTerminalController { throw new Error("Driver instance id mismatch."); } const driverInstanceId = state.requireDriverInstanceId(); + const sessionRunId = readTerminalRunId(input); context.assertActiveConnection(); - await viewerEventDelivery.flushSafely(); - context.assertActiveConnection(); - const link = await this.#getRuntimeSessionLink({ + const link = await this.#getRuntimeSessionLink(context, { refresh: runtimeSessionLinkNeedsRefresh(state.runtimeSessionLink), + sessionRunId, }); await recordDriverInstanceFailure(env, { + driverConnectionId: context.connectionId, + driverGeneration: context.epoch.generation, driverInstanceId, error: input.error, link, + sessionRunId, }); + + await state.setTerminalSessionRunId(sessionRunId, context.epoch); + + viewerEventDelivery.requestStateSync(link.sessionId); context.assertActiveConnection(); withRuntimeLogContext(() => { @@ -117,30 +132,47 @@ export class DriverInstanceRpcRunTerminalController { }); }); - await state.setErrorMessage(input.error.message); + await state.setConnectionErrorMessage(context.epoch, input.error.message); + context.assertActiveConnection(); - const socket = sockets.getDriverSocket(); + const socket = sockets.getDriverSocket(context.epoch); if (socket && socket.readyState === WebSocket.OPEN) { - sockets.scheduleDriverSocketClose(1011, "runtime.failed"); + sockets.scheduleDriverSocketClose(context.epoch, 1011, "runtime.failed"); } else { - await finalizeTerminalState(); + await finalizeTerminalState(context.epoch); } - await syncSessionViewerState(env, link.sessionId); - return { ok: true }; } - async #getRuntimeSessionLink(options: { refresh?: boolean } = {}): Promise { + async #getRuntimeSessionLink( + context: DriverInstanceRpcOperationContext, + options: { refresh?: boolean; sessionRunId: SessionRunId }, + ): Promise { const { env, state } = this.#dependencies; - if (options.refresh !== true && state.runtimeSessionLink !== null) { + if ( + options.refresh !== true && + state.runtimeSessionLink?.sessionRunId === options.sessionRunId + ) { return state.runtimeSessionLink; } - const link = await getRuntimeSessionLink(env.DB, state.requireDriverInstanceId()); + const link = await getRuntimeSessionLink(env.DB, state.requireDriverInstanceId(), { + sessionRunId: options.sessionRunId, + }); + context.assertActiveConnection(); + + if (link.sessionRunId !== options.sessionRunId) { + throw new Error("Terminal Driver Session Run identity does not match the request."); + } + state.setRuntimeSessionLink(link); return link; } } + +function readTerminalRunId(input: DriverCompletionInput | DriverFailureInput): SessionRunId { + return parsePlatformId(input.runId, "terminal Driver Session Run id"); +} diff --git a/apps/api/src/modules/runtime/infrastructure/driver-instance/rpc-wire.ts b/apps/api/src/modules/runtime/infrastructure/driver-instance/rpc-wire.ts index afbde39b..a4cc7d6b 100644 --- a/apps/api/src/modules/runtime/infrastructure/driver-instance/rpc-wire.ts +++ b/apps/api/src/modules/runtime/infrastructure/driver-instance/rpc-wire.ts @@ -1,13 +1,12 @@ -import { parseDriverEventEnvelope } from "@mosoo/agent-driver/events"; import type { DriverCommandUpdateInput, DriverCompletionInput, - DriverEventBatchInput, DriverEventBatchOutput, DriverExternalToolEffectClaimInput, DriverExternalToolEffectClaimOutput, - DriverExternalToolEffectCompleteInput, - DriverExternalToolEffectUnknownInput, + DriverExternalToolEffectObserveInput, + DriverExternalToolEffectSettleInput, + DriverExternalToolEffectState, DriverFailureInput, DriverHeartbeatInput, DriverHeartbeatOutput, @@ -20,156 +19,235 @@ import type { DriverReadyInput, } from "@mosoo/agent-driver/orpc"; import { DriverCapability } from "@mosoo/contracts/driver-instance"; -import { ExternalToolEffectClaim } from "@mosoo/contracts/external-tool-effect"; import { - RuntimeCommand, + ExternalToolEffectClaim, + ExternalToolEffectClaimToken, + ExternalToolEffectSettlement, + ExternalToolEffectState, +} from "@mosoo/contracts/external-tool-effect"; +import { + InputStartCommandResult, McpExecuteCommandResult, - RuntimeCommandResult, - RuntimeCommandStatus, + RuntimeCommand, } from "@mosoo/contracts/runtime-command"; -import { RunError } from "@mosoo/contracts/session-run"; +import { DurableRunError } from "@mosoo/contracts/session-run"; import { NonEmptyString, PrimitiveRecord, parseSchemaValue } from "@mosoo/contracts/validation"; +import { + RUNTIME_EVENT_KINDS, + RUNTIME_EVENT_SCHEMA_VERSION, + parseRuntimeEventEnvelope, +} from "@mosoo/runtime-events"; import { eventIterator, os } from "@orpc/server"; import { type } from "arktype"; +import { EVENT_BATCH_MAX_SIZE, LOG_BATCH_MAX_SIZE } from "./connections"; +import type { HostDriverEventBatchInput } from "./event-types"; + +const PositiveSafeInteger = type("number.integer >= 1 & number.safe"); +const NonNegativeSafeInteger = type("number.integer >= 0 & number.safe"); +const DriverEventBatchMaxSizeWire = PositiveSafeInteger.narrow((size, context) => + size <= EVENT_BATCH_MAX_SIZE + ? true + : context.reject({ expected: `at most ${EVENT_BATCH_MAX_SIZE}` }), +); +const DriverCapabilitiesWire = DriverCapability.array().narrow((capabilities, context) => + new Set(capabilities.map(({ id }) => id)).size === capabilities.length + ? true + : context.reject({ expected: "unique capability ids" }), +); + const DriverHelloInputWire = type({ - capabilities: DriverCapability.array(), + capabilities: DriverCapabilitiesWire, driverVersion: NonEmptyString, - pid: "number", - protocolVersion: "2", + pid: PositiveSafeInteger, + protocolVersion: "3", runtime: '"openai-runtime" | "claude-agent-sdk" | "acp-fallback"', - startedAt: "string", -}); + startedAt: NonEmptyString, +}).onUndeclaredKey("reject"); + +const DriverRunConfigWire = type({ + commandLeaseMs: NonNegativeSafeInteger, + envPolicy: '"strict"', + eventBatchMaxSize: DriverEventBatchMaxSizeWire, + organizationPath: NonEmptyString, +}).onUndeclaredKey("reject"); const DriverHelloOutputWire = type({ - acceptedCapabilities: DriverCapability.array(), + acceptedCapabilities: DriverCapabilitiesWire, connectionId: NonEmptyString, driverInstanceId: NonEmptyString, - heartbeatIntervalMs: "number >= 250", - runConfig: { - commandLeaseMs: "number >= 0", - envPolicy: '"strict"', - eventBatchMaxSize: "number >= 0", - organizationPath: NonEmptyString, - }, - runId: "string | null", -}); + heartbeatIntervalMs: "number.integer >= 250 & number.safe", + runConfig: DriverRunConfigWire, + runId: type("null").or(NonEmptyString), +}).onUndeclaredKey("reject"); const DriverHeartbeatInputWire = type({ - at: "string", - pid: "number", + at: NonEmptyString, + pid: PositiveSafeInteger, reason: '"interval" | "ping"', -}); +}).onUndeclaredKey("reject"); const DriverHeartbeatOutputWire = type({ - heartbeatCount: "number >= 0", + heartbeatCount: NonNegativeSafeInteger, ok: "true", -}); +}).onUndeclaredKey("reject"); const DriverReadyInputWire = type({ at: NonEmptyString, driverInstanceId: NonEmptyString, - pid: "number", -}); + pid: PositiveSafeInteger, +}).onUndeclaredKey("reject"); + +const RuntimeEventKindWire = type.enumerated(...RUNTIME_EVENT_KINDS); +const DriverRuntimeEventWire = type({ + actor: '"agent" | "api" | "driver" | "system" | "tool" | "user"', + "correlationId?": "string", + delivery: '"best_effort" | "lossless"', + "driverInstanceId?": "string", + id: NonEmptyString, + kind: RuntimeEventKindWire, + "native?": "unknown", + occurredAt: NonEmptyString, + origin: '"api" | "driver" | "file" | "runtime" | "system" | "viewer"', + payload: "unknown", + "receivedAt?": "string", + "runId?": "string", + "runtimeId?": "string", + schemaVersion: `'${RUNTIME_EVENT_SCHEMA_VERSION}'`, + sessionId: NonEmptyString, + "sourceEventId?": "string", + "traceId?": "string", + visibility: '"owner_debug" | "participant" | "public" | "system_internal"', +}).onUndeclaredKey("reject"); const DriverEventEnvelopeWire = type({ - event: "unknown", + event: DriverRuntimeEventWire, eventId: NonEmptyString, "occurredAt?": "string | null | undefined", -}); +}).onUndeclaredKey("reject"); const DriverEventReceiptWire = type({ - "eventId?": "string | undefined", - seq: "number >= 0", - type: NonEmptyString, -}); + eventId: NonEmptyString, + seq: NonNegativeSafeInteger, + type: RuntimeEventKindWire, +}).onUndeclaredKey("reject"); const DriverEventBatchInputWire = type({ driverInstanceId: NonEmptyString, - events: DriverEventEnvelopeWire.array(), -}); + events: DriverEventEnvelopeWire.array().atMostLength(EVENT_BATCH_MAX_SIZE), +}).onUndeclaredKey("reject"); const DriverEventBatchOutputWire = type({ accepted: DriverEventReceiptWire.array(), -}); +}).onUndeclaredKey("reject"); const DriverLogContextWire = type({ "parentSpanId?": "string", "requestId?": "string", "sandboxId?": "string", "sessionId?": "string", - "spanId?": NonEmptyString, - "traceId?": NonEmptyString, -}); + "spanId?": "string", + "traceId?": "string", +}).onUndeclaredKey("reject"); const DriverLogErrorWire = type({ "code?": "string | number", - message: NonEmptyString, - name: NonEmptyString, + message: "string", + name: "string", "stack?": "string | null", -}); +}).onUndeclaredKey("reject"); const DriverLogEntryWire = type({ "context?": DriverLogContextWire, "error?": DriverLogErrorWire, "fields?": PrimitiveRecord, level: '"debug" | "error" | "info" | "trace" | "warn"', - message: NonEmptyString, + message: "string", "namespace?": "string | null", - seq: "number >= 0", + seq: NonNegativeSafeInteger, timestamp: NonEmptyString, -}); +}).onUndeclaredKey("reject"); const DriverLogBatchInputWire = type({ driverInstanceId: NonEmptyString, - logs: DriverLogEntryWire.array(), -}); + logs: DriverLogEntryWire.array().atMostLength(LOG_BATCH_MAX_SIZE), +}).onUndeclaredKey("reject"); const DriverLogBatchOutputWire = type({ ok: "true", -}); +}).onUndeclaredKey("reject"); -const DriverCommandUpdateInputWire = type({ +const OkOutputWire = type({ ok: "true" }).onUndeclaredKey("reject"); + +const DriverCommandAcceptedInputWire = type({ commandId: NonEmptyString, driverInstanceId: NonEmptyString, - "error?": RunError, - "result?": RuntimeCommandResult, - status: RuntimeCommandStatus, -}); + status: '"accepted"', +}).onUndeclaredKey("reject"); + +const DriverCommandCancelledInputWire = type({ + commandId: NonEmptyString, + driverInstanceId: NonEmptyString, + status: '"cancelled"', +}).onUndeclaredKey("reject"); + +const DriverCommandCompletedInputWire = type({ + commandId: NonEmptyString, + driverInstanceId: NonEmptyString, + "result?": InputStartCommandResult.or(McpExecuteCommandResult), + status: '"completed"', +}).onUndeclaredKey("reject"); + +const DriverCommandFailedInputWire = type({ + commandId: NonEmptyString, + driverInstanceId: NonEmptyString, + error: DurableRunError, + status: '"failed"', +}).onUndeclaredKey("reject"); + +const DriverCommandUpdateInputWire = DriverCommandAcceptedInputWire.or( + DriverCommandCancelledInputWire, +) + .or(DriverCommandCompletedInputWire) + .or(DriverCommandFailedInputWire); const DriverExternalToolEffectClaimInputWire = type({ + claimToken: ExternalToolEffectClaimToken, commandId: NonEmptyString, driverInstanceId: NonEmptyString, -}); +}).onUndeclaredKey("reject"); -const DriverExternalToolEffectCompleteInputWire = type({ +const DriverExternalToolEffectObserveInputWire = type({ commandId: NonEmptyString, driverInstanceId: NonEmptyString, - "providerReceiptJson?": "string | null | undefined", - result: McpExecuteCommandResult, -}); +}).onUndeclaredKey("reject"); -const DriverExternalToolEffectUnknownInputWire = type({ +const DriverExternalToolEffectSettleInputWire = type({ + claimToken: ExternalToolEffectClaimToken, commandId: NonEmptyString, driverInstanceId: NonEmptyString, -}); + effectId: NonEmptyString, + settlement: ExternalToolEffectSettlement, +}).onUndeclaredKey("reject"); const DriverNextCommandInputWire = type({ driverInstanceId: NonEmptyString, -}); +}).onUndeclaredKey("reject"); const DriverNextCommandOutputWire = type({ command: type("null").or(RuntimeCommand), -}); +}).onUndeclaredKey("reject"); const DriverCompletionInputWire = type({ driverInstanceId: NonEmptyString, -}); + runId: NonEmptyString, +}).onUndeclaredKey("reject"); const DriverFailureInputWire = type({ driverInstanceId: NonEmptyString, - error: RunError, -}); + error: DurableRunError, + runId: NonEmptyString, +}).onUndeclaredKey("reject"); type DriverEventBatchOutputWireValue = typeof DriverEventBatchOutputWire.infer; type DriverHelloOutputWireValue = typeof DriverHelloOutputWire.infer; @@ -180,65 +258,87 @@ export interface RuntimeOrpcContext { onClaimExternalToolEffect( input: DriverExternalToolEffectClaimInput, ): Promise; - onCompleteExternalToolEffect(input: DriverExternalToolEffectCompleteInput): Promise<{ ok: true }>; onCompleteRun(input: DriverCompletionInput): Promise<{ ok: true }>; onFailRun(input: DriverFailureInput): Promise<{ ok: true }>; onHeartbeat(input: DriverHeartbeatInput): Promise; onHello(input: DriverHelloInput): Promise; onNextCommand(input: DriverNextCommandInput): Promise; - onPushEvents(input: DriverEventBatchInput): Promise; + onObserveExternalToolEffect( + input: DriverExternalToolEffectObserveInput, + ): Promise; + onPushEvents(input: HostDriverEventBatchInput): Promise; onPushLogs(input: DriverLogBatchInput): Promise; - onMarkExternalToolEffectUnknown( - input: DriverExternalToolEffectUnknownInput, - ): Promise<{ ok: true }>; onReady(input: DriverReadyInput): Promise<{ ok: true }>; + onSettleExternalToolEffect( + input: DriverExternalToolEffectSettleInput, + ): Promise; onWatchCommands(): AsyncIteratorObject; } -function parseDriverCommandUpdateInput(input: unknown): DriverCommandUpdateInput { +export function parseDriverCommandUpdateInput(input: unknown): DriverCommandUpdateInput { return parseSchemaValue(DriverCommandUpdateInputWire, input); } -function parseDriverExternalToolEffectClaimInput( +export function parseDriverExternalToolEffectClaimInput( input: unknown, ): DriverExternalToolEffectClaimInput { return parseSchemaValue(DriverExternalToolEffectClaimInputWire, input); } -function parseDriverExternalToolEffectCompleteInput( +export function parseDriverExternalToolEffectObserveInput( input: unknown, -): DriverExternalToolEffectCompleteInput { - return parseSchemaValue(DriverExternalToolEffectCompleteInputWire, input); +): DriverExternalToolEffectObserveInput { + return parseSchemaValue(DriverExternalToolEffectObserveInputWire, input); } -function parseDriverExternalToolEffectUnknownInput( +export function parseDriverExternalToolEffectSettleInput( input: unknown, -): DriverExternalToolEffectUnknownInput { - return parseSchemaValue(DriverExternalToolEffectUnknownInputWire, input); +): DriverExternalToolEffectSettleInput { + return parseSchemaValue(DriverExternalToolEffectSettleInputWire, input); } -function parseDriverCompletionInput(input: unknown): DriverCompletionInput { +export function parseDriverCompletionInput(input: unknown): DriverCompletionInput { return parseSchemaValue(DriverCompletionInputWire, input); } -export function parseDriverEventBatchInput(input: unknown): DriverEventBatchInput { +export function parseDriverEventBatchInput(input: unknown): HostDriverEventBatchInput { const batch = parseSchemaValue(DriverEventBatchInputWire, input); return { driverInstanceId: batch.driverInstanceId, - events: batch.events.map(parseDriverEventEnvelope), + events: batch.events.map((envelope) => ({ + event: parseRuntimeEventEnvelope(envelope.event), + eventId: envelope.eventId, + occurredAt: envelope.occurredAt, + })), }; } -function parseDriverFailureInput(input: unknown): DriverFailureInput { +export function parseDriverFailureInput(input: unknown): DriverFailureInput { return parseSchemaValue(DriverFailureInputWire, input); } -function parseDriverLogBatchInput(input: unknown): DriverLogBatchInput { +export function parseDriverHeartbeatInput(input: unknown): DriverHeartbeatInput { + return parseSchemaValue(DriverHeartbeatInputWire, input); +} + +export function parseDriverHeartbeatOutput(input: unknown): DriverHeartbeatOutput { + return parseSchemaValue(DriverHeartbeatOutputWire, input); +} + +export function parseDriverHelloInput(input: unknown): DriverHelloInput { + return parseSchemaValue(DriverHelloInputWire, input); +} + +export function parseDriverLogBatchInput(input: unknown): DriverLogBatchInput { return parseSchemaValue(DriverLogBatchInputWire, input); } -function parseDriverNextCommandInput(input: unknown): DriverNextCommandInput { +export function parseDriverLogBatchOutput(input: unknown): DriverLogBatchOutput { + return parseSchemaValue(DriverLogBatchOutputWire, input); +} + +export function parseDriverNextCommandInput(input: unknown): DriverNextCommandInput { return parseSchemaValue(DriverNextCommandInputWire, input); } @@ -246,20 +346,22 @@ export function parseDriverReadyInput(input: unknown): DriverReadyInput { return parseSchemaValue(DriverReadyInputWire, input); } -function toDriverEventBatchOutputWire( - output: DriverEventBatchOutput, -): DriverEventBatchOutputWireValue { - return parseSchemaValue(DriverEventBatchOutputWire, output); +export function parseDriverEventBatchOutput(input: unknown): DriverEventBatchOutputWireValue { + return parseSchemaValue(DriverEventBatchOutputWire, input); } -function toDriverHelloOutputWire(output: DriverHelloOutput): DriverHelloOutputWireValue { - return parseSchemaValue(DriverHelloOutputWire, output); +export function parseDriverHelloOutput(input: unknown): DriverHelloOutputWireValue { + return parseSchemaValue(DriverHelloOutputWire, input); +} + +export function parseDriverNextCommandOutput(input: unknown): DriverNextCommandOutputWireValue { + return parseSchemaValue(DriverNextCommandOutputWire, input); } function toDriverNextCommandOutputWire( output: DriverNextCommandOutput, ): DriverNextCommandOutputWireValue { - return parseSchemaValue(DriverNextCommandOutputWire, output); + return parseDriverNextCommandOutput(output); } const base = os.$context(); @@ -270,43 +372,55 @@ export const runtimeOrpcRouter = { .input(DriverExternalToolEffectClaimInputWire) .output(ExternalToolEffectClaim) .handler(async ({ context, input }) => - context.onClaimExternalToolEffect(parseDriverExternalToolEffectClaimInput(input)), + parseSchemaValue( + ExternalToolEffectClaim, + await context.onClaimExternalToolEffect(parseDriverExternalToolEffectClaimInput(input)), + ), ), commandUpdate: base .input(DriverCommandUpdateInputWire) - .output(type({ ok: "true" })) + .output(OkOutputWire) .handler(async ({ context, input }) => context.onCommandUpdate(parseDriverCommandUpdateInput(input)), ), - completeExternalToolEffect: base - .input(DriverExternalToolEffectCompleteInputWire) - .output(type({ ok: "true" })) - .handler(async ({ context, input }) => - context.onCompleteExternalToolEffect(parseDriverExternalToolEffectCompleteInput(input)), - ), completeRun: base .input(DriverCompletionInputWire) - .output(type({ ok: "true" })) + .output(OkOutputWire) .handler(async ({ context, input }) => context.onCompleteRun(parseDriverCompletionInput(input)), ), failRun: base .input(DriverFailureInputWire) - .output(type({ ok: "true" })) + .output(OkOutputWire) .handler(async ({ context, input }) => context.onFailRun(parseDriverFailureInput(input))), heartbeat: base .input(DriverHeartbeatInputWire) .output(DriverHeartbeatOutputWire) - .handler(async ({ context, input }) => context.onHeartbeat(input)), + .handler(async ({ context, input }) => + parseDriverHeartbeatOutput(await context.onHeartbeat(parseDriverHeartbeatInput(input))), + ), hello: base .input(DriverHelloInputWire) .output(DriverHelloOutputWire) - .handler(async ({ context, input }) => toDriverHelloOutputWire(await context.onHello(input))), + .handler(async ({ context, input }) => + parseDriverHelloOutput(await context.onHello(parseDriverHelloInput(input))), + ), + observeExternalToolEffect: base + .input(DriverExternalToolEffectObserveInputWire) + .output(ExternalToolEffectState) + .handler(async ({ context, input }) => + parseSchemaValue( + ExternalToolEffectState, + await context.onObserveExternalToolEffect( + parseDriverExternalToolEffectObserveInput(input), + ), + ), + ), pushEvents: base .input(DriverEventBatchInputWire) .output(DriverEventBatchOutputWire) .handler(async ({ context, input }) => - toDriverEventBatchOutputWire(await context.onPushEvents(parseDriverEventBatchInput(input))), + parseDriverEventBatchOutput(await context.onPushEvents(parseDriverEventBatchInput(input))), ), pushLogs: base .input(DriverLogBatchInputWire) @@ -314,13 +428,16 @@ export const runtimeOrpcRouter = { .handler(async ({ context, input }) => context.onPushLogs(parseDriverLogBatchInput(input))), ready: base .input(DriverReadyInputWire) - .output(type({ ok: "true" })) + .output(OkOutputWire) .handler(async ({ context, input }) => context.onReady(parseDriverReadyInput(input))), - markExternalToolEffectUnknown: base - .input(DriverExternalToolEffectUnknownInputWire) - .output(type({ ok: "true" })) + settleExternalToolEffect: base + .input(DriverExternalToolEffectSettleInputWire) + .output(ExternalToolEffectState) .handler(async ({ context, input }) => - context.onMarkExternalToolEffectUnknown(parseDriverExternalToolEffectUnknownInput(input)), + parseSchemaValue( + ExternalToolEffectState, + await context.onSettleExternalToolEffect(parseDriverExternalToolEffectSettleInput(input)), + ), ), }, driverInstance: { diff --git a/apps/api/src/modules/runtime/infrastructure/driver-instance/rpc.ts b/apps/api/src/modules/runtime/infrastructure/driver-instance/rpc.ts index 268ec76b..f17ffb62 100644 --- a/apps/api/src/modules/runtime/infrastructure/driver-instance/rpc.ts +++ b/apps/api/src/modules/runtime/infrastructure/driver-instance/rpc.ts @@ -1,12 +1,12 @@ import type { DriverCommandUpdateInput, DriverCompletionInput, - DriverEventBatchInput, DriverEventBatchOutput, DriverExternalToolEffectClaimInput, DriverExternalToolEffectClaimOutput, - DriverExternalToolEffectCompleteInput, - DriverExternalToolEffectUnknownInput, + DriverExternalToolEffectObserveInput, + DriverExternalToolEffectSettleInput, + DriverExternalToolEffectState, DriverFailureInput, DriverHeartbeatInput, DriverHelloInput, @@ -19,12 +19,15 @@ import type { } from "@mosoo/agent-driver/orpc"; import type { RuntimeCommand } from "@mosoo/contracts/runtime-command"; +import type { HostDriverEventBatchInput } from "./event-types"; import type { RuntimeOrpcContext } from "./rpc-wire"; +import type { DriverInstanceConnectionEpoch } from "./state"; export type DriverInstanceRpcContext = RuntimeOrpcContext; export interface DriverInstanceRpcOperationContext { readonly connectionId: string; + readonly epoch: DriverInstanceConnectionEpoch; assertActiveConnection(): void; } @@ -37,10 +40,10 @@ export interface DriverInstanceRpcHandler { input: DriverExternalToolEffectClaimInput, context: DriverInstanceRpcOperationContext, ): Promise; - handleCompleteExternalToolEffect( - input: DriverExternalToolEffectCompleteInput, + handleObserveExternalToolEffect( + input: DriverExternalToolEffectObserveInput, context: DriverInstanceRpcOperationContext, - ): Promise<{ ok: true }>; + ): Promise; handleCompleteRun( input: DriverCompletionInput, context: DriverInstanceRpcOperationContext, @@ -62,17 +65,17 @@ export interface DriverInstanceRpcHandler { context: DriverInstanceRpcOperationContext, ): Promise; handlePushEvents( - input: DriverEventBatchInput, + input: HostDriverEventBatchInput, context: DriverInstanceRpcOperationContext, ): Promise; handlePushLogs( input: DriverLogBatchInput, context: DriverInstanceRpcOperationContext, ): Promise; - handleMarkExternalToolEffectUnknown( - input: DriverExternalToolEffectUnknownInput, + handleSettleExternalToolEffect( + input: DriverExternalToolEffectSettleInput, context: DriverInstanceRpcOperationContext, - ): Promise<{ ok: true }>; + ): Promise; handleReady( input: DriverReadyInput, context: DriverInstanceRpcOperationContext, @@ -88,18 +91,18 @@ export function createDriverInstanceRpcContext( onClaimExternalToolEffect: async (input) => handler.handleClaimExternalToolEffect(input, context), onCommandUpdate: async (input) => handler.handleCommandUpdate(input, context), - onCompleteExternalToolEffect: async (input) => - handler.handleCompleteExternalToolEffect(input, context), onCompleteRun: async (input) => handler.handleCompleteRun(input, context), onFailRun: async (input) => handler.handleFailRun(input, context), onHeartbeat: async (input) => handler.handleHeartbeat(input, context), onHello: async (input) => handler.handleHello(input, context), onNextCommand: async (input) => handler.handleNextCommand(input, context), + onObserveExternalToolEffect: async (input) => + handler.handleObserveExternalToolEffect(input, context), onPushEvents: async (input) => handler.handlePushEvents(input, context), onPushLogs: async (input) => handler.handlePushLogs(input, context), - onMarkExternalToolEffectUnknown: async (input) => - handler.handleMarkExternalToolEffectUnknown(input, context), onReady: async (input) => handler.handleReady(input, context), + onSettleExternalToolEffect: async (input) => + handler.handleSettleExternalToolEffect(input, context), onWatchCommands: () => handler.watchCommands(context)[Symbol.asyncIterator]() as ReturnType< RuntimeOrpcContext["onWatchCommands"] diff --git a/apps/api/src/modules/runtime/infrastructure/driver-instance/run-transitions.ts b/apps/api/src/modules/runtime/infrastructure/driver-instance/run-transitions.ts deleted file mode 100644 index 6c204579..00000000 --- a/apps/api/src/modules/runtime/infrastructure/driver-instance/run-transitions.ts +++ /dev/null @@ -1,12 +0,0 @@ -import type { RuntimeDriverRunTransition } from "./event-types"; - -export function hasTerminalRuntimeDriverRunTransition( - transitions: readonly RuntimeDriverRunTransition[], -): boolean { - return transitions.some( - (transition) => - transition.status === "cancelled" || - transition.status === "completed" || - transition.status === "failed", - ); -} diff --git a/apps/api/src/modules/runtime/infrastructure/driver-instance/runtime-artifact-attempt.repository.ts b/apps/api/src/modules/runtime/infrastructure/driver-instance/runtime-artifact-attempt.repository.ts new file mode 100644 index 00000000..34ef7b96 --- /dev/null +++ b/apps/api/src/modules/runtime/infrastructure/driver-instance/runtime-artifact-attempt.repository.ts @@ -0,0 +1,896 @@ +import { fileRecordsTable, runtimeArtifactAttemptsTable } from "@mosoo/db"; +import type { AccountId, FileId, RuntimeEventId, SessionId, SessionRunId } from "@mosoo/id"; +import { stringifyRuntimeEventSemanticValue } from "@mosoo/runtime-events"; +import { and, eq, inArray, isNull, lte, or, sql } from "drizzle-orm"; + +import type { ApiBindings } from "../../../../platform/cloudflare/worker-types"; +import { getAppDatabase } from "../../../../platform/db/drizzle"; +import { currentTimestampMs } from "../../../../time"; +import { + createRuntimeOutputContentSha256, + deleteRuntimeArtifactObject, +} from "../../../files/application/file-store"; +import type { DriverRuntimeEventFence } from "../../../sessions/infrastructure/session-runtime-event-store.types"; +import { + normalizeRuntimeSessionOutputRelativePath, + toRuntimeSessionOutputArtifactPath, +} from "./runtime-session-outputs"; + +const RUNTIME_ARTIFACT_STAGE_TTL_MS = 24 * 60 * 60_000; +const RUNTIME_ARTIFACT_DELETE_QUARANTINE_MS = 7 * 24 * 60 * 60_000; +const RUNTIME_ARTIFACT_DELETE_LEASE_MS = 5 * 60_000; +const RUNTIME_ARTIFACT_CLEANUP_BATCH_SIZE = 25; + +export interface RuntimeArtifactCapturePlanFile { + readonly contentType: string | null; + readonly expectedSize: number; + readonly fileId: FileId; + readonly name: string; + readonly objectKey: string; + readonly operation: "upsert"; + readonly readPath: string; + readonly sourcePath: string; +} + +export type RuntimeArtifactCaptureStatus = + | "complete" + | "omitted_file_limit" + | "omitted_runtime_unavailable" + | "omitted_size_limit" + | "omitted_source_changed" + | "omitted_source_missing"; + +export interface RuntimeArtifactCapturePlan { + readonly captureStatus: RuntimeArtifactCaptureStatus; + readonly files: readonly ( + | RuntimeArtifactCapturePlanFile + | { readonly operation: "delete"; readonly sourcePath: string } + )[]; + readonly mode: "delta" | "snapshot"; + readonly version: 1; +} + +export interface RuntimeArtifactUpsertManifestFile { + readonly contentSha256: string; + readonly contentType: string | null; + readonly disposition: "create" | "reuse"; + readonly etag: string; + readonly fileId: FileId; + readonly name: string; + readonly objectKey: string; + readonly operation: "upsert"; + readonly parentPath: string; + readonly path: string; + readonly size: number; + readonly sourcePath: string; +} + +export interface RuntimeArtifactDeleteManifestFile { + readonly operation: "delete"; + readonly sourcePath: string; +} + +export type RuntimeArtifactManifestFile = + | RuntimeArtifactDeleteManifestFile + | RuntimeArtifactUpsertManifestFile; + +export interface RuntimeArtifactManifest { + readonly captureStatus: RuntimeArtifactCaptureStatus; + readonly files: readonly RuntimeArtifactManifestFile[]; + readonly mode: "delta" | "snapshot"; + readonly semanticHash: string; + readonly sourceEventId: string; + readonly version: 1; +} + +export interface StagedRuntimeArtifactProjection { + readonly attemptId: string; + readonly manifestJson: string; + readonly manifestSha256: string; +} + +export interface ReadyRuntimeArtifactRecord { + readonly contentType: string | null; + readonly etag: string; + readonly fileId: FileId; + readonly name: string; + readonly objectKey: string; + readonly parentPath: string; + readonly path: string; + readonly size: number; +} + +interface RuntimeArtifactAttemptIdentity { + readonly createdByAccountId: AccountId; + readonly driverFence: DriverRuntimeEventFence; + readonly eventType: string; + readonly runId: SessionRunId; + readonly semanticHash: string; + readonly sessionId: SessionId; + readonly sourceEventId: string; +} + +function isRecord(value: unknown): value is Record { + return typeof value === "object" && value !== null && !Array.isArray(value); +} + +function readRequiredString(record: Record, key: string): string { + const value = record[key]; + if (typeof value !== "string" || value.length === 0) { + throw new Error(`Runtime artifact manifest ${key} must be a non-empty string.`); + } + return value; +} + +function parseManifestFile(value: unknown): RuntimeArtifactManifestFile { + if (!isRecord(value)) { + throw new Error("Runtime artifact manifest file must be an object."); + } + const operation = value["operation"]; + const sourcePath = readRequiredString(value, "sourcePath"); + if (operation === "delete") { + return { operation, sourcePath }; + } + const contentType = value["contentType"]; + const disposition = value["disposition"]; + const size = value["size"]; + if ( + operation !== "upsert" || + (contentType !== null && typeof contentType !== "string") || + (disposition !== "create" && disposition !== "reuse") || + !Number.isSafeInteger(size) || + (size as number) < 0 + ) { + throw new Error("Runtime artifact manifest file has invalid storage metadata."); + } + const contentSha256 = readRequiredString(value, "contentSha256"); + if (!/^[0-9a-f]{64}$/.test(contentSha256)) { + throw new Error("Runtime artifact manifest content hash is invalid."); + } + return { + contentSha256, + contentType, + disposition, + etag: readRequiredString(value, "etag"), + fileId: readRequiredString(value, "fileId") as FileId, + name: readRequiredString(value, "name"), + objectKey: readRequiredString(value, "objectKey"), + operation, + parentPath: readRequiredString(value, "parentPath"), + path: readRequiredString(value, "path"), + size: size as number, + sourcePath, + }; +} + +export function parseRuntimeArtifactManifest(value: string): RuntimeArtifactManifest { + const parsed: unknown = JSON.parse(value); + if ( + !isRecord(parsed) || + parsed["version"] !== 1 || + (parsed["captureStatus"] !== "complete" && + parsed["captureStatus"] !== "omitted_file_limit" && + parsed["captureStatus"] !== "omitted_runtime_unavailable" && + parsed["captureStatus"] !== "omitted_size_limit" && + parsed["captureStatus"] !== "omitted_source_changed" && + parsed["captureStatus"] !== "omitted_source_missing") || + (parsed["mode"] !== "delta" && parsed["mode"] !== "snapshot") || + !Array.isArray(parsed["files"]) + ) { + throw new Error("Runtime artifact manifest is invalid."); + } + const sourceEventId = readRequiredString(parsed, "sourceEventId"); + const semanticHash = readRequiredString(parsed, "semanticHash"); + if (!/^[0-9a-f]{64}$/.test(semanticHash)) { + throw new Error("Runtime artifact manifest semantic hash is invalid."); + } + const files = parsed["files"].map(parseManifestFile); + if (parsed["captureStatus"] !== "complete" && files.length !== 0) { + throw new Error("An omitted runtime artifact capture must be empty."); + } + const identities = new Set(); + for (const file of files) { + for (const identity of [ + file.sourcePath, + ...(file.operation === "upsert" ? [file.fileId, file.path, file.objectKey] : []), + ]) { + if (identities.has(identity)) { + throw new Error("Runtime artifact manifest contains a duplicate file identity."); + } + identities.add(identity); + } + } + return { + captureStatus: parsed["captureStatus"], + files, + mode: parsed["mode"], + semanticHash, + sourceEventId, + version: 1, + }; +} + +export async function createRuntimeArtifactManifest( + input: Omit, +): Promise<{ manifestJson: string; manifestSha256: string }> { + const manifest: RuntimeArtifactManifest = { + ...input, + files: input.files.toSorted((left, right) => + left.sourcePath === right.sourcePath + ? (left.operation === "upsert" ? left.fileId : "").localeCompare( + right.operation === "upsert" ? right.fileId : "", + ) + : left.sourcePath.localeCompare(right.sourcePath), + ), + version: 1, + }; + const manifestJson = stringifyRuntimeEventSemanticValue(manifest); + return { + manifestJson, + manifestSha256: await createRuntimeOutputContentSha256(new TextEncoder().encode(manifestJson)), + }; +} + +export async function createRuntimeArtifactAttempt( + database: D1Database, + input: RuntimeArtifactAttemptIdentity & { + readonly attemptId: string; + readonly timestampMs?: number; + }, +): Promise { + if (input.driverFence.sessionRunId !== input.runId) { + throw new Error("Runtime artifact attempt does not match its fenced Session Run."); + } + const timestampMs = input.timestampMs ?? currentTimestampMs(); + const result = await database + .prepare( + `INSERT INTO runtime_artifact_attempt ( + accepted_event_id, created_at, created_by_account_id, + delete_after, driver_connection_id, driver_generation, driver_instance_id, + event_type, expires_at, id, manifest_json, manifest_sha256, + owned_object_keys_json, run_id, semantic_hash, session_id, source_event_id, + status, updated_at + ) + SELECT NULL, ?, ?, NULL, ?, ?, ?, ?, ?, ?, NULL, NULL, '[]', ?, ?, ?, ?, + 'staging', ? + FROM session AS session + INNER JOIN session_run AS run + ON run.id = ? AND run.session_id = session.id + INNER JOIN driver_instance AS driver + ON driver.id = run.driver_instance_id + WHERE session.id = ? + AND session.archived_at IS NULL + AND session.cleanup_operation_kind IS NULL + AND session.last_run_id = run.id + AND session.status = 'RUNNING' + AND session.status_operation_id IS NULL + AND run.status IN ('queued', 'booting', 'running', 'waiting_input') + AND driver.id = ? + AND driver.generation = ? + AND driver.connection_id = ? + AND driver.sandbox_session_id = session.id + RETURNING id`, + ) + .bind( + timestampMs, + input.createdByAccountId, + input.driverFence.connectionId, + input.driverFence.generation, + input.driverFence.driverInstanceId, + input.eventType, + timestampMs + RUNTIME_ARTIFACT_STAGE_TTL_MS, + input.attemptId, + input.runId, + input.semanticHash, + input.sessionId, + input.sourceEventId, + timestampMs, + input.runId, + input.sessionId, + input.driverFence.driverInstanceId, + input.driverFence.generation, + input.driverFence.connectionId, + ) + .first<{ id: string }>(); + + if (result?.id !== input.attemptId) { + throw new Error("Runtime artifact attempt lost its active Driver fence."); + } +} + +function exactActiveAttemptFenceSql(): string { + return `EXISTS ( + SELECT 1 + FROM session AS session + INNER JOIN session_run AS run + ON run.id = runtime_artifact_attempt.run_id + AND run.session_id = session.id + INNER JOIN driver_instance AS driver + ON driver.id = run.driver_instance_id + WHERE session.id = runtime_artifact_attempt.session_id + AND session.archived_at IS NULL + AND session.cleanup_operation_kind IS NULL + AND session.last_run_id = run.id + AND session.status = 'RUNNING' + AND session.status_operation_id IS NULL + AND run.status IN ('queued', 'booting', 'running', 'waiting_input') + AND driver.id = runtime_artifact_attempt.driver_instance_id + AND driver.generation = runtime_artifact_attempt.driver_generation + AND driver.connection_id = runtime_artifact_attempt.driver_connection_id + AND driver.sandbox_session_id = session.id + )`; +} + +export async function getReadyRuntimeArtifact( + database: D1Database, + input: { readonly parentPath: string; readonly sessionId: SessionId }, +): Promise { + const row = await getAppDatabase(database) + .select({ + contentType: fileRecordsTable.mimeType, + etag: fileRecordsTable.etag, + fileId: fileRecordsTable.id, + name: fileRecordsTable.name, + objectKey: fileRecordsTable.objectKey, + parentPath: fileRecordsTable.parentPath, + path: fileRecordsTable.path, + size: fileRecordsTable.size, + }) + .from(fileRecordsTable) + .where( + and( + eq(fileRecordsTable.scopeKind, "session"), + eq(fileRecordsTable.scopeId, input.sessionId), + eq(fileRecordsTable.sessionKind, "artifact"), + eq(fileRecordsTable.status, "ready"), + eq(fileRecordsTable.parentPath, input.parentPath), + ), + ) + .get(); + if (row === undefined) { + return null; + } + if (row.etag === null) { + throw new Error("Ready runtime artifact is missing its object etag."); + } + return { ...row, etag: row.etag }; +} + +export async function claimRuntimeArtifactObjectKey( + database: D1Database, + input: { readonly attemptId: string; readonly objectKey: string; readonly timestampMs?: number }, +): Promise { + if (!input.objectKey.startsWith(`runtime-artifact-attempts/v1/${input.attemptId}/files/`)) { + throw new Error("Runtime artifact object key is outside its attempt namespace."); + } + const timestampMs = input.timestampMs ?? currentTimestampMs(); + const result = await database + .prepare( + `UPDATE runtime_artifact_attempt + SET owned_object_keys_json = json_insert(owned_object_keys_json, '$[#]', ?), + updated_at = ? + WHERE id = ? + AND status = 'staging' + AND expires_at > ? + AND NOT EXISTS ( + SELECT 1 FROM json_each(owned_object_keys_json) WHERE value = ? + ) + AND ${exactActiveAttemptFenceSql()} + RETURNING id`, + ) + .bind(input.objectKey, timestampMs, input.attemptId, timestampMs, input.objectKey) + .first<{ id: string }>(); + if (result?.id !== input.attemptId) { + throw new Error("Runtime artifact object lost its staging ownership fence."); + } +} + +export async function sealRuntimeArtifactAttempt( + database: D1Database, + input: StagedRuntimeArtifactProjection & { readonly timestampMs?: number }, +): Promise { + const timestampMs = input.timestampMs ?? currentTimestampMs(); + const manifest = parseRuntimeArtifactManifest(input.manifestJson); + for (const file of manifest.files) { + const outputPrefix = "outputs/"; + const relativePath = file.sourcePath.startsWith(outputPrefix) + ? normalizeRuntimeSessionOutputRelativePath(file.sourcePath.slice(outputPrefix.length)) + : null; + if ( + relativePath === null || + file.sourcePath !== toRuntimeSessionOutputArtifactPath(relativePath) + ) { + throw new Error("Runtime artifact manifest source path is not canonical."); + } + if ( + file.operation === "upsert" && + file.disposition === "create" && + !file.objectKey.startsWith(`runtime-artifact-attempts/v1/${input.attemptId}/files/`) + ) { + throw new Error("Runtime artifact manifest object is outside its attempt namespace."); + } + } + const expectedManifestSha256 = await createRuntimeOutputContentSha256( + new TextEncoder().encode(input.manifestJson), + ); + if (input.manifestSha256 !== expectedManifestSha256) { + throw new Error("Runtime artifact manifest hash does not match its canonical bytes."); + } + const result = await database + .prepare( + `UPDATE runtime_artifact_attempt + SET manifest_json = ?, manifest_sha256 = ?, status = 'staged', updated_at = ? + WHERE id = ? + AND status = 'staging' + AND expires_at > ? + AND source_event_id = ? + AND semantic_hash = ? + AND ( + (event_type = 'run.completed' + AND json_extract(?, '$.mode') = 'snapshot' + AND NOT EXISTS ( + SELECT 1 FROM json_each(?, '$.files') AS file + WHERE json_extract(file.value, '$.operation') = 'delete' + )) + OR (event_type IN ('file.change.updated', 'file.changed') + AND json_extract(?, '$.mode') = 'delta') + ) + AND ${exactActiveAttemptFenceSql()} + RETURNING id`, + ) + .bind( + input.manifestJson, + input.manifestSha256, + timestampMs, + input.attemptId, + timestampMs, + manifest.sourceEventId, + manifest.semanticHash, + input.manifestJson, + input.manifestJson, + input.manifestJson, + ) + .first<{ id: string }>(); + if (result?.id !== input.attemptId) { + throw new Error("Runtime artifact manifest lost its staging ownership fence."); + } +} + +export function prepareRuntimeArtifactPromotion( + database: D1Database, + input: StagedRuntimeArtifactProjection & { + readonly eventId: RuntimeEventId; + readonly timestampMs: number; + }, +): D1PreparedStatement[] { + return [ + database + .prepare( + `INSERT INTO file_record ( + committed, created_at, created_by_account_id, etag, expires_at, id, + mime_type, name, object_key, owner_id, owner_kind, parent_path, path, + purpose, runtime_event_seq, scope_id, scope_kind, session_kind, size, + status, updated_at, version + ) + SELECT 1, event.created_at, attempt.created_by_account_id, + json_extract(file.value, '$.etag'), NULL, + json_extract(file.value, '$.fileId'), + json_extract(file.value, '$.contentType'), + json_extract(file.value, '$.name'), + json_extract(file.value, '$.objectKey'), + event.session_id, 'session', + json_extract(file.value, '$.parentPath'), + json_extract(file.value, '$.path'), + 'session_artifact', event.seq, event.session_id, 'session', 'artifact', + json_extract(file.value, '$.size'), 'ready', event.created_at, 1 + FROM runtime_artifact_attempt AS attempt + INNER JOIN session_event AS event + ON event.id = ? + AND event.session_id = attempt.session_id + AND event.run_id = attempt.run_id + AND event.source_event_id = attempt.source_event_id + AND event.semantic_hash = attempt.semantic_hash + AND event.artifact_attempt_id = attempt.id + AND event.artifact_manifest_sha256 = attempt.manifest_sha256 + INNER JOIN json_each(attempt.manifest_json, '$.files') AS file + ON json_extract(file.value, '$.operation') = 'upsert' + AND json_extract(file.value, '$.disposition') = 'create' + WHERE attempt.id = ? + AND attempt.status = 'staged' + AND attempt.expires_at > ? + AND attempt.manifest_sha256 = ?`, + ) + .bind(input.eventId, input.attemptId, input.timestampMs, input.manifestSha256), + database + .prepare( + `UPDATE file_record + SET runtime_event_seq = MAX(COALESCE(runtime_event_seq, 0), ( + SELECT event.seq FROM session_event AS event WHERE event.id = ? + )), + updated_at = MAX(updated_at, ( + SELECT event.created_at FROM session_event AS event WHERE event.id = ? + )) + WHERE EXISTS ( + SELECT 1 + FROM runtime_artifact_attempt AS attempt + INNER JOIN session_event AS event + ON event.id = ? + AND event.session_id = attempt.session_id + AND event.run_id = attempt.run_id + AND event.source_event_id = attempt.source_event_id + AND event.semantic_hash = attempt.semantic_hash + AND event.artifact_attempt_id = attempt.id + AND event.artifact_manifest_sha256 = attempt.manifest_sha256 + INNER JOIN json_each(attempt.manifest_json, '$.files') AS file + ON json_extract(file.value, '$.operation') = 'upsert' + AND json_extract(file.value, '$.disposition') = 'reuse' + WHERE attempt.id = ? + AND attempt.status = 'staged' + AND attempt.expires_at > ? + AND attempt.manifest_sha256 = ? + AND file_record.id = json_extract(file.value, '$.fileId') + AND file_record.scope_kind = 'session' + AND file_record.scope_id = event.session_id + AND file_record.session_kind = 'artifact' + AND file_record.status = 'ready' + AND file_record.parent_path = json_extract(file.value, '$.parentPath') + AND file_record.path = json_extract(file.value, '$.path') + AND file_record.name = json_extract(file.value, '$.name') + AND file_record.object_key = json_extract(file.value, '$.objectKey') + AND file_record.etag = json_extract(file.value, '$.etag') + AND file_record.size = json_extract(file.value, '$.size') + AND file_record.mime_type IS json_extract(file.value, '$.contentType') + )`, + ) + .bind( + input.eventId, + input.eventId, + input.eventId, + input.attemptId, + input.timestampMs, + input.manifestSha256, + ), + database + .prepare( + `UPDATE session_artifact_head + SET file_id = NULL, + runtime_event_seq = (SELECT seq FROM session_event WHERE id = ?), + source_event_id = (SELECT source_event_id FROM session_event WHERE id = ?), + updated_at = (SELECT created_at FROM session_event WHERE id = ?) + WHERE session_id = (SELECT session_id FROM session_event WHERE id = ?) + AND runtime_event_seq < (SELECT seq FROM session_event WHERE id = ?) + AND EXISTS ( + SELECT 1 + FROM runtime_artifact_attempt AS attempt + INNER JOIN session_event AS event + ON event.id = ? + AND event.artifact_attempt_id = attempt.id + AND event.artifact_manifest_sha256 = attempt.manifest_sha256 + WHERE attempt.id = ? + AND attempt.status = 'staged' + AND attempt.expires_at > ? + AND attempt.manifest_sha256 = ? + AND json_extract(attempt.manifest_json, '$.mode') = 'snapshot' + AND json_extract(attempt.manifest_json, '$.captureStatus') = 'complete' + )`, + ) + .bind( + input.eventId, + input.eventId, + input.eventId, + input.eventId, + input.eventId, + input.eventId, + input.attemptId, + input.timestampMs, + input.manifestSha256, + ), + database + .prepare( + `INSERT INTO session_artifact_head ( + file_id, runtime_event_seq, session_id, source_event_id, source_path, updated_at + ) + SELECT CASE + WHEN json_extract(file.value, '$.operation') = 'upsert' + THEN json_extract(file.value, '$.fileId') + ELSE NULL + END, + event.seq, event.session_id, event.source_event_id, + json_extract(file.value, '$.sourcePath'), event.created_at + FROM runtime_artifact_attempt AS attempt + INNER JOIN session_event AS event + ON event.id = ? + AND event.session_id = attempt.session_id + AND event.run_id = attempt.run_id + AND event.source_event_id = attempt.source_event_id + AND event.semantic_hash = attempt.semantic_hash + AND event.artifact_attempt_id = attempt.id + AND event.artifact_manifest_sha256 = attempt.manifest_sha256 + INNER JOIN json_each(attempt.manifest_json, '$.files') AS file ON 1 = 1 + WHERE attempt.id = ? + AND attempt.status = 'staged' + AND attempt.expires_at > ? + AND attempt.manifest_sha256 = ? + ON CONFLICT(session_id, source_path) DO UPDATE SET + file_id = excluded.file_id, + runtime_event_seq = excluded.runtime_event_seq, + source_event_id = excluded.source_event_id, + updated_at = excluded.updated_at + WHERE excluded.runtime_event_seq > session_artifact_head.runtime_event_seq + OR ( + excluded.runtime_event_seq = session_artifact_head.runtime_event_seq + AND excluded.source_event_id = session_artifact_head.source_event_id + )`, + ) + .bind(input.eventId, input.attemptId, input.timestampMs, input.manifestSha256), + database + .prepare( + `UPDATE runtime_artifact_attempt AS attempt + SET accepted_event_id = ?, expires_at = NULL, owned_object_keys_json = '[]', + status = 'accepted', updated_at = ? + WHERE attempt.id = ? + AND attempt.status = 'staged' + AND attempt.expires_at > ? + AND attempt.manifest_json = ? + AND attempt.manifest_sha256 = ? + AND NOT EXISTS ( + SELECT 1 + FROM json_each(attempt.owned_object_keys_json) AS owned + WHERE NOT EXISTS ( + SELECT 1 + FROM json_each(attempt.manifest_json, '$.files') AS file + WHERE json_extract(file.value, '$.operation') = 'upsert' + AND json_extract(file.value, '$.disposition') = 'create' + AND json_extract(file.value, '$.objectKey') = owned.value + ) + ) + AND NOT EXISTS ( + SELECT 1 + FROM json_each(attempt.manifest_json, '$.files') AS file + WHERE json_extract(file.value, '$.operation') = 'upsert' + AND json_extract(file.value, '$.disposition') = 'create' + AND NOT EXISTS ( + SELECT 1 + FROM json_each(attempt.owned_object_keys_json) AS owned + WHERE owned.value = json_extract(file.value, '$.objectKey') + ) + ) + AND ( + SELECT COUNT(DISTINCT json_extract(file.value, '$.objectKey')) + FROM json_each(attempt.manifest_json, '$.files') AS file + WHERE json_extract(file.value, '$.operation') = 'upsert' + AND json_extract(file.value, '$.disposition') = 'create' + ) = json_array_length(attempt.owned_object_keys_json) + AND EXISTS ( + SELECT 1 FROM session_event AS event + WHERE event.id = ? + AND event.session_id = attempt.session_id + AND event.run_id = attempt.run_id + AND event.source_event_id = attempt.source_event_id + AND event.semantic_hash = attempt.semantic_hash + AND event.artifact_attempt_id = attempt.id + AND event.artifact_manifest_json = attempt.manifest_json + AND event.artifact_manifest_sha256 = attempt.manifest_sha256 + ) + AND NOT EXISTS ( + SELECT 1 + FROM json_each(attempt.manifest_json, '$.files') AS file + INNER JOIN session_event AS event ON event.id = ? + WHERE NOT EXISTS ( + SELECT 1 + FROM session_artifact_head AS head + WHERE head.session_id = attempt.session_id + AND head.source_path = json_extract(file.value, '$.sourcePath') + AND head.runtime_event_seq = event.seq + AND head.source_event_id = event.source_event_id + AND head.file_id IS CASE + WHEN json_extract(file.value, '$.operation') = 'upsert' + THEN json_extract(file.value, '$.fileId') + ELSE NULL + END + ) + ) + AND NOT EXISTS ( + SELECT 1 + FROM session_artifact_head AS head + INNER JOIN session_event AS event ON event.id = ? + WHERE json_extract(attempt.manifest_json, '$.mode') = 'snapshot' + AND json_extract(attempt.manifest_json, '$.captureStatus') = 'complete' + AND head.session_id = attempt.session_id + AND head.runtime_event_seq < event.seq + ) + AND NOT EXISTS ( + SELECT 1 + FROM json_each(attempt.manifest_json, '$.files') AS file + WHERE json_extract(file.value, '$.operation') = 'upsert' + AND NOT EXISTS ( + SELECT 1 FROM file_record AS stored + INNER JOIN session_event AS event ON event.id = ? + WHERE stored.id = json_extract(file.value, '$.fileId') + AND stored.scope_kind = 'session' + AND stored.scope_id = attempt.session_id + AND stored.session_kind = 'artifact' + AND stored.status = 'ready' + AND stored.parent_path = json_extract(file.value, '$.parentPath') + AND stored.path = json_extract(file.value, '$.path') + AND stored.name = json_extract(file.value, '$.name') + AND stored.object_key = json_extract(file.value, '$.objectKey') + AND stored.etag = json_extract(file.value, '$.etag') + AND stored.size = json_extract(file.value, '$.size') + AND stored.mime_type IS json_extract(file.value, '$.contentType') + AND stored.runtime_event_seq >= event.seq + ) + )`, + ) + .bind( + input.eventId, + input.timestampMs, + input.attemptId, + input.timestampMs, + input.manifestJson, + input.manifestSha256, + input.eventId, + input.eventId, + input.eventId, + input.eventId, + ), + database + .prepare( + `INSERT INTO session_event (id) + SELECT ? + WHERE NOT EXISTS ( + SELECT 1 + FROM runtime_artifact_attempt AS attempt + INNER JOIN session_event AS event ON event.id = attempt.accepted_event_id + WHERE attempt.id = ? + AND attempt.status = 'accepted' + AND attempt.accepted_event_id = ? + AND attempt.manifest_json = event.artifact_manifest_json + AND attempt.manifest_sha256 = event.artifact_manifest_sha256 + AND event.artifact_attempt_id = attempt.id + )`, + ) + .bind(crypto.randomUUID(), input.attemptId, input.eventId), + ]; +} + +interface CleanupAttempt { + readonly id: string; + readonly keys: readonly string[]; + readonly deleteAfter: number; +} + +function parseOwnedObjectKeys(value: string): string[] { + const parsed: unknown = JSON.parse(value); + if (!Array.isArray(parsed) || parsed.some((key) => typeof key !== "string" || key.length === 0)) { + throw new Error("Runtime artifact attempt owned object keys are invalid."); + } + return [...new Set(parsed)]; +} + +async function claimRuntimeArtifactCleanupAttempts( + database: D1Database, + nowMs: number, +): Promise { + await getAppDatabase(database) + .delete(runtimeArtifactAttemptsTable) + .where( + and( + eq(runtimeArtifactAttemptsTable.status, "accepted"), + sql`NOT EXISTS ( + SELECT 1 FROM session_event + WHERE session_event.id = ${runtimeArtifactAttemptsTable.acceptedEventId} + AND session_event.artifact_attempt_id = ${runtimeArtifactAttemptsTable.id} + )`, + ), + ) + .run(); + + const candidates = await getAppDatabase(database) + .select({ + deleteAfter: runtimeArtifactAttemptsTable.deleteAfter, + id: runtimeArtifactAttemptsTable.id, + ownedObjectKeysJson: runtimeArtifactAttemptsTable.ownedObjectKeysJson, + status: runtimeArtifactAttemptsTable.status, + updatedAt: runtimeArtifactAttemptsTable.updatedAt, + }) + .from(runtimeArtifactAttemptsTable) + .where( + or( + and( + inArray(runtimeArtifactAttemptsTable.status, ["staging", "staged"]), + lte(runtimeArtifactAttemptsTable.expiresAt, nowMs), + ), + and( + eq(runtimeArtifactAttemptsTable.status, "deleting"), + lte(runtimeArtifactAttemptsTable.updatedAt, nowMs - RUNTIME_ARTIFACT_DELETE_LEASE_MS), + ), + ), + ) + .orderBy(runtimeArtifactAttemptsTable.id) + .limit(RUNTIME_ARTIFACT_CLEANUP_BATCH_SIZE) + .all(); + const claimed: CleanupAttempt[] = []; + + for (const candidate of candidates) { + const deleteAfter = candidate.deleteAfter ?? nowMs + RUNTIME_ARTIFACT_DELETE_QUARANTINE_MS; + const row = await getAppDatabase(database) + .update(runtimeArtifactAttemptsTable) + .set({ + acceptedEventId: null, + deleteAfter, + expiresAt: null, + status: "deleting", + updatedAt: nowMs, + }) + .where( + and( + eq(runtimeArtifactAttemptsTable.id, candidate.id), + eq(runtimeArtifactAttemptsTable.status, candidate.status), + candidate.deleteAfter === null + ? isNull(runtimeArtifactAttemptsTable.deleteAfter) + : eq(runtimeArtifactAttemptsTable.deleteAfter, candidate.deleteAfter), + eq(runtimeArtifactAttemptsTable.updatedAt, candidate.updatedAt), + ), + ) + .returning({ id: runtimeArtifactAttemptsTable.id }) + .get(); + if (row !== undefined) { + claimed.push({ + deleteAfter, + id: candidate.id, + keys: parseOwnedObjectKeys(candidate.ownedObjectKeysJson), + }); + } + } + return claimed; +} + +async function runtimeArtifactObjectIsOwned( + database: D1Database, + input: { readonly attemptId: string; readonly objectKey: string }, +): Promise { + const row = await database + .prepare( + `SELECT 1 AS owned + WHERE EXISTS (SELECT 1 FROM file_record WHERE object_key = ?) + OR EXISTS ( + SELECT 1 + FROM runtime_artifact_attempt AS other, + json_each(other.owned_object_keys_json) AS owned_key + WHERE other.id <> ? + AND other.status IN ('staging', 'staged', 'accepted') + AND owned_key.value = ? + ) + LIMIT 1`, + ) + .bind(input.objectKey, input.attemptId, input.objectKey) + .first<{ owned: number }>(); + return row !== null; +} + +export async function cleanupRuntimeArtifactAttempts(bindings: ApiBindings): Promise { + const nowMs = currentTimestampMs(); + const attempts = await claimRuntimeArtifactCleanupAttempts(bindings.DB, nowMs); + for (const attempt of attempts) { + for (const objectKey of attempt.keys) { + if (await runtimeArtifactObjectIsOwned(bindings.DB, { attemptId: attempt.id, objectKey })) { + continue; + } + await deleteRuntimeArtifactObject(bindings, objectKey); + } + if (attempt.deleteAfter > nowMs) { + continue; + } + await getAppDatabase(bindings.DB) + .delete(runtimeArtifactAttemptsTable) + .where( + and( + eq(runtimeArtifactAttemptsTable.id, attempt.id), + eq(runtimeArtifactAttemptsTable.status, "deleting"), + eq(runtimeArtifactAttemptsTable.deleteAfter, attempt.deleteAfter), + ), + ) + .run(); + } +} diff --git a/apps/api/src/modules/runtime/infrastructure/driver-instance/runtime-artifact-staging.ts b/apps/api/src/modules/runtime/infrastructure/driver-instance/runtime-artifact-staging.ts new file mode 100644 index 00000000..aa9ffc87 --- /dev/null +++ b/apps/api/src/modules/runtime/infrastructure/driver-instance/runtime-artifact-staging.ts @@ -0,0 +1,665 @@ +import { createSessionArtifactPath, normalizeContentType } from "@mosoo/contracts/file"; +import { createPlatformId, parsePlatformId } from "@mosoo/id"; +import type { AccountId, FileId, SessionId, SessionRunId } from "@mosoo/id"; +import { readRuntimeEventFileChanges } from "@mosoo/runtime-events"; +import type { RuntimeEventEnvelope } from "@mosoo/runtime-events"; + +import { withDisposedRpcResource } from "../../../../platform/cloudflare/rpc-disposal"; +import type { ApiBindings } from "../../../../platform/cloudflare/worker-types"; +import { quoteShellArg } from "../../../../shared/shell"; +import { + createRuntimeOutputContentSha256, + createRuntimeOutputParentPath, + getRuntimeOutputName, + putRuntimeArtifactObject, +} from "../../../files/application/file-store"; +import type { DriverRuntimeEventFence } from "../../../sessions/infrastructure/session-runtime-event-store.types"; +import { getRuntimeSubjectKeepAliveHandle } from "../runtime-subject-lifecycle/runtime-subject-lifecycle.service"; +import { getRuntimeConversationSession } from "../runtime-subject-lifecycle/runtime-subject-store"; +import { readSandboxFileBytes } from "../sandbox-file-bytes"; +import type { ExecutionSessionHandle } from "../sandbox-handles"; +import type { RuntimeSessionLink } from "./event-types"; +import { + claimRuntimeArtifactObjectKey, + createRuntimeArtifactAttempt, + createRuntimeArtifactManifest, + getReadyRuntimeArtifact, + sealRuntimeArtifactAttempt, +} from "./runtime-artifact-attempt.repository"; +import type { + RuntimeArtifactCapturePlan, + RuntimeArtifactCapturePlanFile, + RuntimeArtifactManifestFile, + StagedRuntimeArtifactProjection, +} from "./runtime-artifact-attempt.repository"; +import { + RUNTIME_SESSION_OUTPUT_MAX_FILE_BYTES, + RUNTIME_SESSION_OUTPUT_MAX_TOTAL_BYTES, + RUNTIME_SESSION_OUTPUT_SCAN_MAX_FILES, + getRuntimeSessionOutputDirectory, + guessRuntimeSessionOutputContentType, + readRuntimeSessionOutputInventory, + toRuntimeSessionOutputArtifactPath, + toRuntimeSessionOutputFile, +} from "./runtime-session-outputs"; + +export interface DurableRuntimeArtifactEvent { + readonly event: RuntimeEventEnvelope; + readonly semanticHash: string; + readonly sourceEventId: string; +} + +interface RuntimeArtifactCaptureUpsert { + readonly contentType: string | null; + readonly expectedSize: number | null; + readonly operation: "upsert"; + readonly readPath: string; + readonly sourcePath: string; +} + +type RuntimeArtifactCaptureFile = + | RuntimeArtifactCaptureUpsert + | { readonly operation: "delete"; readonly sourcePath: string }; + +interface RuntimeArtifactCaptureSources { + readonly captureStatus: RuntimeArtifactCapturePlan["captureStatus"]; + readonly files: readonly RuntimeArtifactCaptureFile[]; + readonly mode: "delta" | "snapshot"; + readonly sandboxSessionId: string | null; + readonly sourcePaths: readonly string[]; +} + +class RuntimeArtifactCaptureUnavailable extends Error { + constructor(message: string, cause?: unknown) { + super(message, { cause }); + this.name = "RuntimeArtifactCaptureUnavailable"; + } +} + +function omittedRuntimeArtifactSources(mode: "delta" | "snapshot"): RuntimeArtifactCaptureSources { + return { + captureStatus: "omitted_runtime_unavailable", + files: [], + mode, + sandboxSessionId: null, + sourcePaths: [], + }; +} + +async function captureRuntimeArtifactSourcesOrOmit( + mode: "delta" | "snapshot", + capture: () => Promise, +): Promise { + try { + return await capture(); + } catch (error) { + if (error instanceof RuntimeArtifactCaptureUnavailable) { + return omittedRuntimeArtifactSources(mode); + } + throw error; + } +} + +function readRuntimeFileChangeContentType( + metadata: Record | undefined, +): string | null { + const contentType = metadata?.["contentType"] ?? metadata?.["mimeType"]; + return typeof contentType === "string" && contentType.trim().length > 0 ? contentType : null; +} + +function resolveRuntimeOutputCreator(link: RuntimeSessionLink): AccountId | null { + const actorId = link.executionOwnerId ?? link.callerId ?? link.creatorId; + return actorId === null ? null : parsePlatformId(actorId, "runtime output creator"); +} + +function createRuntimeSessionOutputListCommand(outputDir: string): string { + const quotedOutputDir = quoteShellArg(outputDir); + const command = [ + `if [ ! -d ${quotedOutputDir} ]; then exit 0; fi`, + `cd ${quotedOutputDir}`, + "runtime_output_scan_file=$(mktemp)", + "trap 'rm -f \"$runtime_output_scan_file\"' EXIT", + `find . -type f -print0 | head -z -n ${RUNTIME_SESSION_OUTPUT_SCAN_MAX_FILES + 1} > "$runtime_output_scan_file"; runtime_output_pipe_status=("\${PIPESTATUS[@]}"); runtime_output_find_status="\${runtime_output_pipe_status[0]}"; runtime_output_head_status="\${runtime_output_pipe_status[1]}"; if { [ "$runtime_output_find_status" -ne 0 ] && [ "$runtime_output_find_status" -ne 141 ]; } || [ "$runtime_output_head_status" -ne 0 ]; then exit 1; fi`, + 'LC_ALL=C sort -z -o "$runtime_output_scan_file" "$runtime_output_scan_file"', + "xargs -0 -r stat --printf='%n\\0%s\\0' -- < \"$runtime_output_scan_file\"", + ].join(" && "); + return `bash -lc ${quoteShellArg(command)}`; +} + +async function listRuntimeSessionOutputFiles( + handle: ExecutionSessionHandle, + outputDir: string, +): Promise> { + const result = await handle.exec(createRuntimeSessionOutputListCommand(outputDir)); + if (!result.success || result.exitCode !== 0) { + throw new Error( + result.stderr.trim() || + result.stdout.trim() || + `Failed to list runtime session outputs in ${outputDir}.`, + ); + } + const inventory = readRuntimeSessionOutputInventory(result.stdout); + if (inventory.length > RUNTIME_SESSION_OUTPUT_SCAN_MAX_FILES) { + return { captureStatus: "omitted_file_limit", files: [] }; + } + let totalBytes = 0; + for (const file of inventory) { + if ( + file.size > RUNTIME_SESSION_OUTPUT_MAX_FILE_BYTES || + totalBytes > RUNTIME_SESSION_OUTPUT_MAX_TOTAL_BYTES - file.size + ) { + return { captureStatus: "omitted_size_limit", files: [] }; + } + totalBytes += file.size; + } + return { + captureStatus: "complete", + files: inventory.map((file) => ({ + contentType: guessRuntimeSessionOutputContentType(file.relativePath), + expectedSize: file.size, + operation: "upsert", + readPath: `${outputDir}/${file.relativePath}`, + sourcePath: toRuntimeSessionOutputArtifactPath(file.relativePath), + })), + }; +} + +async function readRuntimeArtifactFileSize( + handle: ExecutionSessionHandle, + path: string, +): Promise { + const quotedPath = quoteShellArg(path); + const result = await handle.exec( + `sh -lc ${quoteShellArg( + `if runtime_output_size=$(stat --printf='%s' -- ${quotedPath}); then printf '%s' "$runtime_output_size"; elif [ ! -e ${quotedPath} ]; then exit 44; else exit 1; fi`, + )}`, + ); + if (result.exitCode === 44) { + return null; + } + if (!result.success || result.exitCode !== 0) { + throw new Error( + result.stderr.trim() || result.stdout.trim() || `Failed to stat runtime output ${path}.`, + ); + } + const output = result.stdout.trim(); + const size = Number(output); + if (!/^\d+$/.test(output) || !Number.isSafeInteger(size)) { + throw new Error(`Runtime output size is invalid for ${path}.`); + } + return size; +} + +function getRuntimeArtifactCaptureStatus( + files: readonly RuntimeArtifactCaptureFile[], +): RuntimeArtifactCaptureSources["captureStatus"] { + if (files.length > RUNTIME_SESSION_OUTPUT_SCAN_MAX_FILES) { + return "omitted_file_limit"; + } + let totalBytes = 0; + for (const file of files) { + if (file.operation === "delete") { + continue; + } + if (file.expectedSize === null) { + throw new Error(`Runtime output size is missing for ${file.sourcePath}.`); + } + if ( + file.expectedSize > RUNTIME_SESSION_OUTPUT_MAX_FILE_BYTES || + totalBytes > RUNTIME_SESSION_OUTPUT_MAX_TOTAL_BYTES - file.expectedSize + ) { + return "omitted_size_limit"; + } + totalBytes += file.expectedSize; + } + return "complete"; +} + +function isRuntimeArtifactEvent(event: RuntimeEventEnvelope): boolean { + return ( + event.kind === "file.change.updated" || + event.kind === "file.changed" || + event.kind === "run.completed" + ); +} + +async function captureRuntimeFileChangeSources(input: { + bindings: ApiBindings; + event: RuntimeEventEnvelope; + excludedSourcePaths: ReadonlySet; + link: RuntimeSessionLink; +}): Promise { + if (input.link.sessionId === null || input.link.sandboxId === null) { + throw new RuntimeArtifactCaptureUnavailable( + "Runtime file artifact event is missing its active sandbox identity.", + ); + } + const conversation = await getRuntimeConversationSession(input.bindings.DB, input.link.sessionId); + if (conversation === null) { + throw new RuntimeArtifactCaptureUnavailable( + "Runtime file artifact event is missing its sandbox conversation.", + ); + } + const filesByPath = new Map(); + for (const change of readRuntimeEventFileChanges(input.event)) { + const outputFile = toRuntimeSessionOutputFile({ + contentType: readRuntimeFileChangeContentType(change.metadata), + cwd: conversation.cwd, + path: change.path, + }); + if (outputFile === null) { + continue; + } + filesByPath.set( + outputFile.artifactPath, + change.change === "delete" + ? { operation: "delete", sourcePath: outputFile.artifactPath } + : { + contentType: outputFile.contentType, + expectedSize: null, + operation: "upsert", + readPath: outputFile.readPath, + sourcePath: outputFile.artifactPath, + }, + ); + } + const sourcePaths = [...filesByPath.keys()]; + const files = [...filesByPath.values()] + .filter((file) => !input.excludedSourcePaths.has(file.sourcePath)) + .toSorted((left, right) => left.sourcePath.localeCompare(right.sourcePath)); + if (files.length > RUNTIME_SESSION_OUTPUT_SCAN_MAX_FILES) { + return { + captureStatus: "omitted_file_limit", + files: [], + mode: "delta", + sandboxSessionId: conversation.sandboxSessionId, + sourcePaths, + }; + } + let sizedFiles = files; + if (files.some((file) => file.operation === "upsert")) { + try { + sizedFiles = await withDisposedRpcResource( + await getRuntimeSubjectKeepAliveHandle( + input.bindings, + input.link.sandboxId, + conversation.sandboxIncarnation, + ), + async (sandbox) => { + const sandboxSession = await sandbox.getSession(conversation.sandboxSessionId); + return Promise.all( + files.map(async (file) => + file.operation === "delete" + ? file + : { + contentType: file.contentType, + expectedSize: await readRuntimeArtifactFileSize(sandboxSession, file.readPath), + operation: file.operation, + readPath: file.readPath, + sourcePath: file.sourcePath, + }, + ), + ); + }, + ); + } catch (error) { + throw new RuntimeArtifactCaptureUnavailable( + "Runtime file artifact sources are unavailable.", + error, + ); + } + } + const captureStatus = sizedFiles.some( + (file) => file.operation === "upsert" && file.expectedSize === null, + ) + ? "omitted_source_missing" + : getRuntimeArtifactCaptureStatus(sizedFiles); + return { + captureStatus, + files: captureStatus === "complete" ? sizedFiles : [], + mode: "delta", + sandboxSessionId: conversation.sandboxSessionId, + sourcePaths, + }; +} + +async function captureRuntimeOutputSnapshotSources(input: { + bindings: ApiBindings; + link: RuntimeSessionLink; +}): Promise { + if (input.link.sessionId === null || input.link.sandboxId === null) { + throw new RuntimeArtifactCaptureUnavailable( + "Runtime output snapshot is missing its active sandbox identity.", + ); + } + const conversation = await getRuntimeConversationSession(input.bindings.DB, input.link.sessionId); + if (conversation === null) { + throw new RuntimeArtifactCaptureUnavailable( + "Runtime output snapshot is missing its sandbox conversation.", + ); + } + const outputDir = getRuntimeSessionOutputDirectory(conversation.cwd); + let capture: Pick; + try { + capture = await withDisposedRpcResource( + await getRuntimeSubjectKeepAliveHandle( + input.bindings, + input.link.sandboxId, + conversation.sandboxIncarnation, + ), + async (sandbox) => { + const sandboxSession = await sandbox.getSession(conversation.sandboxSessionId); + return listRuntimeSessionOutputFiles(sandboxSession, outputDir); + }, + ); + } catch (error) { + throw new RuntimeArtifactCaptureUnavailable("Runtime output snapshot is unavailable.", error); + } + return { + ...capture, + mode: "snapshot", + sandboxSessionId: conversation.sandboxSessionId, + sourcePaths: capture.files.map((file) => file.sourcePath), + }; +} + +async function stageRuntimeArtifactOmission( + bindings: ApiBindings, + input: { + readonly createdByAccountId: AccountId; + readonly driverFence: DriverRuntimeEventFence; + readonly event: DurableRuntimeArtifactEvent; + readonly mode: "delta" | "snapshot"; + readonly runId: SessionRunId; + readonly sessionId: SessionId; + }, +): Promise { + const attemptId = crypto.randomUUID(); + await createRuntimeArtifactAttempt(bindings.DB, { + attemptId, + createdByAccountId: input.createdByAccountId, + driverFence: input.driverFence, + eventType: input.event.event.kind, + runId: input.runId, + semanticHash: input.event.semanticHash, + sessionId: input.sessionId, + sourceEventId: input.event.sourceEventId, + }); + const manifest = await createRuntimeArtifactManifest({ + captureStatus: "omitted_runtime_unavailable", + files: [], + mode: input.mode, + semanticHash: input.event.semanticHash, + sourceEventId: input.event.sourceEventId, + }); + const projection = { attemptId, ...manifest }; + await sealRuntimeArtifactAttempt(bindings.DB, projection); + return projection; +} + +export async function stageRuntimeArtifactEvents( + bindings: ApiBindings, + input: { + readonly driverFence: DriverRuntimeEventFence; + readonly events: readonly DurableRuntimeArtifactEvent[]; + readonly link: RuntimeSessionLink; + }, +): Promise> { + const staged = new Map(); + if (input.link.sessionId === null || input.link.sessionRunId === null) { + if (input.events.some(({ event }) => isRuntimeArtifactEvent(event))) { + throw new Error("Runtime artifact events require a Session Run identity."); + } + return staged; + } + const runId = input.link.sessionRunId; + const createdBy = resolveRuntimeOutputCreator(input.link); + if (createdBy === null) { + throw new Error("Runtime artifact events require a creator identity."); + } + const sessionId = parsePlatformId(input.link.sessionId, "runtime output session ID"); + const artifactEvents = input.events.filter(({ event }) => isRuntimeArtifactEvent(event)); + const completedEvent = artifactEvents.find(({ event }) => event.kind === "run.completed"); + const stageUnavailableArtifacts = async (): Promise< + ReadonlyMap + > => { + const projections = new Map(); + for (const event of completedEvent === undefined ? artifactEvents : [completedEvent]) { + projections.set( + event.sourceEventId, + await stageRuntimeArtifactOmission(bindings, { + createdByAccountId: createdBy, + driverFence: input.driverFence, + event, + mode: event.event.kind === "run.completed" ? "snapshot" : "delta", + runId, + sessionId, + }), + ); + } + return projections; + }; + const captures: { event: DurableRuntimeArtifactEvent; sources: RuntimeArtifactCaptureSources }[] = + []; + if (completedEvent !== undefined) { + captures.push({ + event: completedEvent, + sources: await captureRuntimeArtifactSourcesOrOmit("snapshot", () => + captureRuntimeOutputSnapshotSources({ bindings, link: input.link }), + ), + }); + } else { + const laterPaths = new Set(); + for (let index = artifactEvents.length - 1; index >= 0; index -= 1) { + const event = artifactEvents[index]; + if (event === undefined) { + continue; + } + const sources = await captureRuntimeArtifactSourcesOrOmit("delta", () => + captureRuntimeFileChangeSources({ + bindings, + event: event.event, + excludedSourcePaths: laterPaths, + link: input.link, + }), + ); + for (const sourcePath of sources.sourcePaths) { + laterPaths.add(sourcePath); + } + captures.unshift({ + event, + sources, + }); + } + } + if (captures.some(({ sources }) => sources.captureStatus === "omitted_runtime_unavailable")) { + return stageUnavailableArtifacts(); + } + + for (const { event: durableEvent, sources } of captures) { + if ( + sources.mode === "delta" && + sources.captureStatus === "complete" && + sources.files.length === 0 + ) { + continue; + } + const proposedAttemptId = crypto.randomUUID(); + const proposedCapturePlan: RuntimeArtifactCapturePlan = { + captureStatus: sources.captureStatus, + files: sources.files.map((file) => { + if (file.operation === "delete") { + return file; + } + if (file.expectedSize === null) { + throw new Error(`Runtime output size is missing for ${file.sourcePath}.`); + } + const fileId = createPlatformId(); + return { + contentType: file.contentType, + expectedSize: file.expectedSize, + fileId, + name: getRuntimeOutputName(file.sourcePath), + objectKey: `runtime-artifact-attempts/v1/${proposedAttemptId}/files/${fileId}`, + operation: file.operation, + readPath: file.readPath, + sourcePath: file.sourcePath, + }; + }), + mode: sources.mode, + version: 1, + }; + await createRuntimeArtifactAttempt(bindings.DB, { + attemptId: proposedAttemptId, + createdByAccountId: createdBy, + driverFence: input.driverFence, + eventType: durableEvent.event.kind, + runId, + semanticHash: durableEvent.semanticHash, + sessionId, + sourceEventId: durableEvent.sourceEventId, + }); + const attemptId = proposedAttemptId; + const capturePlan = proposedCapturePlan; + + let captureStatus: RuntimeArtifactCapturePlan["captureStatus"] = capturePlan.captureStatus; + const manifestFiles: RuntimeArtifactManifestFile[] = capturePlan.files.flatMap((file) => + file.operation === "delete" ? [file] : [], + ); + const upserts = capturePlan.files.filter( + (file): file is RuntimeArtifactCapturePlanFile => file.operation === "upsert", + ); + const capturedBodies = new Map(); + if (captureStatus === "complete" && upserts.length > 0) { + const sandboxSessionId = sources.sandboxSessionId; + if (input.link.sandboxId === null || sandboxSessionId === null) { + return stageUnavailableArtifacts(); + } + const conversation = await getRuntimeConversationSession(bindings.DB, sessionId); + if ( + conversation === null || + conversation.sandboxSessionId !== sandboxSessionId || + conversation.sandboxId !== input.link.sandboxId + ) { + return stageUnavailableArtifacts(); + } + try { + await withDisposedRpcResource( + await getRuntimeSubjectKeepAliveHandle( + bindings, + input.link.sandboxId, + conversation.sandboxIncarnation, + ), + async (sandbox) => { + const sandboxSession = await sandbox.getSession(sandboxSessionId); + let totalBytes = 0; + for (const file of upserts) { + const body = await readSandboxFileBytes( + sandboxSession, + file.readPath, + RUNTIME_SESSION_OUTPUT_MAX_FILE_BYTES, + ); + if ( + body.byteLength > RUNTIME_SESSION_OUTPUT_MAX_FILE_BYTES || + totalBytes > RUNTIME_SESSION_OUTPUT_MAX_TOTAL_BYTES - body.byteLength + ) { + captureStatus = "omitted_size_limit"; + capturedBodies.clear(); + return; + } + if (body.byteLength !== file.expectedSize) { + captureStatus = "omitted_source_changed"; + capturedBodies.clear(); + return; + } + totalBytes += body.byteLength; + capturedBodies.set(file.objectKey, body); + } + }, + ); + } catch { + return stageUnavailableArtifacts(); + } + } + if (captureStatus !== "complete") { + manifestFiles.length = 0; + } else { + let storageUnavailable = false; + for (const file of upserts) { + const body = capturedBodies.get(file.objectKey); + if (body === undefined) { + throw new Error(`Runtime output content is missing for ${file.sourcePath}.`); + } + const contentSha256 = await createRuntimeOutputContentSha256(body); + const parentPath = createRuntimeOutputParentPath(file.sourcePath, contentSha256); + const existing = await getReadyRuntimeArtifact(bindings.DB, { + parentPath, + sessionId, + }); + if (existing !== null) { + manifestFiles.push({ + contentSha256, + contentType: existing.contentType, + disposition: "reuse", + etag: existing.etag, + fileId: existing.fileId, + name: existing.name, + objectKey: existing.objectKey, + operation: "upsert", + parentPath, + path: existing.path, + size: existing.size, + sourcePath: file.sourcePath, + }); + continue; + } + await claimRuntimeArtifactObjectKey(bindings.DB, { + attemptId, + objectKey: file.objectKey, + }); + const contentType = normalizeContentType(file.contentType ?? "application/octet-stream"); + const stored = await putRuntimeArtifactObject({ + attemptId, + bindings, + body, + contentSha256, + contentType, + objectKey: file.objectKey, + sourcePath: file.sourcePath, + }); + if (stored === null) { + storageUnavailable = true; + break; + } + manifestFiles.push({ + contentSha256, + contentType: stored.contentType ?? contentType, + disposition: "create", + etag: stored.etag, + fileId: file.fileId, + name: file.name, + objectKey: file.objectKey, + operation: "upsert", + parentPath, + path: createSessionArtifactPath(file.fileId, file.name), + size: stored.contentLength, + sourcePath: file.sourcePath, + }); + } + if (storageUnavailable) { + return stageUnavailableArtifacts(); + } + } + const manifest = await createRuntimeArtifactManifest({ + captureStatus, + files: manifestFiles, + mode: sources.mode, + semanticHash: durableEvent.semanticHash, + sourceEventId: durableEvent.sourceEventId, + }); + const projection = { attemptId, ...manifest }; + await sealRuntimeArtifactAttempt(bindings.DB, projection); + staged.set(durableEvent.sourceEventId, projection); + } + return staged; +} diff --git a/apps/api/src/modules/runtime/infrastructure/driver-instance/runtime-event-compaction.ts b/apps/api/src/modules/runtime/infrastructure/driver-instance/runtime-event-compaction.ts deleted file mode 100644 index 12e6bd6e..00000000 --- a/apps/api/src/modules/runtime/infrastructure/driver-instance/runtime-event-compaction.ts +++ /dev/null @@ -1,197 +0,0 @@ -import { - readRuntimeEventMessageContent, - readRuntimeEventPayload, - readRuntimeEventString, -} from "@mosoo/runtime-events"; -import type { - RuntimeEventEnvelope, - RuntimeEventKind, - RuntimeEventToolStatus, -} from "@mosoo/runtime-events"; - -import type { ProjectedRuntimeEventRecord } from "./event-types"; - -export type RuntimeEventPayloadRecord = Record; - -export type TextStreamKind = "message" | "thought"; - -export interface StreamAccumulator { - content: string; - firstRecord: ProjectedRuntimeEventRecord; - key: string; - lastRecord: ProjectedRuntimeEventRecord; - runId: string | null; - sessionId: string; -} - -export interface TextStreamAccumulator extends StreamAccumulator { - kind: TextStreamKind; - role: "agent" | "user"; -} - -export interface ToolCallAccumulator extends StreamAccumulator { - itemCompleted: boolean; - name: string | null; - payload: RuntimeEventPayloadRecord; - rawInput: string | null; - rawOutput: string | null; - status: RuntimeEventToolStatus; - toolCallId: string; -} - -const terminalRunKinds = new Set([ - "run.cancelled", - "run.completed", - "run.failed", -]); - -export function toStreamKey(input: { - id: string; - kind: TextStreamKind | "tool"; - runId: string | null; - sessionId: string; -}): string { - return `${input.sessionId}:${input.runId ?? ""}:${input.kind}:${input.id}`; -} - -function readRecordOccurredAt(record: ProjectedRuntimeEventRecord): number | null { - if (record.occurredAt !== null) { - return record.occurredAt; - } - - const occurredAt = Date.parse(record.event.occurredAt); - return Number.isFinite(occurredAt) ? occurredAt : null; -} - -export function readPayloadString( - payload: RuntimeEventPayloadRecord, - field: string, -): string | null { - const value = payload[field]; - return typeof value === "string" ? value : null; -} - -function appendStreamText(current: string | null, next: string | null): string | null { - if (next === null || next.length === 0) { - return current; - } - - if (current === null || current.length === 0) { - return next; - } - - return `${current}${next}`; -} - -function mergeSnapshotText(current: string | null, next: string | null): string | null { - if (next === null || next.length === 0) { - return current; - } - - if (current === null || current.length === 0) { - return next; - } - - if (next.length > current.length && next.startsWith(current)) { - return next; - } - - if (current.length >= next.length && current.startsWith(next)) { - return current; - } - - return `${current}${next}`; -} - -export function mergeDeliveredText( - current: string | null, - next: string | null, - delivery: RuntimeEventEnvelope["delivery"], -): string | null { - return delivery === "best_effort" - ? appendStreamText(current, next) - : mergeSnapshotText(current, next); -} - -export function mergeTextEventContent( - accumulator: TextStreamAccumulator, - event: RuntimeEventEnvelope, -): void { - const delta = readRuntimeEventString(readRuntimeEventPayload(event), "contentDelta"); - - if (delta !== null) { - accumulator.content = appendStreamText(accumulator.content, delta) ?? ""; - return; - } - - accumulator.content = - mergeSnapshotText(accumulator.content, readRuntimeEventMessageContent(event)) ?? ""; -} - -export function readRuntimeEventMessageRoleUpdate( - event: RuntimeEventEnvelope, -): TextStreamAccumulator["role"] | null { - const role = readRuntimeEventString(readRuntimeEventPayload(event), "role"); - - return role === "agent" || role === "user" ? role : null; -} - -function createEventWithPayload( - source: RuntimeEventEnvelope, - input: { - delivery?: RuntimeEventEnvelope["delivery"]; - kind: RuntimeEventKind; - payload: RuntimeEventPayloadRecord; - sourceEventId: string | null; - }, -): RuntimeEventEnvelope { - const { sourceEventId: _sourceEventId, ...rest } = source; - - return { - ...rest, - delivery: input.delivery ?? "lossless", - kind: input.kind, - payload: input.payload, - ...(input.sourceEventId === null ? {} : { sourceEventId: input.sourceEventId }), - }; -} - -export function createCompactedRecord( - accumulator: StreamAccumulator, - input: { - kind: RuntimeEventKind; - payload: RuntimeEventPayloadRecord; - }, -): ProjectedRuntimeEventRecord { - return { - event: createEventWithPayload(accumulator.lastRecord.event, { - kind: input.kind, - payload: input.payload, - sourceEventId: accumulator.lastRecord.sourceEventId, - }), - occurredAt: readRecordOccurredAt(accumulator.firstRecord), - sourceEventId: accumulator.lastRecord.sourceEventId, - }; -} - -export function readToolItemId(event: RuntimeEventEnvelope): string | null { - if (event.kind !== "item.completed" && event.kind !== "item.started") { - return null; - } - - const payload = readRuntimeEventPayload(event); - - if (readRuntimeEventString(payload, "itemType") !== "tool_call") { - return null; - } - - return readRuntimeEventString(payload, "itemId") ?? event.id; -} - -export function isTerminalRunEvent(event: RuntimeEventEnvelope): boolean { - return terminalRunKinds.has(event.kind); -} - -export function toTerminalToolStatus(event: RuntimeEventEnvelope): RuntimeEventToolStatus { - return event.kind === "run.failed" || event.kind === "run.cancelled" ? "failed" : "completed"; -} diff --git a/apps/api/src/modules/runtime/infrastructure/driver-instance/runtime-event-persistence-compactor.ts b/apps/api/src/modules/runtime/infrastructure/driver-instance/runtime-event-persistence-compactor.ts deleted file mode 100644 index 5e523ed2..00000000 --- a/apps/api/src/modules/runtime/infrastructure/driver-instance/runtime-event-persistence-compactor.ts +++ /dev/null @@ -1,440 +0,0 @@ -import { - readRuntimeEventMessageKey, - readRuntimeEventMessageRole, - readRuntimeEventPayload, - readRuntimeEventString, - readRuntimeEventToolCallId, - readRuntimeEventToolName, - readRuntimeEventToolStatusFromEvent, - readRuntimeRunPayload, -} from "@mosoo/runtime-events"; -import type { RuntimeEventEnvelope, RuntimeEventToolStatus } from "@mosoo/runtime-events"; - -import type { ProjectedRuntimeEventRecord } from "./event-types"; -import { - createCompactedRecord, - isTerminalRunEvent, - mergeDeliveredText, - mergeTextEventContent, - readPayloadString, - readRuntimeEventMessageRoleUpdate, - readToolItemId, - toStreamKey, - toTerminalToolStatus, -} from "./runtime-event-compaction"; -import type { - RuntimeEventPayloadRecord, - TextStreamAccumulator, - TextStreamKind, - ToolCallAccumulator, -} from "./runtime-event-compaction"; - -export class RuntimeEventPersistenceCompactor { - readonly #messages = new Map(); - readonly #thoughts = new Map(); - readonly #tools = new Map(); - - compact(records: ProjectedRuntimeEventRecord[]): ProjectedRuntimeEventRecord[] { - const output: ProjectedRuntimeEventRecord[] = []; - - for (const record of records) { - if (this.#applyMessage(record, output)) { - continue; - } - - if (this.#applyThought(record, output)) { - continue; - } - - if (this.#applyTool(record)) { - continue; - } - - if (isTerminalRunEvent(record.event)) { - output.push(...this.#flushRun(record.event, toTerminalToolStatus(record.event))); - output.push(record); - continue; - } - - output.push(record); - } - - output.push(...this.#flushReadyTools()); - return output; - } - - #applyMessage( - record: ProjectedRuntimeEventRecord, - output: ProjectedRuntimeEventRecord[], - ): boolean { - const event = record.event; - - if ( - event.kind !== "message.added" && - event.kind !== "message.completed" && - event.kind !== "message.delta" && - event.kind !== "message.started" - ) { - return false; - } - - const key = readRuntimeEventMessageKey(event); - - if (key === null) { - return true; - } - - const streamKey = toStreamKey({ - id: key, - kind: "message", - runId: event.runId ?? null, - sessionId: event.sessionId, - }); - const accumulator = this.#upsertTextAccumulator(this.#messages, { - key: streamKey, - kind: "message", - record, - }); - mergeTextEventContent(accumulator, event); - - if (event.kind === "message.added" || event.kind === "message.completed") { - this.#messages.delete(streamKey); - const compacted = this.#createMessageRecord(accumulator); - - if (compacted !== null) { - output.push(compacted); - } else if (event.kind === "message.added") { - output.push(record); - } - } - - return true; - } - - #applyThought( - record: ProjectedRuntimeEventRecord, - output: ProjectedRuntimeEventRecord[], - ): boolean { - const event = record.event; - - if ( - event.kind !== "thought.completed" && - event.kind !== "thought.delta" && - event.kind !== "thought.started" - ) { - return false; - } - - const key = readRuntimeEventMessageKey(event); - - if (key === null) { - return true; - } - - const streamKey = toStreamKey({ - id: key, - kind: "thought", - runId: event.runId ?? null, - sessionId: event.sessionId, - }); - const accumulator = this.#upsertTextAccumulator(this.#thoughts, { - key: streamKey, - kind: "thought", - record, - }); - mergeTextEventContent(accumulator, event); - - if (event.kind === "thought.completed") { - this.#thoughts.delete(streamKey); - const compacted = this.#createThoughtRecord(accumulator); - - if (compacted !== null) { - output.push(compacted); - } - } - - return true; - } - - #applyTool(record: ProjectedRuntimeEventRecord): boolean { - const event = record.event; - const toolCallId = readRuntimeEventToolCallId(event) ?? readToolItemId(event); - - if (toolCallId === null) { - return false; - } - - const key = toStreamKey({ - id: toolCallId, - kind: "tool", - runId: event.runId ?? null, - sessionId: event.sessionId, - }); - const accumulator = this.#upsertToolAccumulator(key, record, toolCallId); - const payload = readRuntimeEventPayload(event); - - accumulator.lastRecord = record; - this.#mergeToolPayload(accumulator, payload); - accumulator.content = - mergeDeliveredText( - accumulator.content, - readPayloadString(payload, "content"), - event.delivery, - ) ?? ""; - accumulator.rawInput = mergeDeliveredText( - accumulator.rawInput, - readPayloadString(payload, "rawInput"), - event.delivery, - ); - accumulator.rawOutput = mergeDeliveredText( - accumulator.rawOutput, - readPayloadString(payload, "rawOutput"), - event.delivery, - ); - accumulator.status = readRuntimeEventToolStatusFromEvent(event); - - const name = readRuntimeEventToolName(event); - - if (name !== null) { - accumulator.name = name; - } - - if (event.kind === "item.completed") { - accumulator.itemCompleted = true; - const status = readRuntimeEventString(payload, "status"); - accumulator.status = status === "failed" ? "failed" : "completed"; - } - - return true; - } - - #upsertTextAccumulator( - map: Map, - input: { - key: string; - kind: TextStreamKind; - record: ProjectedRuntimeEventRecord; - }, - ): TextStreamAccumulator { - const existing = map.get(input.key); - - if (existing !== undefined) { - existing.lastRecord = input.record; - existing.role = readRuntimeEventMessageRoleUpdate(input.record.event) ?? existing.role; - return existing; - } - - const accumulator: TextStreamAccumulator = { - content: "", - firstRecord: input.record, - key: input.key, - kind: input.kind, - lastRecord: input.record, - role: readRuntimeEventMessageRole(input.record.event), - runId: input.record.event.runId ?? null, - sessionId: input.record.event.sessionId, - }; - map.set(input.key, accumulator); - return accumulator; - } - - #upsertToolAccumulator( - key: string, - record: ProjectedRuntimeEventRecord, - toolCallId: string, - ): ToolCallAccumulator { - const existing = this.#tools.get(key); - - if (existing !== undefined) { - return existing; - } - - const accumulator: ToolCallAccumulator = { - content: "", - firstRecord: record, - itemCompleted: false, - key, - lastRecord: record, - name: readRuntimeEventToolName(record.event), - payload: {}, - rawInput: null, - rawOutput: null, - runId: record.event.runId ?? null, - sessionId: record.event.sessionId, - status: readRuntimeEventToolStatusFromEvent(record.event), - toolCallId, - }; - this.#tools.set(key, accumulator); - return accumulator; - } - - #createMessageRecord(accumulator: TextStreamAccumulator): ProjectedRuntimeEventRecord | null { - if (accumulator.content.length === 0) { - return null; - } - - const payload: RuntimeEventPayloadRecord = { - ...readRuntimeEventPayload(accumulator.lastRecord.event), - content: accumulator.content, - messageId: readRuntimeEventMessageKey(accumulator.lastRecord.event) ?? accumulator.key, - role: accumulator.role, - }; - delete payload["contentDelta"]; - - return createCompactedRecord(accumulator, { - kind: "message.added", - payload, - }); - } - - #createThoughtRecord(accumulator: TextStreamAccumulator): ProjectedRuntimeEventRecord | null { - if (accumulator.content.length === 0) { - return null; - } - - const payload: RuntimeEventPayloadRecord = { - ...readRuntimeEventPayload(accumulator.lastRecord.event), - content: accumulator.content, - thoughtId: readRuntimeEventMessageKey(accumulator.lastRecord.event) ?? accumulator.key, - }; - delete payload["contentDelta"]; - - return createCompactedRecord(accumulator, { - kind: "thought.completed", - payload, - }); - } - - #createToolRecord(accumulator: ToolCallAccumulator): ProjectedRuntimeEventRecord { - const payload = { - ...this.#mergeToolPayloads(accumulator), - status: accumulator.status, - toolCallId: accumulator.toolCallId, - }; - - return createCompactedRecord(accumulator, { - kind: "tool.call.updated", - payload, - }); - } - - #mergeToolPayloads(accumulator: ToolCallAccumulator): RuntimeEventPayloadRecord { - const merged: RuntimeEventPayloadRecord = { ...accumulator.payload }; - - if (accumulator.name !== null) { - merged["title"] ??= accumulator.name; - } - - if (accumulator.content.length > 0) { - merged["content"] = accumulator.content; - } - - if (accumulator.rawInput !== null) { - merged["rawInput"] = accumulator.rawInput; - } - - if (accumulator.rawOutput !== null) { - merged["rawOutput"] = accumulator.rawOutput; - } - - return merged; - } - - #mergeToolPayload(accumulator: ToolCallAccumulator, payload: RuntimeEventPayloadRecord): void { - for (const [key, value] of Object.entries(payload)) { - if (key !== "content" && key !== "rawInput" && key !== "rawOutput") { - accumulator.payload[key] = value; - } - } - } - - #flushReadyTools(): ProjectedRuntimeEventRecord[] { - const output: ProjectedRuntimeEventRecord[] = []; - - for (const [key, accumulator] of this.#tools) { - if (!accumulator.itemCompleted) { - continue; - } - - this.#tools.delete(key); - output.push(this.#createToolRecord(accumulator)); - } - - return output; - } - - #flushRun( - terminalEvent: RuntimeEventEnvelope, - terminalToolStatus: RuntimeEventToolStatus, - ): ProjectedRuntimeEventRecord[] { - const output: ProjectedRuntimeEventRecord[] = []; - const runId = terminalEvent.runId ?? null; - const terminalToolResult = readTerminalToolResult(terminalEvent); - - for (const [key, accumulator] of this.#messages) { - if (accumulator.sessionId !== terminalEvent.sessionId || accumulator.runId !== runId) { - continue; - } - - this.#messages.delete(key); - const compacted = this.#createMessageRecord(accumulator); - - if (compacted !== null) { - output.push(compacted); - } - } - - for (const [key, accumulator] of this.#thoughts) { - if (accumulator.sessionId !== terminalEvent.sessionId || accumulator.runId !== runId) { - continue; - } - - this.#thoughts.delete(key); - const compacted = this.#createThoughtRecord(accumulator); - - if (compacted !== null) { - output.push(compacted); - } - } - - for (const [key, accumulator] of this.#tools) { - if (accumulator.sessionId !== terminalEvent.sessionId || accumulator.runId !== runId) { - continue; - } - - this.#tools.delete(key); - - if (accumulator.status === "running") { - accumulator.status = terminalToolStatus; - } - - if ( - terminalToolResult !== null && - accumulator.status === "failed" && - accumulator.rawOutput === null && - accumulator.content.length === 0 - ) { - accumulator.content = terminalToolResult; - accumulator.rawOutput = terminalToolResult; - } - - output.push(this.#createToolRecord(accumulator)); - } - - return output; - } -} - -function readTerminalToolResult(event: RuntimeEventEnvelope): string | null { - if (event.kind === "run.failed") { - const run = readRuntimeRunPayload(event).run; - const message = run?.error?.message ?? "Run failed before the tool returned a result."; - return `Tool failed before returning a result: ${message}`; - } - - if (event.kind === "run.cancelled") { - return "Tool was cancelled before returning a result."; - } - - return null; -} diff --git a/apps/api/src/modules/runtime/infrastructure/driver-instance/runtime-event-replay-filter.ts b/apps/api/src/modules/runtime/infrastructure/driver-instance/runtime-event-replay-filter.ts deleted file mode 100644 index 86c7554a..00000000 --- a/apps/api/src/modules/runtime/infrastructure/driver-instance/runtime-event-replay-filter.ts +++ /dev/null @@ -1,208 +0,0 @@ -import type { DriverEventEnvelope } from "@mosoo/agent-driver/events"; - -type ReplayRuntimeEvent = DriverEventEnvelope["event"]; -type ReplayRuntimeEventPayload = Record; - -export function filterDurablyAcceptedRuntimeStreamReplays( - events: readonly DriverEventEnvelope[], - durableEventIds: ReadonlySet, -): DriverEventEnvelope[] { - const durableStreams = createDurableRuntimeStreamReplayIndex(events, durableEventIds); - - return events.filter((envelope) => { - if (envelope.eventId.length > 0 && durableEventIds.has(envelope.eventId)) { - return false; - } - - return !isDurablyAcceptedRuntimeStreamReplay(envelope.event, durableStreams); - }); -} - -interface DurableRuntimeStreamReplayIndex { - messageKeys: Set; - runKeys: Set; - thoughtKeys: Set; - toolKeys: Set; -} - -function createDurableRuntimeStreamReplayIndex( - events: readonly DriverEventEnvelope[], - durableEventIds: ReadonlySet, -): DurableRuntimeStreamReplayIndex { - const index: DurableRuntimeStreamReplayIndex = { - messageKeys: new Set(), - runKeys: new Set(), - thoughtKeys: new Set(), - toolKeys: new Set(), - }; - - for (const envelope of events) { - if (envelope.eventId.length === 0 || !durableEventIds.has(envelope.eventId)) { - continue; - } - - const event = envelope.event; - - if (event.kind === "message.added" || event.kind === "message.completed") { - const key = readRuntimeMessageReplayKey(event); - - if (key !== null) { - index.messageKeys.add(key); - } - continue; - } - - if (event.kind === "thought.completed") { - const key = readRuntimeThoughtReplayKey(event); - - if (key !== null) { - index.thoughtKeys.add(key); - } - continue; - } - - if (event.kind === "item.completed" || event.kind === "tool.call.updated") { - const key = readRuntimeToolReplayKey(event); - - if (key !== null) { - index.toolKeys.add(key); - } - continue; - } - - if ( - event.kind === "run.cancelled" || - event.kind === "run.completed" || - event.kind === "run.failed" - ) { - const key = readRuntimeRunReplayKey(event); - - if (key !== null) { - index.runKeys.add(key); - } - } - } - - return index; -} - -function isDurablyAcceptedRuntimeStreamReplay( - event: ReplayRuntimeEvent, - index: DurableRuntimeStreamReplayIndex, -): boolean { - if (isRunBoundRuntimeStreamEvent(event)) { - const runKey = readRuntimeRunReplayKey(event); - - if (runKey !== null && index.runKeys.has(runKey)) { - return true; - } - } - - const messageKey = readRuntimeMessageReplayKey(event); - - if (messageKey !== null && index.messageKeys.has(messageKey)) { - return true; - } - - const thoughtKey = readRuntimeThoughtReplayKey(event); - - if (thoughtKey !== null && index.thoughtKeys.has(thoughtKey)) { - return true; - } - - const toolKey = readRuntimeToolReplayKey(event); - - return toolKey !== null && index.toolKeys.has(toolKey); -} - -function isRunBoundRuntimeStreamEvent(event: ReplayRuntimeEvent): boolean { - return ( - event.kind === "item.completed" || - event.kind === "item.started" || - event.kind === "message.added" || - event.kind === "message.completed" || - event.kind === "message.delta" || - event.kind === "message.started" || - event.kind === "thought.completed" || - event.kind === "thought.delta" || - event.kind === "thought.started" || - event.kind === "tool.call.updated" - ); -} - -function readRuntimeRunReplayKey(event: ReplayRuntimeEvent): string | null { - return event.runId === undefined ? null : `${event.sessionId}:${event.runId}`; -} - -function readRuntimeMessageReplayKey(event: ReplayRuntimeEvent): string | null { - if ( - event.kind !== "message.added" && - event.kind !== "message.completed" && - event.kind !== "message.delta" && - event.kind !== "message.started" - ) { - return null; - } - - const payload = readRuntimeEventPayload(event); - const messageKey = readRuntimeEventString(payload, "messageId") ?? event.id; - return messageKey === null ? null : `${event.sessionId}:${event.runId ?? ""}:${messageKey}`; -} - -function readRuntimeThoughtReplayKey(event: ReplayRuntimeEvent): string | null { - if ( - event.kind !== "thought.completed" && - event.kind !== "thought.delta" && - event.kind !== "thought.started" - ) { - return null; - } - - const payload = readRuntimeEventPayload(event); - const thoughtKey = readRuntimeEventString(payload, "thoughtId") ?? event.id; - return thoughtKey === null ? null : `${event.sessionId}:${event.runId ?? ""}:${thoughtKey}`; -} - -function readRuntimeToolReplayKey(event: ReplayRuntimeEvent): string | null { - const toolCallId = readRuntimeEventToolCallId(event) ?? readRuntimeToolItemReplayId(event); - return toolCallId === null ? null : `${event.sessionId}:${event.runId ?? ""}:${toolCallId}`; -} - -function readRuntimeToolItemReplayId(event: ReplayRuntimeEvent): string | null { - if (event.kind !== "item.completed" && event.kind !== "item.started") { - return null; - } - - const payload = readRuntimeEventPayload(event); - - if (readRuntimeEventString(payload, "itemType") !== "tool_call") { - return null; - } - - return readRuntimeEventString(payload, "itemId") ?? event.id; -} - -function readRuntimeEventToolCallId(event: ReplayRuntimeEvent): string | null { - if (event.kind !== "tool.call.updated") { - return null; - } - - return readRuntimeEventString(readRuntimeEventPayload(event), "toolCallId") ?? event.id; -} - -function readRuntimeEventPayload(event: ReplayRuntimeEvent): ReplayRuntimeEventPayload { - return isRuntimeEventPayload(event.payload) ? event.payload : {}; -} - -function isRuntimeEventPayload(value: unknown): value is ReplayRuntimeEventPayload { - return typeof value === "object" && value !== null && !Array.isArray(value); -} - -function readRuntimeEventString(value: unknown, field: string): string | null { - if (!isRuntimeEventPayload(value)) { - return null; - } - - const entry = value[field]; - return typeof entry === "string" && entry.length > 0 ? entry : null; -} diff --git a/apps/api/src/modules/runtime/infrastructure/driver-instance/runtime-session-outputs.ts b/apps/api/src/modules/runtime/infrastructure/driver-instance/runtime-session-outputs.ts index 2ccf1d22..a6741faf 100644 --- a/apps/api/src/modules/runtime/infrastructure/driver-instance/runtime-session-outputs.ts +++ b/apps/api/src/modules/runtime/infrastructure/driver-instance/runtime-session-outputs.ts @@ -1,5 +1,14 @@ +import { normalizeLibraryFilePath } from "@mosoo/contracts/file"; + export const RUNTIME_SESSION_OUTPUT_DIR_NAME = "outputs"; export const RUNTIME_SESSION_OUTPUT_SCAN_MAX_FILES = 100; +export const RUNTIME_SESSION_OUTPUT_MAX_FILE_BYTES = 8 * 1024 * 1024; +export const RUNTIME_SESSION_OUTPUT_MAX_TOTAL_BYTES = 32 * 1024 * 1024; + +export interface RuntimeSessionOutputInventoryFile { + readonly relativePath: string; + readonly size: number; +} export interface RuntimeSessionOutputFile { readonly artifactPath: string; @@ -27,10 +36,6 @@ const contentTypesByExtension = new Map([ ["zip", "application/zip"], ]); -function normalizeSandboxPath(value: string): string { - return value.trim().replaceAll("\\", "/").replace(/\/+$/, ""); -} - function joinSandboxPath(parent: string, child: string): string { return `${parent.replace(/\/+$/, "")}/${child.replace(/^\/+/, "")}`; } @@ -44,31 +49,12 @@ export function normalizeRuntimeSessionOutputRelativePath(value: unknown): strin return null; } - const normalizedPath = value.trim().replaceAll("\\", "/"); - - if ( - normalizedPath.length === 0 || - normalizedPath.includes("\0") || - normalizedPath.startsWith("/") - ) { + try { + const normalizedPath = normalizeLibraryFilePath(value); + return normalizedPath === value ? normalizedPath : null; + } catch { return null; } - - const segments: string[] = []; - - for (const segment of normalizedPath.split("/")) { - if (segment.length === 0 || segment === ".") { - continue; - } - - if (segment === "..") { - return null; - } - - segments.push(segment); - } - - return segments.length === 0 ? null : segments.join("/"); } export function toRuntimeSessionOutputArtifactPath(relativePath: string): string { @@ -91,18 +77,16 @@ export function toRuntimeSessionOutputFile(input: { readonly path: string; }): RuntimeSessionOutputFile | null { const outputDir = getRuntimeSessionOutputDirectory(input.cwd); - const normalizedPath = normalizeSandboxPath(input.path); + const normalizedPath = input.path; let relativePath: string | null; if (normalizedPath.startsWith("/")) { - const normalizedOutputDir = normalizeSandboxPath(outputDir); - - if (!normalizedPath.startsWith(`${normalizedOutputDir}/`)) { + if (!normalizedPath.startsWith(`${outputDir}/`)) { return null; } relativePath = normalizeRuntimeSessionOutputRelativePath( - normalizedPath.slice(normalizedOutputDir.length + 1), + normalizedPath.slice(outputDir.length + 1), ); } else { const normalizedRelativePath = normalizeRuntimeSessionOutputRelativePath(normalizedPath); @@ -140,24 +124,41 @@ export function toRuntimeSessionOutputFile(input: { }; } -export function readRuntimeSessionOutputListing(stdout: string): string[] { +export function readRuntimeSessionOutputInventory( + stdout: string, +): RuntimeSessionOutputInventoryFile[] { + const files: RuntimeSessionOutputInventoryFile[] = []; const seen = new Set(); - const paths: string[] = []; + const fields = stdout.split("\0"); - for (const line of stdout.split("\n")) { - const normalizedPath = normalizeRuntimeSessionOutputRelativePath(line); + if (fields.at(-1) === "") { + fields.pop(); + } + if (fields.length % 2 !== 0) { + throw new Error("Runtime output inventory is invalid."); + } - if (normalizedPath === null || seen.has(normalizedPath)) { - continue; + for (let index = 0; index < fields.length; index += 2) { + const inventoryPath = fields[index]; + const relativePath = inventoryPath?.startsWith("./") + ? normalizeRuntimeSessionOutputRelativePath(inventoryPath.slice(2)) + : null; + const sizeText = fields[index + 1]; + const size = Number(sizeText); + + if ( + relativePath === null || + seen.has(relativePath) || + !/^\d+$/.test(sizeText ?? "") || + !Number.isSafeInteger(size) || + size < 0 + ) { + throw new Error("Runtime output inventory is invalid."); } - seen.add(normalizedPath); - paths.push(normalizedPath); - - if (paths.length >= RUNTIME_SESSION_OUTPUT_SCAN_MAX_FILES) { - break; - } + seen.add(relativePath); + files.push({ relativePath, size }); } - return paths; + return files; } diff --git a/apps/api/src/modules/runtime/infrastructure/driver-instance/runtime-state-store.ts b/apps/api/src/modules/runtime/infrastructure/driver-instance/runtime-state-store.ts index 12ab0f27..1bfbdae2 100644 --- a/apps/api/src/modules/runtime/infrastructure/driver-instance/runtime-state-store.ts +++ b/apps/api/src/modules/runtime/infrastructure/driver-instance/runtime-state-store.ts @@ -6,14 +6,16 @@ import { import type { DriverHeartbeatInput, DriverHelloInput, + DriverHelloOutput, DriverReadyInput, } from "@mosoo/agent-driver/orpc"; import { parseRuntimeCommand } from "@mosoo/contracts/runtime-command"; import type { RuntimeCommand } from "@mosoo/contracts/runtime-command"; import { parsePlatformId } from "@mosoo/id"; -import type { DriverInstanceId } from "@mosoo/id"; +import type { DriverInstanceId, SessionRunId } from "@mosoo/id"; -import type { DriverInstanceCloseSnapshot } from "./state"; +import { parseDriverHelloOutput } from "./rpc-wire"; +import type { DriverInstanceCloseSnapshot, DriverInstanceConnectionEpoch } from "./state"; export const HEARTBEAT_STATE_PERSIST_INTERVAL_MS = 10_000; export const DRIVER_INSTANCE_STATE_STORAGE_KEY = "driverInstanceState"; @@ -28,11 +30,27 @@ export interface DriverInstanceStoredState { errorMessage: string | null; heartbeatCount: number; hello: DriverHelloInput | null; + helloOutput: DriverHelloOutput | null; lastHeartbeat: DriverHeartbeatInput | null; + pendingHello: DriverInstancePendingHello | null; + pendingReady: DriverInstancePendingReady | null; ready: DriverReadyInput | null; + terminalCleanupComplete: boolean; + terminalSessionRunId: SessionRunId | null; traceId: string | null; } +export interface DriverInstancePendingHello { + epoch: DriverInstanceConnectionEpoch; + input: DriverHelloInput; + output: DriverHelloOutput; +} + +export interface DriverInstancePendingReady { + epoch: DriverInstanceConnectionEpoch; + input: DriverReadyInput; +} + interface DriverInstanceRuntimeStorage { deleteAll(): Promise; get(key: string): Promise; @@ -72,8 +90,13 @@ export function parseStoredState(value: unknown): DriverInstanceStoredState { errorMessage: readNullableString(value, "errorMessage"), heartbeatCount: readRequiredNumber(value, "heartbeatCount"), hello: parseNullableHello(value["hello"]), + helloOutput: parseOptionalNullableHelloOutput(value["helloOutput"]), lastHeartbeat: parseNullableHeartbeat(value["lastHeartbeat"]), + pendingHello: parseOptionalPendingHello(value["pendingHello"]), + pendingReady: parseOptionalPendingReady(value["pendingReady"]), ready: parseNullableReady(value["ready"]), + terminalCleanupComplete: readOptionalBoolean(value, "terminalCleanupComplete", false), + terminalSessionRunId: readOptionalNullableSessionRunId(value, "terminalSessionRunId"), traceId: readNullableString(value, "traceId"), }; } @@ -89,12 +112,43 @@ export function createEmptyStoredState(): DriverInstanceStoredState { errorMessage: null, heartbeatCount: 0, hello: null, + helloOutput: null, lastHeartbeat: null, + pendingHello: null, + pendingReady: null, ready: null, + terminalCleanupComplete: false, + terminalSessionRunId: null, traceId: null, }; } +function readOptionalBoolean( + value: Record, + field: string, + defaultValue: boolean, +): boolean { + const entry = value[field]; + + if (entry === undefined) { + return defaultValue; + } + if (typeof entry !== "boolean") { + throw new TypeError(`Driver instance stored state ${field} must be a boolean.`); + } + + return entry; +} + +function readOptionalNullableSessionRunId( + value: Record, + field: string, +): SessionRunId | null { + const entry = value[field]; + + return entry === undefined || entry === null ? null : parsePlatformId(entry, field); +} + function isRecord(value: unknown): value is Record { return typeof value === "object" && value !== null && !Array.isArray(value); } @@ -188,6 +242,54 @@ function parseNullableHello(value: unknown): DriverHelloInput | null { return value === null ? null : parseDriverHelloInput(value); } +function parseOptionalNullableHelloOutput(value: unknown): DriverHelloOutput | null { + return value === undefined || value === null ? null : parseDriverHelloOutput(value); +} + +function parseConnectionEpoch(value: unknown): DriverInstanceConnectionEpoch { + if (!isRecord(value)) { + throw new TypeError("Driver instance connection epoch must be an object."); + } + + const connectionId = readRequiredString(value, "connectionId"); + const generation = readRequiredNumber(value, "generation"); + + if (connectionId.length === 0 || !Number.isSafeInteger(generation)) { + throw new TypeError("Driver instance connection epoch is invalid."); + } + + return { connectionId, generation }; +} + +function parseOptionalPendingHello(value: unknown): DriverInstancePendingHello | null { + if (value === undefined || value === null) { + return null; + } + if (!isRecord(value)) { + throw new TypeError("Pending Driver hello must be an object."); + } + + return { + epoch: parseConnectionEpoch(value["epoch"]), + input: parseDriverHelloInput(value["input"]), + output: parseDriverHelloOutput(value["output"]), + }; +} + +function parseOptionalPendingReady(value: unknown): DriverInstancePendingReady | null { + if (value === undefined || value === null) { + return null; + } + if (!isRecord(value)) { + throw new TypeError("Pending Driver ready must be an object."); + } + + return { + epoch: parseConnectionEpoch(value["epoch"]), + input: parseDriverReadyInput(value["input"]), + }; +} + function parseNullableReady(value: unknown): DriverReadyInput | null { if (value === null) { return null; diff --git a/apps/api/src/modules/runtime/infrastructure/driver-instance/runtime-state.ts b/apps/api/src/modules/runtime/infrastructure/driver-instance/runtime-state.ts index d500dc11..afe6cc0a 100644 --- a/apps/api/src/modules/runtime/infrastructure/driver-instance/runtime-state.ts +++ b/apps/api/src/modules/runtime/infrastructure/driver-instance/runtime-state.ts @@ -1,23 +1,16 @@ -import type { DriverEventEnvelope } from "@mosoo/agent-driver/events"; import type { - DriverEventReceipt, DriverHeartbeatInput, DriverHelloInput, + DriverHelloOutput, DriverReadyInput, } from "@mosoo/agent-driver/orpc"; import type { RuntimeCommand } from "@mosoo/contracts/runtime-command"; -import type { DriverInstanceId } from "@mosoo/id"; +import type { DriverInstanceId, SessionRunId } from "@mosoo/id"; import { isTruthy } from "../../../../shared/truthiness"; import type { DriverInstanceCommandState, RuntimeCommandWaiter } from "./commands"; import { createDriverDebugResumeSnapshot } from "./debug-resume-snapshot"; import type { DriverDebugRecoveryMode } from "./debug-resume-snapshot"; -import { - createReceiptsForDriverEvents, - filterNewDriverEvents, - readReceiptsForProcessedDriverEvents, - rememberDriverEventReceipts, -} from "./driver-event-receipts"; import { createDeferred, withTimeout } from "./driver-instance-support"; import type { Deferred } from "./driver-instance-support"; import type { RuntimeSessionLink } from "./event-types"; @@ -29,21 +22,33 @@ import { parseStoredState, } from "./runtime-state-store"; import type { + DriverInstancePendingHello, + DriverInstancePendingReady, DriverInstanceRuntimeStateContext, DriverInstanceStoredState, } from "./runtime-state-store"; import type { DriverInstanceCloseSnapshot, - DriverInstanceHeartbeatResult, - DriverInstanceHelloResult, + DriverInstanceConnectionEpoch, DriverInstanceReadyResult, DriverInstanceSnapshot, DriverInstanceWaitForCloseResult, - HeartbeatWaiter, } from "./state"; +interface GenerationWaiter { + deferred: Deferred; + generation: number; +} + +export type DriverInstanceHandshakeStageOutcome = "applied" | "replay" | "resume"; + +function isExactJson(left: unknown, right: unknown): boolean { + return JSON.stringify(left) === JSON.stringify(right); +} + interface DriverInstanceResetOptions { beforeReset: () => Promise; + driverGeneration: number; } export interface DriverInstanceHeartbeatRecord { @@ -52,25 +57,26 @@ export interface DriverInstanceHeartbeatRecord { export class DriverInstanceRuntimeState { close: DriverInstanceCloseSnapshot | null = null; - readonly closeWaiters: Deferred[] = []; + readonly closeWaiters: GenerationWaiter[] = []; commandQueue: RuntimeCommand[] = []; readonly commandWaiters: RuntimeCommandWaiter[] = []; connectedAt: number | null = null; connectionId: string | null = null; driverGeneration: number | null = null; driverInstanceId: DriverInstanceId | null = null; - driverEventReceiptSeq = 0; errorMessage: string | null = null; heartbeatCount = 0; - readonly heartbeatWaiters: HeartbeatWaiter[] = []; hello: DriverHelloInput | null = null; - readonly helloWaiters: Deferred[] = []; + helloOutput: DriverHelloOutput | null = null; lastHeartbeat: DriverHeartbeatInput | null = null; lastPersistedHeartbeatAtMs: number | null = null; - readonly processedDriverEventReceipts = new Map(); + pendingHello: DriverInstancePendingHello | null = null; + pendingReady: DriverInstancePendingReady | null = null; ready: DriverReadyInput | null = null; - readonly readyWaiters: Deferred[] = []; + readonly readyWaiters: GenerationWaiter[] = []; runtimeSessionLink: RuntimeSessionLink | null = null; + terminalCleanupComplete = false; + terminalSessionRunId: SessionRunId | null = null; terminalized = false; traceId: string | null = null; readonly #ctx: DriverInstanceRuntimeStateContext; @@ -86,17 +92,20 @@ export class DriverInstanceRuntimeState { this.connectionId = snapshot.connectionId; this.driverGeneration = snapshot.driverGeneration; this.driverInstanceId = snapshot.driverInstanceId; - this.driverEventReceiptSeq = 0; this.errorMessage = snapshot.errorMessage; this.heartbeatCount = snapshot.heartbeatCount; this.hello = snapshot.hello; + this.helloOutput = snapshot.helloOutput; this.lastHeartbeat = snapshot.lastHeartbeat; this.lastPersistedHeartbeatAtMs = snapshot.lastHeartbeat ? parseHeartbeatTimestampMs(snapshot.lastHeartbeat.at) : null; - this.processedDriverEventReceipts.clear(); + this.pendingHello = snapshot.pendingHello; + this.pendingReady = snapshot.pendingReady; this.ready = snapshot.ready ?? null; this.runtimeSessionLink = null; + this.terminalCleanupComplete = snapshot.terminalCleanupComplete; + this.terminalSessionRunId = snapshot.terminalSessionRunId; this.terminalized = snapshot.close !== null; this.traceId = snapshot.traceId; } @@ -116,8 +125,13 @@ export class DriverInstanceRuntimeState { errorMessage: this.errorMessage, heartbeatCount: this.heartbeatCount, hello: this.hello, + helloOutput: this.helloOutput, lastHeartbeat: this.lastHeartbeat, + pendingHello: this.pendingHello, + pendingReady: this.pendingReady, ready: this.ready, + terminalCleanupComplete: this.terminalCleanupComplete, + terminalSessionRunId: this.terminalSessionRunId, traceId: this.traceId, }; } @@ -136,19 +150,37 @@ export class DriverInstanceRuntimeState { ); } - async persistClose(close: DriverInstanceCloseSnapshot): Promise { + async persistClose( + close: DriverInstanceCloseSnapshot, + epoch: DriverInstanceConnectionEpoch, + ): Promise { + this.assertConnectionEpoch(epoch); + await this.#ctx.storage.put(DRIVER_INSTANCE_STATE_STORAGE_KEY, { + ...this.#toStoredState(), + close, + terminalCleanupComplete: false, + }); + this.assertConnectionEpoch(epoch); this.close = close; - await this.#persistState(); + this.terminalCleanupComplete = false; + this.terminalized = true; } async persistCommandQueue(): Promise { await this.#persistState(); } - async persistTerminalSnapshot(): Promise { + async persistTerminalSnapshot(epoch: DriverInstanceConnectionEpoch): Promise { + this.assertConnectionEpoch(epoch); this.requireDriverInstanceId(); + await this.#ctx.storage.put(DRIVER_INSTANCE_STATE_STORAGE_KEY, { + ...this.#toStoredState(), + commandQueue: [], + terminalCleanupComplete: true, + }); + this.assertConnectionEpoch(epoch); this.commandQueue = []; - await this.#persistState(); + this.terminalCleanupComplete = true; } async recordAcceptedConnection(input: { @@ -157,15 +189,30 @@ export class DriverInstanceRuntimeState { driverGeneration: number; traceId: string | null; }): Promise { - this.connectedAt = input.connectedAt; - this.connectionId = input.connectionId; - this.driverGeneration = input.driverGeneration; - - if (input.traceId !== null) { - this.traceId = input.traceId; - } + const isNewConnection = + this.connectionId !== input.connectionId || this.driverGeneration !== input.driverGeneration; + const next: DriverInstanceStoredState = { + ...this.#toStoredState(), + connectedAt: input.connectedAt, + connectionId: input.connectionId, + driverGeneration: input.driverGeneration, + errorMessage: null, + ...(isNewConnection + ? { + heartbeatCount: 0, + hello: null, + helloOutput: null, + lastHeartbeat: null, + pendingHello: null, + pendingReady: null, + ready: null, + } + : {}), + traceId: input.traceId ?? this.traceId, + }; - await this.#persistState(); + await this.#ctx.storage.put(DRIVER_INSTANCE_STATE_STORAGE_KEY, next); + this.#applyStoredState(next); } async recordHeartbeat(payload: DriverHeartbeatInput): Promise { @@ -179,71 +226,120 @@ export class DriverInstanceRuntimeState { this.lastPersistedHeartbeatAtMs = heartbeatAtMs; } - const result: DriverInstanceHeartbeatResult = { - heartbeat: payload, - heartbeatCount: this.heartbeatCount, - lastHeartbeatAt: payload.at, - }; - - for (const waiter of this.heartbeatWaiters.splice(0)) { - if (result.heartbeatCount > waiter.afterCount) { - waiter.deferred.resolve(result); - continue; - } - - this.heartbeatWaiters.push(waiter); - } - return { shouldPersistCanonical }; } - async recordHello(input: DriverHelloInput): Promise { - if (this.hello) { - throw new Error("Driver hello has already been received."); + async stageHello( + epoch: DriverInstanceConnectionEpoch, + input: DriverHelloInput, + output: DriverHelloOutput, + ): Promise { + this.assertConnectionEpoch(epoch); + + if (this.hello !== null || this.helloOutput !== null) { + if (isExactJson(this.hello, input) && isExactJson(this.helloOutput, output)) { + return "replay"; + } + throw new Error("Driver hello conflicts with the canonical receipt."); } - this.hello = input; - await this.#persistState(); + const pending = { epoch, input, output } satisfies DriverInstancePendingHello; - const result: DriverInstanceHelloResult = { - heartbeatCount: this.heartbeatCount, - hello: input, - lastHeartbeatAt: this.lastHeartbeat?.at ?? null, - }; + if (this.pendingHello !== null) { + if (isExactJson(this.pendingHello, pending)) { + return "resume"; + } + throw new Error("Driver hello conflicts with the pending receipt."); + } - return result; + await this.#ctx.storage.put(DRIVER_INSTANCE_STATE_STORAGE_KEY, { + ...this.#toStoredState(), + pendingHello: pending, + }); + this.assertConnectionEpoch(epoch); + this.pendingHello = pending; + return "applied"; } - async recordReady(input: DriverReadyInput): Promise { - if (this.ready) { - throw new Error("Driver ready has already been received."); + async commitHello(epoch: DriverInstanceConnectionEpoch): Promise { + this.assertConnectionEpoch(epoch); + const pending = this.pendingHello; + + if (pending === null || !isExactJson(pending.epoch, epoch)) { + throw new Error("Pending Driver hello receipt was lost."); } - this.ready = input; - await this.#persistState(); + await this.#ctx.storage.put(DRIVER_INSTANCE_STATE_STORAGE_KEY, { + ...this.#toStoredState(), + hello: pending.input, + helloOutput: pending.output, + pendingHello: null, + }); + this.assertConnectionEpoch(epoch); + this.hello = pending.input; + this.helloOutput = pending.output; + this.pendingHello = null; + return pending.output; + } + + async stageReady( + epoch: DriverInstanceConnectionEpoch, + input: DriverReadyInput, + ): Promise { + this.assertConnectionEpoch(epoch); + + if (this.ready !== null) { + if (isExactJson(this.ready, input)) { + return "replay"; + } + throw new Error("Driver ready conflicts with the canonical receipt."); + } - return { - heartbeatCount: this.heartbeatCount, - lastHeartbeatAt: this.lastHeartbeat?.at ?? null, - ready: input, - }; - } + const pending = { epoch, input } satisfies DriverInstancePendingReady; - rejectHeartbeatWaiters(error: Error): void { - for (const waiter of this.heartbeatWaiters.splice(0)) { - waiter.deferred.reject(error); + if (this.pendingReady !== null) { + if (isExactJson(this.pendingReady, pending)) { + return "resume"; + } + throw new Error("Driver ready conflicts with the pending receipt."); } + + await this.#ctx.storage.put(DRIVER_INSTANCE_STATE_STORAGE_KEY, { + ...this.#toStoredState(), + pendingReady: pending, + }); + this.assertConnectionEpoch(epoch); + this.pendingReady = pending; + return "applied"; } - rejectHelloWaiters(error: Error): void { - for (const waiter of this.helloWaiters.splice(0)) { - waiter.reject(error); + async commitReady(epoch: DriverInstanceConnectionEpoch): Promise { + this.assertConnectionEpoch(epoch); + const pending = this.pendingReady; + + if (pending === null || !isExactJson(pending.epoch, epoch)) { + throw new Error("Pending Driver ready receipt was lost."); } + + await this.#ctx.storage.put(DRIVER_INSTANCE_STATE_STORAGE_KEY, { + ...this.#toStoredState(), + pendingReady: null, + ready: pending.input, + }); + this.assertConnectionEpoch(epoch); + this.pendingReady = null; + this.ready = pending.input; + + return this.readyResult(); } - rejectReadyWaiters(error: Error): void { + rejectReadyWaiters(error: Error, generation: number): void { for (const waiter of this.readyWaiters.splice(0)) { - waiter.reject(error); + if (waiter.generation === generation) { + waiter.deferred.reject(error); + } else { + this.readyWaiters.push(waiter); + } } } @@ -271,12 +367,47 @@ export class DriverInstanceRuntimeState { return this.driverGeneration; } + connectionEpoch(): DriverInstanceConnectionEpoch | null { + return this.connectionId === null || this.driverGeneration === null + ? null + : { connectionId: this.connectionId, generation: this.driverGeneration }; + } + + requireConnectionEpoch(): DriverInstanceConnectionEpoch { + const epoch = this.connectionEpoch(); + + if (epoch === null) { + throw new Error("Driver connection epoch was not initialized."); + } + + return epoch; + } + + matchesConnectionEpoch(epoch: DriverInstanceConnectionEpoch): boolean { + return this.connectionId === epoch.connectionId && this.driverGeneration === epoch.generation; + } + + assertConnectionEpoch(epoch: DriverInstanceConnectionEpoch): void { + if (!this.matchesConnectionEpoch(epoch)) { + throw new Error("Driver connection is no longer current."); + } + } + async resetForReuse(options: DriverInstanceResetOptions): Promise { await options.beforeReset(); + const staleError = new Error("Driver generation is no longer current."); + for (const waiter of this.closeWaiters.splice(0)) { + waiter.deferred.reject(staleError); + } + for (const waiter of this.readyWaiters.splice(0)) { + waiter.deferred.reject(staleError); + } + const driverInstanceId = this.requireDriverInstanceId(); this.#applyStoredState({ ...createEmptyStoredState(), + driverGeneration: options.driverGeneration, driverInstanceId, }); await this.#ctx.storage.deleteAll(); @@ -287,49 +418,80 @@ export class DriverInstanceRuntimeState { const error = new Error(reason); for (const waiter of this.closeWaiters.splice(0)) { - waiter.reject(error); + waiter.deferred.reject(error); } for (const waiter of this.commandWaiters.splice(0)) { waiter.deferred.resolve(null); } - for (const waiter of this.helloWaiters.splice(0)) { - waiter.reject(error); - } - - for (const waiter of this.heartbeatWaiters.splice(0)) { - waiter.deferred.reject(error); - } - for (const waiter of this.readyWaiters.splice(0)) { - waiter.reject(error); + waiter.deferred.reject(error); } this.#applyStoredState(createEmptyStoredState()); } - resolveCloseWaiters(result: DriverInstanceWaitForCloseResult): void { + resolveCloseWaiters(result: DriverInstanceWaitForCloseResult, generation: number): void { for (const waiter of this.closeWaiters.splice(0)) { - waiter.resolve(result); - } - } - - resolveHelloWaiters(result: DriverInstanceHelloResult): void { - for (const waiter of this.helloWaiters.splice(0)) { - waiter.resolve(result); + if (waiter.generation === generation) { + waiter.deferred.resolve(result); + } else { + this.closeWaiters.push(waiter); + } } } - resolveReadyWaiters(result: DriverInstanceReadyResult): void { + resolveReadyWaiters(result: DriverInstanceReadyResult, generation: number): void { for (const waiter of this.readyWaiters.splice(0)) { - waiter.resolve(result); + if (waiter.generation === generation) { + waiter.deferred.resolve(result); + } else { + this.readyWaiters.push(waiter); + } } } - async setDriverInstanceId(driverInstanceId: DriverInstanceId): Promise { + async initializeDriverInstance( + driverInstanceId: DriverInstanceId, + driverGeneration: number, + ): Promise { + await this.#ctx.storage.put(DRIVER_INSTANCE_STATE_STORAGE_KEY, { + ...this.#toStoredState(), + driverGeneration, + driverInstanceId, + }); + this.driverGeneration = driverGeneration; this.driverInstanceId = driverInstanceId; - await this.#persistState(); + } + + async setDriverGeneration(driverGeneration: number): Promise { + await this.#ctx.storage.put(DRIVER_INSTANCE_STATE_STORAGE_KEY, { + ...this.#toStoredState(), + driverGeneration, + }); + this.driverGeneration = driverGeneration; + } + + async setTerminalSessionRunId( + sessionRunId: SessionRunId, + epoch?: DriverInstanceConnectionEpoch, + ): Promise { + if (epoch !== undefined) { + this.assertConnectionEpoch(epoch); + } + if (this.terminalSessionRunId !== null && this.terminalSessionRunId !== sessionRunId) { + throw new Error("Terminal Session Run identity is already fixed."); + } + + await this.#ctx.storage.put(DRIVER_INSTANCE_STATE_STORAGE_KEY, { + ...this.#toStoredState(), + terminalSessionRunId: sessionRunId, + }); + if (epoch !== undefined) { + this.assertConnectionEpoch(epoch); + } + this.terminalSessionRunId = sessionRunId; } async setErrorMessage(message: string): Promise { @@ -339,9 +501,26 @@ export class DriverInstanceRuntimeState { this.errorMessage = message; await this.#persistState(); - this.rejectHelloWaiters(new Error(message)); - this.rejectHeartbeatWaiters(new Error(message)); - this.rejectReadyWaiters(new Error(message)); + this.rejectReadyWaiters(new Error(message), this.requireDriverGeneration()); + } + + async setConnectionErrorMessage( + epoch: DriverInstanceConnectionEpoch, + message: string, + ): Promise { + this.assertConnectionEpoch(epoch); + + if (isTruthy(this.errorMessage)) { + return; + } + + await this.#ctx.storage.put(DRIVER_INSTANCE_STATE_STORAGE_KEY, { + ...this.#toStoredState(), + errorMessage: message, + }); + this.assertConnectionEpoch(epoch); + this.errorMessage = message; + this.rejectReadyWaiters(new Error(message), epoch.generation); } setRuntimeSessionLink(link: RuntimeSessionLink): void { @@ -355,43 +534,19 @@ export class DriverInstanceRuntimeState { ); } - filterUnprocessedDriverEvents(events: readonly DriverEventEnvelope[]): DriverEventEnvelope[] { - return filterNewDriverEvents({ - events, - processedReceipts: this.processedDriverEventReceipts, - }); - } - - createDriverEventReceipts(events: readonly DriverEventEnvelope[]): DriverEventReceipt[] { - const result = createReceiptsForDriverEvents({ - events, - nextSeq: this.driverEventReceiptSeq, - }); - this.driverEventReceiptSeq = result.nextSeq; - return result.receipts; - } - - readProcessedDriverEventReceipts(events: readonly DriverEventEnvelope[]): DriverEventReceipt[] { - return readReceiptsForProcessedDriverEvents({ - events, - processedReceipts: this.processedDriverEventReceipts, - }); - } - - rememberProcessedDriverEventReceipts(receipts: DriverEventReceipt[]): void { - rememberDriverEventReceipts({ - processedReceipts: this.processedDriverEventReceipts, - receipts, - }); - } - - async setTraceId(traceId: string): Promise { + async setTraceId(traceId: string, epoch?: DriverInstanceConnectionEpoch): Promise { + if (epoch !== undefined) { + this.assertConnectionEpoch(epoch); + } if (this.traceId === traceId) { return; } this.traceId = traceId; await this.#persistState(); + if (epoch !== undefined) { + this.assertConnectionEpoch(epoch); + } } snapshot(driverSocketConnected: boolean): DriverInstanceSnapshot { @@ -399,7 +554,6 @@ export class DriverInstanceRuntimeState { return { close: this.close, debugResume: createDriverDebugResumeSnapshot({ - lastEventSeq: this.driverEventReceiptSeq, recoveryMode, sandboxId: this.runtimeSessionLink?.sandboxId ?? null, }), @@ -426,103 +580,88 @@ export class DriverInstanceRuntimeState { return "fresh"; } - async waitForClose(timeoutMs: number): Promise { + async waitForClose( + generation: number, + timeoutMs: number, + ): Promise { + this.assertGeneration(generation); + if (this.close) { return this.closeResult(); } - const deferred = createDeferred(); - this.closeWaiters.push(deferred); - return withTimeout( - deferred.promise, - timeoutMs, - `Driver instance ${this.requireDriverInstanceId()} close`, - ); + const waiter: GenerationWaiter = { + deferred: createDeferred(), + generation, + }; + this.closeWaiters.push(waiter); + try { + return await withTimeout( + waiter.deferred.promise, + timeoutMs, + `Driver instance ${this.requireDriverInstanceId()} close`, + ); + } finally { + this.#removeWaiter(this.closeWaiters, waiter); + } } - async waitForHeartbeat( - afterCount: number, - timeoutMs: number, - ): Promise { - if (this.lastHeartbeat && this.heartbeatCount > afterCount) { - return { - heartbeat: this.lastHeartbeat, - heartbeatCount: this.heartbeatCount, - lastHeartbeatAt: this.lastHeartbeat.at, - }; - } + async waitForReady(generation: number, timeoutMs: number): Promise { + this.assertGeneration(generation); if (isTruthy(this.errorMessage)) { throw new Error(this.errorMessage); } if (this.close) { - throw new Error(`Driver instance ${this.requireDriverInstanceId()} is already closed.`); + throw new Error(`Driver instance ${this.requireDriverInstanceId()} closed before ready.`); + } + + if (this.ready) { + return this.readyResult(); } - const waiter: HeartbeatWaiter = { - afterCount, - deferred: createDeferred(), + const waiter: GenerationWaiter = { + deferred: createDeferred(), + generation, }; - this.heartbeatWaiters.push(waiter); - return withTimeout( - waiter.deferred.promise, - timeoutMs, - `Driver instance ${this.requireDriverInstanceId()} heartbeat`, - ); + this.readyWaiters.push(waiter); + try { + return await withTimeout( + waiter.deferred.promise, + timeoutMs, + `Driver instance ${this.requireDriverInstanceId()} ready`, + ); + } finally { + this.#removeWaiter(this.readyWaiters, waiter); + } } - async waitForHello(timeoutMs: number): Promise { - if (this.hello) { - return { - heartbeatCount: this.heartbeatCount, - hello: this.hello, - lastHeartbeatAt: this.lastHeartbeat?.at ?? null, - }; - } + #removeWaiter(waiters: GenerationWaiter[], waiter: GenerationWaiter): void { + const index = waiters.indexOf(waiter); - if (isTruthy(this.errorMessage)) { - throw new Error(this.errorMessage); + if (index !== -1) { + waiters.splice(index, 1); } - - if (this.close) { - throw new Error(`Driver instance ${this.requireDriverInstanceId()} closed before hello.`); - } - - const deferred = createDeferred(); - this.helloWaiters.push(deferred); - return withTimeout( - deferred.promise, - timeoutMs, - `Driver instance ${this.requireDriverInstanceId()} hello`, - ); } - async waitForReady(timeoutMs: number): Promise { - if (isTruthy(this.errorMessage)) { - throw new Error(this.errorMessage); + assertGeneration(generation: number): void { + if (this.requireDriverGeneration() !== generation) { + throw new Error("Driver generation is no longer current."); } + } - if (this.close) { - throw new Error(`Driver instance ${this.requireDriverInstanceId()} closed before ready.`); + readyResult(): DriverInstanceReadyResult { + if (this.ready === null) { + throw new Error(`Driver instance ${this.requireDriverInstanceId()} is not ready yet.`); } - if (this.ready) { - return { - heartbeatCount: this.heartbeatCount, - lastHeartbeatAt: this.lastHeartbeat?.at ?? null, - ready: this.ready, - }; - } - - const deferred = createDeferred(); - this.readyWaiters.push(deferred); - return withTimeout( - deferred.promise, - timeoutMs, - `Driver instance ${this.requireDriverInstanceId()} ready`, - ); + return { + heartbeatCount: this.heartbeatCount, + lastHeartbeatAt: this.lastHeartbeat?.at ?? null, + ready: this.ready, + }; } closeResult(): DriverInstanceWaitForCloseResult { diff --git a/apps/api/src/modules/runtime/infrastructure/driver-instance/sandbox-binding.ts b/apps/api/src/modules/runtime/infrastructure/driver-instance/sandbox-binding.ts deleted file mode 100644 index f8dafe04..00000000 --- a/apps/api/src/modules/runtime/infrastructure/driver-instance/sandbox-binding.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { requireCloudflareSandboxBinding } from "../../../../platform/cloudflare/sandbox-binding"; -import type { ApiBindings } from "../../../../platform/cloudflare/worker-types"; - -export function requireSandboxBinding(env: ApiBindings) { - return requireCloudflareSandboxBinding(env); -} diff --git a/apps/api/src/modules/runtime/infrastructure/driver-instance/session-link.repository.ts b/apps/api/src/modules/runtime/infrastructure/driver-instance/session-link.repository.ts index e3437ff4..34d8f922 100644 --- a/apps/api/src/modules/runtime/infrastructure/driver-instance/session-link.repository.ts +++ b/apps/api/src/modules/runtime/infrastructure/driver-instance/session-link.repository.ts @@ -21,7 +21,7 @@ import type { SessionRunId, } from "@mosoo/id"; import { parsePlatformId } from "@mosoo/id"; -import { and, eq, inArray, sql } from "drizzle-orm"; +import { and, desc, eq, inArray, sql } from "drizzle-orm"; import { getAppDatabase } from "../../../../platform/db/drizzle"; import { ACTIVE_SESSION_RUN_STATUSES } from "../../domain/session-run-lifecycle.machine"; @@ -35,6 +35,7 @@ interface RuntimeSessionLinkRow { caller_account_id: AccountId | null; creator_account_id: PlatformId | null; origin_json: string | null; + runtime_id: string | null; sandbox_id: SandboxId | null; sandbox_kind: AgentKind | null; sandbox_subject_kind: SandboxSubjectKind | null; @@ -46,6 +47,7 @@ interface RuntimeSessionLinkRow { } export interface GetRuntimeSessionLinkOptions { + latestTerminalRun?: boolean; sessionRunId?: SessionRunId; } @@ -82,14 +84,19 @@ export async function getRuntimeSessionLink( ): Promise { const linkedSessionId = sql`coalesce(${sessionRunsTable.sessionId}, ${sandboxSessionsTable.sessionId})`; const linkedSessionRun = - options.sessionRunId === undefined + options.sessionRunId !== undefined ? and( eq(sessionRunsTable.driverInstanceId, driverInstancesTable.id), - inArray(sessionRunsTable.status, ACTIVE_SESSION_RUN_STATUSES), + eq(sessionRunsTable.id, options.sessionRunId), ) : and( eq(sessionRunsTable.driverInstanceId, driverInstancesTable.id), - eq(sessionRunsTable.id, options.sessionRunId), + inArray( + sessionRunsTable.status, + options.latestTerminalRun === true + ? ["cancelled", "completed", "expired", "failed"] + : ACTIVE_SESSION_RUN_STATUSES, + ), ); const row = (await getAppDatabase(database) @@ -100,6 +107,9 @@ export async function getRuntimeSessionLink( caller_account_id: sessionRunsTable.createdByAccountId, creator_account_id: sessionsTable.creatorAccountId, origin_json: sandboxSessionsTable.originJson, + runtime_id: sql< + string | null + >`coalesce(${sessionRunsTable.runtimeId}, ${sessionsTable.runtimeId})`, sandbox_id: driverInstancesTable.sandboxId, sandbox_kind: sandboxesTable.kind, sandbox_subject_kind: sandboxesTable.subjectKind, @@ -119,6 +129,11 @@ export async function getRuntimeSessionLink( .leftJoin(agentsTable, eq(agentsTable.id, sessionsTable.agentId)) .leftJoin(sandboxesTable, eq(sandboxesTable.id, driverInstancesTable.sandboxId)) .where(eq(driverInstancesTable.id, driverInstanceId)) + .orderBy( + desc(sessionRunsTable.updatedAt), + desc(sessionRunsTable.createdAt), + desc(sessionRunsTable.id), + ) .limit(1) .get()) ?? null; const principals = resolveRuntimeSessionPrincipalIds(row ?? null); @@ -132,6 +147,7 @@ export async function getRuntimeSessionLink( sandboxId: row?.sandbox_id ?? null, sandboxKind: row?.sandbox_kind ?? null, sandboxSubjectKind: row?.sandbox_subject_kind ?? null, + runtimeId: row?.runtime_id ?? null, sessionId: row?.session_id ?? null, sessionRunId: row?.session_run_id ?? null, sessionRunStatus: row?.session_run_status ?? null, @@ -139,3 +155,31 @@ export async function getRuntimeSessionLink( traceId: row?.trace_id ?? null, }; } + +export async function assertActiveRuntimeSessionRun( + database: D1Database, + input: { + driverInstanceId: DriverInstanceId; + sessionId: SessionId; + sessionRunId: SessionRunId; + }, +): Promise { + const row = + (await getAppDatabase(database) + .select({ id: sessionRunsTable.id }) + .from(sessionRunsTable) + .where( + and( + eq(sessionRunsTable.id, input.sessionRunId), + eq(sessionRunsTable.sessionId, input.sessionId), + eq(sessionRunsTable.driverInstanceId, input.driverInstanceId), + inArray(sessionRunsTable.status, ACTIVE_SESSION_RUN_STATUSES), + ), + ) + .limit(1) + .get()) ?? null; + + if (row === null) { + throw new Error("Fresh runtime driver event requires its exact active Session Run."); + } +} diff --git a/apps/api/src/modules/runtime/infrastructure/driver-instance/session-viewer-event-delivery-buffer.ts b/apps/api/src/modules/runtime/infrastructure/driver-instance/session-viewer-event-delivery-buffer.ts index 485bf42b..de31b9a8 100644 --- a/apps/api/src/modules/runtime/infrastructure/driver-instance/session-viewer-event-delivery-buffer.ts +++ b/apps/api/src/modules/runtime/infrastructure/driver-instance/session-viewer-event-delivery-buffer.ts @@ -10,7 +10,10 @@ import type { DriverInstanceId, SessionId } from "@mosoo/id"; import { createErrorLogContext, logError } from "../../../../platform/cloudflare/logger"; import type { ApiBindings } from "../../../../platform/cloudflare/worker-types"; import type { SessionDeliveryEvent } from "../../../sessions/application/session-live-state.service"; -import { publishSessionViewerEvents } from "../../../sessions/application/session-viewer-events.service"; +import { + publishSessionViewerEventBatches, + syncSessionViewerState, +} from "../../../sessions/infrastructure/session/client"; const SESSION_VIEWER_EVENT_DELIVERY_FLUSH_MS = 150; const SESSION_VIEWER_EVENT_DELIVERY_MAX_DELTA_BYTES = 4 * 1024; @@ -21,6 +24,9 @@ const sessionViewerEventEncoder = new TextEncoder(); interface BufferedSessionViewerEvents { deltaBytes: number; events: SessionDeliveryEvent[]; + previousRuntimeEventSeqCursor: number | null; + requiresStateSync: boolean; + runtimeEventSeqCursor: number | null; sessionId: SessionId | null; } @@ -56,6 +62,7 @@ export class SessionViewerEventDeliveryBuffer { readonly #env: ApiBindings; readonly #getDriverInstanceId: () => DriverInstanceId | null; #timer: ReturnType | null = null; + #timerDone: (() => void) | null = null; readonly #withRuntimeLogContext: (fn: () => T) => T; constructor(options: SessionViewerEventDeliveryBufferOptions) { @@ -65,15 +72,68 @@ export class SessionViewerEventDeliveryBuffer { this.#withRuntimeLogContext = options.withRuntimeLogContext; } - enqueue(sessionId: SessionId | null, events: SessionDeliveryEvent[]): void { + enqueue( + sessionId: SessionId | null, + events: SessionDeliveryEvent[], + runtimeEventSeqCursor: number | null = null, + previousRuntimeEventSeqCursor: number | null = null, + ): void { const compactedEvents = compactAgUiSessionEvents(events); if (compactedEvents.length === 0) { return; } + if (runtimeEventSeqCursor !== null) { + this.#queueBuffer(); + this.#pendingBatches.push({ + deltaBytes: compactedEvents.reduce( + (total, event) => total + getAgUiSessionEventDeltaLength(event), + 0, + ), + events: + measureSerializedBytes(compactedEvents) <= + SESSION_VIEWER_EVENT_DELIVERY_MAX_SERIALIZED_BYTES + ? compactedEvents + : [], + previousRuntimeEventSeqCursor, + requiresStateSync: + previousRuntimeEventSeqCursor === null || + measureSerializedBytes(compactedEvents) > + SESSION_VIEWER_EVENT_DELIVERY_MAX_SERIALIZED_BYTES, + runtimeEventSeqCursor, + sessionId, + }); + + if (this.#delivery !== null) { + this.#replacePendingWithStateSync(sessionId, runtimeEventSeqCursor); + } + + if (hasRunStartedEvent(compactedEvents)) { + this.#pendingFirstDelta = true; + } + const isFirstDeltaOfRun = this.#pendingFirstDelta && hasDeltaEvent(compactedEvents); + if (isFirstDeltaOfRun) { + this.#pendingFirstDelta = false; + } + if (isFirstDeltaOfRun || hasTerminalEvent(compactedEvents)) { + void this.#startFlush(); + } else { + this.#scheduleFlush(); + } + return; + } + + if ( + this.#buffer !== null && + (this.#buffer.sessionId !== sessionId || + this.#buffer.runtimeEventSeqCursor !== runtimeEventSeqCursor) + ) { + this.#queueBuffer(); + } + for (const event of compactedEvents) { - this.#appendEvent(sessionId, event); + this.#appendEvent(sessionId, event, runtimeEventSeqCursor); } // O2: cut first-token latency. Arm on RUN_STARTED, then flush the first @@ -89,10 +149,14 @@ export class SessionViewerEventDeliveryBuffer { } if (isFirstDeltaOfRun || hasTerminalEvent(compactedEvents)) { - this.#startFlush(); + void this.#startFlush(); return; } + if (this.#delivery !== null) { + this.#replacePendingWithStateSync(sessionId, runtimeEventSeqCursor); + } + if (this.#buffer) { this.#scheduleFlush(); } @@ -107,16 +171,49 @@ export class SessionViewerEventDeliveryBuffer { async #deliverPendingBatches(): Promise { try { while (this.#pendingBatches.length > 0) { - const batch = this.#pendingBatches.shift(); + const first = this.#pendingBatches.shift(); - if (!batch) { + if (!first) { return; } + if (first.requiresStateSync) { + try { + await syncSessionViewerState(this.#env, first.sessionId); + } catch (error) { + this.#pendingBatches.unshift(first); + throw error; + } + continue; + } + const batches = [first]; + let serializedBytes = measureSerializedBytes(first.events); + for (;;) { + const next = this.#pendingBatches[0]; + if ( + next === undefined || + next.requiresStateSync || + next.sessionId !== first.sessionId || + serializedBytes + measureSerializedBytes(next.events) > + SESSION_VIEWER_EVENT_DELIVERY_MAX_SERIALIZED_BYTES + ) { + break; + } + batches.push(this.#pendingBatches.shift()!); + serializedBytes += measureSerializedBytes(next.events); + } try { - await publishSessionViewerEvents(this.#env, batch.sessionId, batch.events); + await publishSessionViewerEventBatches( + this.#env, + first.sessionId, + batches.map((batch) => ({ + events: batch.events, + previousRuntimeEventSeqCursor: batch.previousRuntimeEventSeqCursor, + runtimeEventSeqCursor: batch.runtimeEventSeqCursor, + })), + ); } catch (error) { - this.#pendingBatches.unshift(batch); + this.#pendingBatches.unshift(...batches); throw error; } } @@ -130,13 +227,15 @@ export class SessionViewerEventDeliveryBuffer { await this.flush(); } catch (error) { this.#reportDeliveryError(error); - - if (this.#hasBufferedEvents() && this.#timer === null) { - this.#scheduleFlush(); - } + this.resetAfterFlush(); } } + requestStateSync(sessionId: SessionId | null): void { + this.#replacePendingWithStateSync(sessionId, null); + void this.#startFlush(); + } + resetAfterFlush(): void { this.#buffer = null; this.#pendingFirstDelta = false; @@ -145,13 +244,17 @@ export class SessionViewerEventDeliveryBuffer { this.#clearTimer(); } - #appendEvent(sessionId: SessionId | null, event: SessionDeliveryEvent): void { + #appendEvent( + sessionId: SessionId | null, + event: SessionDeliveryEvent, + runtimeEventSeqCursor: number | null, + ): void { let buffered = this.#buffer; let events = buffered ? appendCompactedAgUiSessionEvents(buffered.events, [event]) : [event]; let serializedBytes = measureSerializedBytes(events); if (buffered && serializedBytes > SESSION_VIEWER_EVENT_DELIVERY_MAX_SERIALIZED_BYTES) { - this.#startFlush(); + void this.#startFlush(); buffered = null; events = [event]; serializedBytes = measureSerializedBytes(events); @@ -160,6 +263,9 @@ export class SessionViewerEventDeliveryBuffer { this.#buffer = { deltaBytes: (buffered?.deltaBytes ?? 0) + getAgUiSessionEventDeltaLength(event), events, + previousRuntimeEventSeqCursor: null, + requiresStateSync: false, + runtimeEventSeqCursor: buffered?.runtimeEventSeqCursor ?? runtimeEventSeqCursor, sessionId: buffered?.sessionId ?? sessionId, }; @@ -168,7 +274,7 @@ export class SessionViewerEventDeliveryBuffer { this.#buffer.deltaBytes >= SESSION_VIEWER_EVENT_DELIVERY_MAX_DELTA_BYTES || serializedBytes >= SESSION_VIEWER_EVENT_DELIVERY_MAX_SERIALIZED_BYTES ) { - this.#startFlush(); + void this.#startFlush(); } } @@ -177,6 +283,8 @@ export class SessionViewerEventDeliveryBuffer { clearTimeout(this.#timer); this.#timer = null; } + this.#timerDone?.(); + this.#timerDone = null; } #getOrStartDelivery(): Promise { @@ -203,21 +311,80 @@ export class SessionViewerEventDeliveryBuffer { const queued = this.#pendingBatches.splice(0); let events: SessionDeliveryEvent[] = []; let sessionId = queued[0]?.sessionId ?? null; + let previousRuntimeEventSeqCursor = queued[0]?.previousRuntimeEventSeqCursor ?? null; + let requiresStateSync = queued[0]?.requiresStateSync ?? false; + let runtimeEventSeqCursor = queued[0]?.runtimeEventSeqCursor ?? null; for (const batch of queued) { - if (batch.sessionId !== sessionId) { - this.#queueCompactedEvents(sessionId, events); + if ( + batch.sessionId !== sessionId || + batch.runtimeEventSeqCursor !== runtimeEventSeqCursor || + batch.previousRuntimeEventSeqCursor !== previousRuntimeEventSeqCursor || + batch.requiresStateSync !== requiresStateSync + ) { + this.#queueCompactedEvents( + sessionId, + events, + runtimeEventSeqCursor, + previousRuntimeEventSeqCursor, + requiresStateSync, + ); events = []; sessionId = batch.sessionId; + previousRuntimeEventSeqCursor = batch.previousRuntimeEventSeqCursor; + requiresStateSync = batch.requiresStateSync; + runtimeEventSeqCursor = batch.runtimeEventSeqCursor; } events = appendCompactedAgUiSessionEvents(events, batch.events); } - this.#queueCompactedEvents(sessionId, events); + this.#queueCompactedEvents( + sessionId, + events, + runtimeEventSeqCursor, + previousRuntimeEventSeqCursor, + requiresStateSync, + ); } - #queueCompactedEvents(sessionId: SessionId | null, events: SessionDeliveryEvent[]): void { + #queueCompactedEvents( + sessionId: SessionId | null, + events: SessionDeliveryEvent[], + runtimeEventSeqCursor: number | null, + previousRuntimeEventSeqCursor: number | null, + requiresStateSync: boolean, + ): void { + if (requiresStateSync) { + this.#pendingBatches.push({ + deltaBytes: 0, + events: [], + previousRuntimeEventSeqCursor: null, + requiresStateSync: true, + runtimeEventSeqCursor, + sessionId, + }); + return; + } + + if (runtimeEventSeqCursor !== null) { + const compactedEvents = compactAgUiSessionEvents(events); + const mustSync = + measureSerializedBytes(compactedEvents) > + SESSION_VIEWER_EVENT_DELIVERY_MAX_SERIALIZED_BYTES; + this.#pendingBatches.push({ + deltaBytes: compactedEvents.reduce( + (total, event) => total + getAgUiSessionEventDeltaLength(event), + 0, + ), + events: mustSync ? [] : compactedEvents, + previousRuntimeEventSeqCursor, + requiresStateSync: mustSync, + runtimeEventSeqCursor, + sessionId, + }); + return; + } for (const event of events) { const previous = this.#pendingBatches.at(-1); const nextEvents = previous @@ -228,6 +395,8 @@ export class SessionViewerEventDeliveryBuffer { if ( previous && previous.sessionId === sessionId && + !previous.requiresStateSync && + previous.runtimeEventSeqCursor === runtimeEventSeqCursor && nextEvents.length <= SESSION_VIEWER_EVENT_DELIVERY_MAX_EVENTS && nextDeltaBytes <= SESSION_VIEWER_EVENT_DELIVERY_MAX_DELTA_BYTES && measureSerializedBytes(nextEvents) <= SESSION_VIEWER_EVENT_DELIVERY_MAX_SERIALIZED_BYTES @@ -240,15 +409,14 @@ export class SessionViewerEventDeliveryBuffer { this.#pendingBatches.push({ deltaBytes: getAgUiSessionEventDeltaLength(event), events: [event], + previousRuntimeEventSeqCursor, + requiresStateSync: false, + runtimeEventSeqCursor, sessionId, }); } } - #hasBufferedEvents(): boolean { - return this.#buffer !== null || this.#pendingBatches.length > 0; - } - #queueBuffer(): void { if (!this.#buffer) { return; @@ -258,45 +426,56 @@ export class SessionViewerEventDeliveryBuffer { this.#buffer = null; } + #replacePendingWithStateSync( + sessionId: SessionId | null, + runtimeEventSeqCursor: number | null, + ): void { + this.#buffer = null; + this.#pendingBatches.length = 0; + this.#clearTimer(); + this.#pendingBatches.push({ + deltaBytes: 0, + events: [], + previousRuntimeEventSeqCursor: null, + requiresStateSync: true, + runtimeEventSeqCursor, + sessionId, + }); + } + #scheduleFlush(): void { if (this.#timer !== null) { return; } - this.#timer = setTimeout(() => { - this.#timer = null; - this.#startFlush(); - }, SESSION_VIEWER_EVENT_DELIVERY_FLUSH_MS); + const task = new Promise((resolve) => { + this.#timerDone = resolve; + this.#timer = setTimeout(() => { + this.#timer = null; + this.#timerDone = null; + void this.#startFlush().finally(resolve); + }, SESSION_VIEWER_EVENT_DELIVERY_FLUSH_MS); + }); + this.#ctx.waitUntil(task); } - #startFlush(): void { + #startFlush(): Promise { this.#clearTimer(); this.#queueBuffer(); if (this.#delivery) { this.#compactPendingBatches(); - return; + return this.#delivery; } if (this.#pendingBatches.length === 0) { - return; + return Promise.resolve(); } - const task = this.#flushAndReportDeliveryErrors(); + const task = this.flushSafely(); this.#ctx.waitUntil(task); - } - - async #flushAndReportDeliveryErrors(): Promise { - try { - await this.flush(); - } catch (error) { - this.#reportDeliveryError(error); - - if (this.#hasBufferedEvents() && this.#timer === null) { - this.#scheduleFlush(); - } - } + return task; } #reportDeliveryError(error: unknown): void { diff --git a/apps/api/src/modules/runtime/infrastructure/driver-instance/sockets.ts b/apps/api/src/modules/runtime/infrastructure/driver-instance/sockets.ts index 46848dbc..8c5d8de4 100644 --- a/apps/api/src/modules/runtime/infrastructure/driver-instance/sockets.ts +++ b/apps/api/src/modules/runtime/infrastructure/driver-instance/sockets.ts @@ -1,68 +1,102 @@ import { sleepPromise } from "@mosoo/effects"; +import type { DriverInstanceConnectionEpoch } from "./state"; + const DRIVER_SOCKET_TAG = "driver"; +function parseDriverSocketEpoch(socket: WebSocket): DriverInstanceConnectionEpoch | null { + const attachment: unknown = socket.deserializeAttachment(); + + if ( + typeof attachment !== "object" || + attachment === null || + !("connectionId" in attachment) || + !("generation" in attachment) || + typeof attachment.connectionId !== "string" || + attachment.connectionId.length === 0 || + !Number.isSafeInteger(attachment.generation) || + (attachment.generation as number) < 0 + ) { + return null; + } + + return { + connectionId: attachment.connectionId, + generation: attachment.generation as number, + }; +} + +function epochsMatch( + left: DriverInstanceConnectionEpoch, + right: DriverInstanceConnectionEpoch, +): boolean { + return left.connectionId === right.connectionId && left.generation === right.generation; +} + export class DriverInstanceSocketRegistry { readonly #ctx: DurableObjectState; - #activeDriverSocket: WebSocket | null = null; constructor(ctx: DurableObjectState) { this.#ctx = ctx; } - acceptDriverSocket(socket: WebSocket): void { - // Hibernation accept: the socket survives Durable Object eviction, and - // driver messages re-instantiate this object. Command delivery does not - // depend on in-memory waiters — the driver polls nextCommand, which - // claims from D1 — so waking into a fresh instance is safe. + acceptDriverSocket(socket: WebSocket, epoch: DriverInstanceConnectionEpoch): void { this.#ctx.acceptWebSocket(socket, [DRIVER_SOCKET_TAG]); - this.#activeDriverSocket = socket; + socket.serializeAttachment(epoch); } - getDriverSocket(): WebSocket | null { - if (this.#activeDriverSocket && this.#activeDriverSocket.readyState !== WebSocket.CLOSED) { - return this.#activeDriverSocket; - } - - const [socket] = this.#ctx.getWebSockets(DRIVER_SOCKET_TAG); - return socket ?? null; + getSocketEpoch(socket: WebSocket): DriverInstanceConnectionEpoch | null { + return parseDriverSocketEpoch(socket); } - isActiveDriverSocket(socket: WebSocket): boolean { - return this.getDriverSocket() === socket; + socketMatchesEpoch(socket: WebSocket, epoch: DriverInstanceConnectionEpoch): boolean { + const socketEpoch = parseDriverSocketEpoch(socket); + return socketEpoch !== null && epochsMatch(socketEpoch, epoch); } - /** - * True when a different, still-open driver socket has superseded this one. - * Close/error events from superseded sockets must not finalize the state - * that now belongs to the successor connection. - */ - isSupersededDriverSocket(socket: WebSocket): boolean { - const current = this.getDriverSocket(); - return current !== null && current !== socket; + isCurrentDriverSocket( + socket: WebSocket, + capturedEpoch: DriverInstanceConnectionEpoch, + currentEpoch: DriverInstanceConnectionEpoch | null, + ): boolean { + return ( + currentEpoch !== null && + epochsMatch(capturedEpoch, currentEpoch) && + this.socketMatchesEpoch(socket, capturedEpoch) + ); } - releaseDriverSocket(socket: WebSocket): void { - if (this.#activeDriverSocket === socket) { - this.#activeDriverSocket = null; + getDriverSocket(epoch: DriverInstanceConnectionEpoch | null): WebSocket | null { + if (epoch === null) { + return null; } + + return ( + this.#ctx + .getWebSockets(DRIVER_SOCKET_TAG) + .find( + (socket) => + socket.readyState === WebSocket.OPEN && this.socketMatchesEpoch(socket, epoch), + ) ?? null + ); } replaceDriverSockets(): void { - if (this.#activeDriverSocket && this.#activeDriverSocket.readyState !== WebSocket.CLOSED) { - this.#activeDriverSocket.close(1012, "runtime.socket.replaced"); - this.#activeDriverSocket = null; - } - - for (const existingSocket of this.#ctx.getWebSockets(DRIVER_SOCKET_TAG)) { - existingSocket.close(1012, "runtime.socket.replaced"); + for (const socket of this.#ctx.getWebSockets(DRIVER_SOCKET_TAG)) { + if (socket.readyState === WebSocket.OPEN) { + socket.close(1012, "runtime.socket.replaced"); + } } } - scheduleDriverSocketClose(code: number, reason: string): void { - const socket = this.getDriverSocket(); + scheduleDriverSocketClose( + epoch: DriverInstanceConnectionEpoch, + code: number, + reason: string, + ): void { + const socket = this.getDriverSocket(epoch); - if (!socket || socket.readyState !== WebSocket.OPEN) { + if (socket === null) { return; } diff --git a/apps/api/src/modules/runtime/infrastructure/driver-instance/state.ts b/apps/api/src/modules/runtime/infrastructure/driver-instance/state.ts index 866a3721..8ece68fa 100644 --- a/apps/api/src/modules/runtime/infrastructure/driver-instance/state.ts +++ b/apps/api/src/modules/runtime/infrastructure/driver-instance/state.ts @@ -1,11 +1,11 @@ -import type { - DriverHeartbeatInput, - DriverHelloInput, - DriverReadyInput, -} from "@mosoo/agent-driver/orpc"; +import type { DriverHelloInput, DriverReadyInput } from "@mosoo/agent-driver/orpc"; import type { DriverDebugResumeSnapshot } from "./debug-resume-snapshot"; -import type { Deferred } from "./driver-instance-support"; + +export interface DriverInstanceConnectionEpoch { + readonly connectionId: string; + readonly generation: number; +} export interface DriverInstanceCloseSnapshot { at: string; @@ -13,23 +13,6 @@ export interface DriverInstanceCloseSnapshot { reason: string; } -export interface DriverInstanceHeartbeatResult { - heartbeat: DriverHeartbeatInput; - heartbeatCount: number; - lastHeartbeatAt: string; -} - -export interface HeartbeatWaiter { - afterCount: number; - deferred: Deferred; -} - -export interface DriverInstanceHelloResult { - heartbeatCount: number; - hello: DriverHelloInput; - lastHeartbeatAt: string | null; -} - export interface DriverInstanceReadyResult { heartbeatCount: number; lastHeartbeatAt: string | null; diff --git a/apps/api/src/modules/runtime/infrastructure/driver-instance/terminal-driver-events.ts b/apps/api/src/modules/runtime/infrastructure/driver-instance/terminal-driver-events.ts index a5a336b9..e0fe8f48 100644 --- a/apps/api/src/modules/runtime/infrastructure/driver-instance/terminal-driver-events.ts +++ b/apps/api/src/modules/runtime/infrastructure/driver-instance/terminal-driver-events.ts @@ -1,218 +1,233 @@ import type { DriverFailureInput } from "@mosoo/agent-driver/orpc"; +import type { RunError, SessionRunSummary } from "@mosoo/contracts/session-run"; import { createPlatformId } from "@mosoo/id"; import type { DriverInstanceId, RuntimeEventId, SessionId, SessionRunId } from "@mosoo/id"; -import { createRuntimeEvent } from "@mosoo/runtime-events"; -import type { RuntimeEventEnvelope } from "@mosoo/runtime-events"; import type { ApiBindings } from "../../../../platform/cloudflare/worker-types"; -import { appendSessionRuntimeEvents } from "../../../sessions/application/session-event-write.service"; -import { appRuntimeEventToSessionDeliveryEvents } from "../../../sessions/application/session-live-state.service"; -import { recordCanonicalSessionRunFailure } from "../../application/session-runs/session-run-terminal-failure.service"; -import { isTerminalSessionRunStatus } from "../../domain/session-run-status"; -import { setSessionRunStatus } from "../session-runs/session-run-store.repository"; -import type { SessionRunTransitionOutcome } from "../session-runs/session-run-store.repository"; +import { currentTimestampMs, toIsoString } from "../../../../time"; +import { createSessionRunTerminalSourceId } from "../../domain/session-run-terminal-event-id"; +import { getSessionRunSummary } from "../session-runs/session-run-store.repository"; +import { + adoptTerminalRunProjection, + commitTerminalRunProjection, +} from "./completed-run-commit.repository"; +import type { DriverTerminalRunStatus } from "./completed-run-commit.repository"; +import { createCanonicalDriverRunFailedEvent } from "./driver-event-canonicalization"; import type { RuntimeSessionLink } from "./event-types"; +import { getDriverInstanceLifecycleIdentity } from "./lifecycle"; import { getRuntimeSessionLink } from "./session-link.repository"; import { releaseTerminalDriverInstanceSessionRun } from "./terminal-run-release"; -function assertTerminalDriverSessionRunTransition(outcome: SessionRunTransitionOutcome): void { - switch (outcome.kind) { - case "applied": - case "duplicate": { - return; - } - case "stale": { - if (outcome.reason === "terminal_run") { - return; - } - throw new Error("Terminal driver event lost a concurrent run transition."); - } - case "repair_needed": { - throw new Error("Terminal driver event left the session lifecycle projection stale."); - } - case "rejected": { - throw new Error(`Terminal driver event run transition was rejected: ${outcome.reason}.`); - } +function terminalTargetFromRun( + status: SessionRunSummary["status"], +): DriverTerminalRunStatus | null { + switch (status) { + case "cancelled": + return "cancelled"; + case "completed": + return "completed"; + case "failed": + return "failed"; + case "expired": + return "cancelled"; + case "booting": + case "queued": + case "running": + case "waiting_input": + return null; } } -function isStaleTerminalRunTransition(outcome: SessionRunTransitionOutcome): boolean { - return outcome.kind === "stale" && outcome.reason === "terminal_run"; -} - -function isStaleTerminalRunStatus( - outcome: SessionRunTransitionOutcome, - status: "completed" | "failed", -): boolean { - return ( - outcome.kind === "stale" && - outcome.reason === "terminal_run" && - outcome.currentStatus === status - ); -} - -function createTerminalDriverEventId(input: { - readonly driverInstanceId: DriverInstanceId; - readonly kind: "run.completed" | "run.failed"; - readonly sessionRunId: SessionRunId; -}): string { - return `driver-terminal:${input.driverInstanceId}:${input.sessionRunId}:${input.kind}`; -} - -export async function recordDriverInstanceCompletion( +async function commitOrAdoptTerminalRun( bindings: ApiBindings, input: { - driverReady: boolean; + driverConnectionId?: string; + driverGeneration?: number; driverInstanceId: DriverInstanceId; + error: RunError | null; + requestedStatus: "completed" | "failed"; + runtimeId: string; + sessionId: SessionId; + sessionRunId: SessionRunId; }, ): Promise { - void input.driverReady; - const database = bindings.DB; - const link = await getRuntimeSessionLink(database, input.driverInstanceId); + const current = await getSessionRunSummary(bindings.DB, input.sessionRunId); + if (current === null) { + throw new Error("Terminal Driver Session Run was not found."); + } - if ( - hasLinkedSessionRun(link) && - link.sessionRunStatus !== null && - (!isTerminalSessionRunStatus(link.sessionRunStatus) || link.sessionRunStatus === "completed") - ) { - await synthesizeDriverRunFinished(database, { - bindings, - driverInstanceId: input.driverInstanceId, - link, - }); + const currentTarget = terminalTargetFromRun(current.status); + const targetStatus = currentTarget ?? input.requestedStatus; + const expectedDriverObservation = + input.driverConnectionId === undefined || input.driverGeneration === undefined + ? undefined + : { + connectionId: input.driverConnectionId, + driverInstanceId: input.driverInstanceId, + generation: input.driverGeneration, + }; + const adopted = await adoptTerminalRunProjection(bindings.DB, { + ...(expectedDriverObservation === undefined ? {} : { expectedDriverObservation }), + expectedTargetStatus: targetStatus, + runId: input.sessionRunId, + sessionId: input.sessionId, + }); + if (adopted.kind !== "missing") { + if (adopted.kind === "stale") { + throw new Error( + `Terminal Driver Session Run lost a concurrent ${adopted.currentStatus} race.`, + ); + } + return; + } + + if (targetStatus !== "failed") { + throw new Error( + `${targetStatus === "completed" ? "Completed" : "Cancelled"} Session Run is missing its canonical terminal event.`, + ); } - await releaseLinkedRunLease(bindings, { + const runError = currentTarget === "failed" ? current.error : input.error; + if (runError === null) { + throw new Error("Failed Session Run is missing its authoritative durable error."); + } + const timestampMs = currentTimestampMs(); + const timestamp = toIsoString(timestampMs); + const sourceEventId = createSessionRunTerminalSourceId(input.sessionRunId, "run.failed"); + const event = createCanonicalDriverRunFailedEvent({ driverInstanceId: input.driverInstanceId, - sessionRunId: link.sessionRunId, + error: runError, + id: createPlatformId(), + occurredAt: timestamp, + runId: input.sessionRunId, + runtimeId: input.runtimeId, + sessionId: input.sessionId, + traceId: current.traceId, }); + + const outcome = await commitTerminalRunProjection(bindings.DB, { + assistantMessage: null, + error: runError, + ...(expectedDriverObservation === undefined ? {} : { expectedDriverObservation }), + runId: input.sessionRunId, + sessionId: input.sessionId, + source: "driver", + targetStatus: "failed", + terminalEvent: { event, occurredAt: timestampMs, sourceEventId }, + timestampMs, + }); + if (outcome.kind === "stale") { + throw new Error(`Terminal Driver Session Run lost a concurrent ${outcome.currentStatus} race.`); + } } -export async function recordDriverInstanceFailure( +function hasLinkedSessionRun(link: RuntimeSessionLink): link is RuntimeSessionLink & { + runtimeId: string; + sessionId: SessionId; + sessionRunId: SessionRunId; +} { + return link.runtimeId !== null && link.sessionId !== null && link.sessionRunId !== null; +} + +async function finishTerminalDriverRun( bindings: ApiBindings, input: { - error: DriverFailureInput["error"]; + driverConnectionId?: string; + driverGeneration?: number; driverInstanceId: DriverInstanceId; + error: RunError | null; link?: RuntimeSessionLink; + requestedStatus: "completed" | "failed"; + sessionRunId: SessionRunId; }, ): Promise { - const database = bindings.DB; - const link = input.link ?? (await getRuntimeSessionLink(database, input.driverInstanceId)); + if ((input.driverConnectionId === undefined) !== (input.driverGeneration === undefined)) { + throw new Error("Terminal Driver connection identity must be provided together."); + } - if (hasLinkedSessionRun(link)) { - const outcome = await recordCanonicalSessionRunFailure(bindings, { - error: input.error, - runId: link.sessionRunId, - sessionId: link.sessionId, - source: "driver", - }); - if (outcome.kind !== "failed") { - assertTerminalDriverSessionRunTransition(outcome.transition); - } - } else if (link.sessionRunId !== null) { - const outcome = await setSessionRunStatus(database, { - error: input.error, - runId: link.sessionRunId, - source: "driver", - status: "failed", - }); - assertTerminalDriverSessionRunTransition(outcome); + const link = + input.link ?? + (await getRuntimeSessionLink(bindings.DB, input.driverInstanceId, { + sessionRunId: input.sessionRunId, + })); + + if (link.sessionRunId !== input.sessionRunId) { + throw new Error("Terminal Driver Session Run identity does not match the request."); + } + if (!hasLinkedSessionRun(link)) { + throw new Error("Terminal Driver Session Run is missing its durable session identity."); + } + + const driverGeneration = + input.driverGeneration ?? + (await getDriverInstanceLifecycleIdentity(bindings, input.driverInstanceId))?.generation; + if (driverGeneration === undefined) { + throw new Error("Terminal Driver instance identity was not found."); } - await releaseLinkedRunLease(bindings, { + await commitOrAdoptTerminalRun(bindings, { + ...(input.driverConnectionId === undefined + ? {} + : { driverConnectionId: input.driverConnectionId }), + ...(input.driverGeneration === undefined ? {} : { driverGeneration: input.driverGeneration }), driverInstanceId: input.driverInstanceId, - sessionRunId: link.sessionRunId, + error: input.error, + requestedStatus: input.requestedStatus, + runtimeId: link.runtimeId, + sessionId: link.sessionId, + sessionRunId: input.sessionRunId, + }); + await releaseTerminalDriverInstanceSessionRun(bindings, { + ...(input.driverConnectionId === undefined + ? {} + : { expectedDriverConnectionId: input.driverConnectionId }), + driverGeneration, + driverInstanceId: input.driverInstanceId, + sessionRunId: input.sessionRunId, }); } -async function synthesizeDriverRunFinished( - database: D1Database, +export async function recordDriverInstanceCompletion( + bindings: ApiBindings, input: { - bindings: ApiBindings; + driverConnectionId?: string; + driverGeneration?: number; driverInstanceId: DriverInstanceId; - link: RuntimeSessionLink & { - sessionId: SessionId; - sessionRunId: SessionRunId; - }; + sessionRunId: SessionRunId; }, ): Promise { - const eventId = createTerminalDriverEventId({ - driverInstanceId: input.driverInstanceId, - kind: "run.completed", - sessionRunId: input.link.sessionRunId, - }); - const runCompletedEvent = createRuntimeEvent({ + await finishTerminalDriverRun(bindings, { + ...(input.driverConnectionId === undefined + ? {} + : { driverConnectionId: input.driverConnectionId }), + ...(input.driverGeneration === undefined ? {} : { driverGeneration: input.driverGeneration }), driverInstanceId: input.driverInstanceId, - id: createPlatformId(), - kind: "run.completed", - occurredAt: new Date().toISOString(), - payload: { - stopReason: "end_turn", - }, - runId: input.link.sessionRunId, - sessionId: input.link.sessionId, - sourceEventId: eventId, - }); - const [runFinishedEvent] = appRuntimeEventToSessionDeliveryEvents(runCompletedEvent); - - if (runFinishedEvent === undefined) { - throw new Error("Run completion event did not app to session delivery."); - } - - // The terminal RPC proves only that execution ended; it carries no final - // assistant item identity. Never guess from the session's last assistant - // message, which may be progress or belong to an earlier run. The canonical - // projection is written only by an ordered runtime run.completed event that - // names finalMessageId. - const outcome = await setSessionRunStatus(database, { - runId: input.link.sessionRunId, - source: "driver", - status: "completed", - }); - assertTerminalDriverSessionRunTransition(outcome); - if (isStaleTerminalRunTransition(outcome) && !isStaleTerminalRunStatus(outcome, "completed")) { - return; - } - await appendCanonicalTerminalDriverEvent({ - bindings: input.bindings, - event: runCompletedEvent, - }); -} - -async function appendCanonicalTerminalDriverEvent(input: { - bindings: ApiBindings; - event: RuntimeEventEnvelope; -}): Promise { - await appendSessionRuntimeEvents({ - bindings: input.bindings, - events: [input.event], - sessionId: input.event.sessionId, - sourceEventId: input.event.sourceEventId ?? input.event.id, + error: null, + requestedStatus: "completed", + sessionRunId: input.sessionRunId, }); } -function hasLinkedSessionRun(link: RuntimeSessionLink): link is RuntimeSessionLink & { - sessionId: SessionId; - sessionRunId: SessionRunId; -} { - return link.sessionId !== null && link.sessionRunId !== null; -} - -async function releaseLinkedRunLease( +export async function recordDriverInstanceFailure( bindings: ApiBindings, input: { - readonly driverInstanceId: DriverInstanceId; - readonly sessionRunId: SessionRunId | null; + driverConnectionId?: string; + driverGeneration?: number; + error: DriverFailureInput["error"]; + driverInstanceId: DriverInstanceId; + link?: RuntimeSessionLink; + sessionRunId: SessionRunId; }, -): Promise { - if (input.sessionRunId === null) { - return false; - } - - const outcome = await releaseTerminalDriverInstanceSessionRun(bindings, { +): Promise { + await finishTerminalDriverRun(bindings, { + ...(input.driverConnectionId === undefined + ? {} + : { driverConnectionId: input.driverConnectionId }), + ...(input.driverGeneration === undefined ? {} : { driverGeneration: input.driverGeneration }), driverInstanceId: input.driverInstanceId, + error: input.error, + ...(input.link === undefined ? {} : { link: input.link }), + requestedStatus: "failed", sessionRunId: input.sessionRunId, }); - - return outcome.released; } diff --git a/apps/api/src/modules/runtime/infrastructure/driver-instance/terminal-run-release.ts b/apps/api/src/modules/runtime/infrastructure/driver-instance/terminal-run-release.ts index 576754f1..353078be 100644 --- a/apps/api/src/modules/runtime/infrastructure/driver-instance/terminal-run-release.ts +++ b/apps/api/src/modules/runtime/infrastructure/driver-instance/terminal-run-release.ts @@ -1,40 +1,172 @@ -import type { RunError, SessionRunStatus, SessionRunSummary } from "@mosoo/contracts/session-run"; -import { sessionRunsTable } from "@mosoo/db"; -import type { DriverInstanceId, SessionId, SessionRunId } from "@mosoo/id"; -import { and, eq, inArray } from "drizzle-orm"; +import { createMcpExecuteFailedEventIdentity } from "@mosoo/agent-driver/events"; +import type { RunError, SessionRunSummary } from "@mosoo/contracts/session-run"; +import { + driverInstancesTable, + sessionEventsTable, + sessionRunsTable, + sessionsTable, +} from "@mosoo/db"; +import { createPlatformId, parsePlatformId } from "@mosoo/id"; +import type { + DriverInstanceId, + RuntimeEventId, + RuntimeOperationId, + SessionId, + SessionRunId, +} from "@mosoo/id"; +import { createRuntimeEvent, createRuntimeEventSemanticHash } from "@mosoo/runtime-events"; +import type { RuntimeEventEnvelope } from "@mosoo/runtime-events"; +import { and, eq, exists, inArray, isNull, ne, notExists, sql } from "drizzle-orm"; -import { logInfo, logWarn } from "../../../../platform/cloudflare/logger"; +import { logInfo } from "../../../../platform/cloudflare/logger"; import type { ApiBindings } from "../../../../platform/cloudflare/worker-types"; import { getAppDatabase } from "../../../../platform/db/drizzle"; +import { currentTimestampMs, toIsoString } from "../../../../time"; import { appendSessionRuntimeEvents } from "../../../sessions/application/session-event-write.service"; -import { createFailedSessionRunRuntimeEvent } from "../../application/session-runs/session-run-view-events.service"; +import { createSessionRuntimeEventProjection } from "../../../sessions/domain/session-runtime-event-projection"; import { getRuntimeKindPolicy } from "../../domain/runtime-kind-policy"; +import { ACTIVE_SESSION_RUN_STATUSES } from "../../domain/session-run-lifecycle.machine"; import { classifyReclaim, decideReclaimRecovery } from "../../domain/session-run-reclaim-recovery"; import { isTerminalSessionRunStatus } from "../../domain/session-run-status"; -import { createSessionRunTerminalFailureSourceId } from "../../domain/session-run-terminal-event-id"; import { recordRuntimeRunLeaseReleasedOutcome } from "../runtime-subject-lifecycle/runtime-run-lease-store"; import { createSandboxCheckpoints } from "../sandbox-backup.service"; -import { markExecutingExternalToolEffectsUnknownForDriver } from "../session-runs/external-tool-effect-store.repository"; -import { failAcceptedRuntimeCommandsForTerminalDriver } from "../session-runs/runtime-command-store.repository"; -import { setSessionRunStatus } from "../session-runs/session-run-store.repository"; -import type { SessionRunTransitionOutcome } from "../session-runs/session-run-store.repository"; +import { markClaimedExternalToolEffectsUnknownForDriver } from "../session-runs/external-tool-effect-store.repository"; +import { + listAcceptedInputStartCommandRepairsForTerminalDriver, + listAcceptedMcpCommandRepairsForTerminalDriver, + listTerminalDriversWithPendingRuntimeCommands, + repairAcceptedRuntimeCommandsForTerminalDriver, + updateRuntimeCommandRecord, +} from "../session-runs/runtime-command-store.repository"; +import type { + AcceptedInputStartCommandRepair, + AcceptedMcpCommandRepair, +} from "../session-runs/runtime-command-store.repository"; +import { isCattleTerminalCheckpointReadyForNextRun } from "../session-runs/session-run-admission.repository"; +import { getSessionRunSummary } from "../session-runs/session-run-store.repository"; +import { commitTerminalRunProjection } from "./completed-run-commit.repository"; +import { createCanonicalDriverRunFailedEvent } from "./driver-event-canonicalization"; import type { RuntimeSessionLink } from "./event-types"; +import { getDriverInstanceLifecycleIdentity } from "./lifecycle"; import { getRuntimeSessionLink } from "./session-link.repository"; -import { closeReleasedTerminalRuntimeLeaseIfNeeded } from "./terminal-runtime-lease"; - -interface LinkedSessionRunStatusRow { - readonly sessionRunId: SessionRunId | null; - readonly status: SessionRunStatus | null; -} +import { + closeTerminalRuntimeConversationIfNeeded, + recycleTerminalRuntimeLeaseIfNeeded, +} from "./terminal-runtime-lease"; export interface TerminalDriverInstanceSessionRunReleaseResult { readonly link: RuntimeSessionLink | null; readonly released: boolean; } +async function claimTerminalRunRelease( + database: D1Database, + input: { + readonly expectedDriverConnectionId?: string | null; + readonly driverGeneration: number; + readonly driverInstanceId: DriverInstanceId; + readonly sessionRunId: SessionRunId; + }, +): Promise<{ + operationId: RuntimeOperationId; + phase: "cleanup_required" | "release_committed"; +} | null> { + const operationId = parsePlatformId( + input.sessionRunId, + "terminal Run release operation ID", + ); + const db = getAppDatabase(database); + const exactTerminalRun = db + .select({ id: sessionRunsTable.id }) + .from(sessionRunsTable) + .where( + and( + eq(sessionRunsTable.id, input.sessionRunId), + eq(sessionRunsTable.driverInstanceId, input.driverInstanceId), + inArray(sessionRunsTable.status, ["cancelled", "completed", "expired", "failed"]), + ), + ); + const activeSuccessorRun = db + .select({ id: sessionRunsTable.id }) + .from(sessionRunsTable) + .where( + and( + eq(sessionRunsTable.driverInstanceId, input.driverInstanceId), + ne(sessionRunsTable.id, input.sessionRunId), + inArray(sessionRunsTable.status, ACTIVE_SESSION_RUN_STATUSES), + ), + ); + const claimed = await db + .update(driverInstancesTable) + .set({ statusOperationId: operationId }) + .where( + and( + eq(driverInstancesTable.id, input.driverInstanceId), + input.expectedDriverConnectionId === undefined + ? undefined + : input.expectedDriverConnectionId === null + ? isNull(driverInstancesTable.connectionId) + : eq(driverInstancesTable.connectionId, input.expectedDriverConnectionId), + eq(driverInstancesTable.generation, input.driverGeneration), + inArray(driverInstancesTable.status, [ + "provisioning", + "connecting", + "ready", + "stopped", + "failed", + ]), + isNull(driverInstancesTable.statusOperationId), + exists(exactTerminalRun), + notExists(activeSuccessorRun), + ), + ) + .returning({ id: driverInstancesTable.id }) + .get(); + + if (claimed !== undefined) { + return { operationId, phase: "cleanup_required" }; + } + + const adopted = await db + .select({ status: driverInstancesTable.status }) + .from(driverInstancesTable) + .where( + and( + eq(driverInstancesTable.id, input.driverInstanceId), + input.expectedDriverConnectionId === undefined + ? undefined + : input.expectedDriverConnectionId === null + ? isNull(driverInstancesTable.connectionId) + : eq(driverInstancesTable.connectionId, input.expectedDriverConnectionId), + eq(driverInstancesTable.generation, input.driverGeneration), + inArray(driverInstancesTable.status, [ + "provisioning", + "connecting", + "ready", + "stopping", + "stopped", + "failed", + ]), + eq(driverInstancesTable.statusOperationId, operationId), + exists(exactTerminalRun), + notExists(activeSuccessorRun), + ), + ) + .limit(1) + .get(); + + return adopted === undefined + ? null + : { + operationId, + phase: adopted.status === "stopping" ? "release_committed" : "cleanup_required", + }; +} + async function checkpointTerminalRuntimeSessionIfNeeded( bindings: ApiBindings, link: RuntimeSessionLink, + input: { readonly driverGeneration: number; readonly driverInstanceId: DriverInstanceId }, ): Promise { if ( link.sandboxId === null || @@ -46,192 +178,710 @@ async function checkpointTerminalRuntimeSessionIfNeeded( return; } + const lifecycle = await getAppDatabase(bindings.DB) + .select({ + cleanupOperationKind: sessionsTable.cleanupOperationKind, + operationId: sessionsTable.statusOperationId, + }) + .from(sessionsTable) + .where(eq(sessionsTable.id, link.sessionId)) + .limit(1) + .get(); + if (lifecycle?.cleanupOperationKind === "delete" && lifecycle.operationId !== null) { + return; + } + if ( + link.sandboxKind === "cattle" && + (await isCattleTerminalCheckpointReadyForNextRun(bindings.DB, link.sessionId)) + ) { + return; + } + const rules = getRuntimeKindPolicy(link.sandboxKind).checkpoint.createOnTerminal; if (rules.length === 0) { return; } + const driver = await getAppDatabase(bindings.DB) + .select({ sandboxIncarnation: driverInstancesTable.sandboxIncarnation }) + .from(driverInstancesTable) + .where( + and( + eq(driverInstancesTable.id, input.driverInstanceId), + eq(driverInstancesTable.generation, input.driverGeneration), + eq(driverInstancesTable.sandboxId, link.sandboxId), + ), + ) + .limit(1) + .get(); + if (driver === undefined) { + throw new Error("Terminal sandbox checkpoint lost its exact Driver incarnation."); + } + await createSandboxCheckpoints(bindings, { requiredSessionId: link.sessionId, rules, sandboxId: link.sandboxId, - sessionRunId: link.sessionRunId, + terminalAuthority: { + driverGeneration: input.driverGeneration, + driverInstanceId: input.driverInstanceId, + incarnation: driver.sandboxIncarnation, + sessionId: link.sessionId, + sessionRunId: link.sessionRunId, + }, }); } -function toFinalizedDriverRunTransitionRun( - outcome: SessionRunTransitionOutcome, -): SessionRunSummary | null { - switch (outcome.kind) { - case "applied": - case "duplicate": { - return outcome.run; - } - case "stale": { - if (outcome.reason === "terminal_run") { - return null; - } - throw new Error("Finalized driver repair lost a concurrent run transition."); - } - case "repair_needed": { - throw new Error("Finalized driver repair left the session lifecycle projection stale."); - } - case "rejected": { - throw new Error(`Finalized driver repair run transition was rejected: ${outcome.reason}.`); - } - } -} - -async function appendFinalizedDriverRunEvent( +async function failFinalizedDriverRun( bindings: ApiBindings, input: { - readonly run: SessionRunSummary; + readonly driverInstanceId: DriverInstanceId; + readonly runId: SessionRunId; readonly runError: RunError; + readonly runtimeId: string; readonly sessionId: SessionId; }, -): Promise { - await appendSessionRuntimeEvents({ - bindings, - events: [ - createFailedSessionRunRuntimeEvent({ - run: input.run, - runError: input.runError, - sessionId: input.sessionId, - sourceEventId: createSessionRunTerminalFailureSourceId(input.run.id), - }), - ], +): Promise { + const current = await getSessionRunSummary(bindings.DB, input.runId); + + if (current === null) { + throw new Error("Finalized Driver Session Run was not found."); + } + + if (current.status !== "failed" && isTerminalSessionRunStatus(current.status)) { + return null; + } + + const timestampMs = currentTimestampMs(); + const timestamp = toIsoString(timestampMs); + const runError = current.status === "failed" ? (current.error ?? input.runError) : input.runError; + const failedRun: SessionRunSummary = { + ...current, + completedAt: current.completedAt ?? timestamp, + error: runError, + startedAt: current.startedAt ?? timestamp, + status: "failed", + updatedAt: current.status === "failed" ? current.updatedAt : timestamp, + }; + const event = createCanonicalDriverRunFailedEvent({ + driverInstanceId: input.driverInstanceId, + error: runError, + id: createPlatformId(), + occurredAt: timestamp, + runId: input.runId, + runtimeId: input.runtimeId, sessionId: input.sessionId, + traceId: failedRun.traceId, }); + const outcome = await commitTerminalRunProjection(bindings.DB, { + assistantMessage: null, + error: runError, + runId: input.runId, + sessionId: input.sessionId, + source: "maintenance", + targetStatus: "failed", + terminalEvent: { + event, + occurredAt: timestampMs, + sourceEventId: event.sourceEventId ?? null, + }, + timestampMs, + }); + + if (outcome.kind === "stale") { + return null; + } + + return getSessionRunSummary(bindings.DB, input.runId); } export async function releaseTerminalDriverInstanceSessionRun( bindings: ApiBindings, input: { + expectedDriverConnectionId?: string | null; + driverGeneration: number; driverInstanceId: DriverInstanceId; sessionRunId: SessionRunId; }, ): Promise { const database = bindings.DB; + const claim = await claimTerminalRunRelease(database, input); + if (claim === null) { + throw new Error("Terminal Session Run release lost its exact Driver ownership."); + } const link = await getRuntimeSessionLink(database, input.driverInstanceId, { sessionRunId: input.sessionRunId, }); - await checkpointTerminalRuntimeSessionIfNeeded(bindings, link); + if ( + link.sessionRunId !== input.sessionRunId || + link.sessionRunStatus === null || + !isTerminalSessionRunStatus(link.sessionRunStatus) + ) { + throw new Error("Terminal Session Run no longer belongs to this Driver instance."); + } + + await repairTerminalDriverInputCommands(bindings, input); + + if (claim.phase === "release_committed") { + return { link, released: true }; + } + + await checkpointTerminalRuntimeSessionIfNeeded(bindings, link, input); + await closeTerminalRuntimeConversationIfNeeded(bindings, link); + await recycleTerminalRuntimeLeaseIfNeeded(bindings, link); const outcome = await recordRuntimeRunLeaseReleasedOutcome(database, { driverInstanceId: input.driverInstanceId, + expectedDriverGeneration: input.driverGeneration, + expectedDriverOperationId: claim.operationId, expectedSessionRunId: input.sessionRunId, + retainDriverOperationUntilTerminal: true, }); - const released = outcome.status === "applied"; - if (!released) { - logWarn("runtime.terminal.lease_release_skipped", { - driverInstanceId: input.driverInstanceId, - reason: "reason" in outcome ? outcome.reason : outcome.status, - sessionRunId: input.sessionRunId, - status: outcome.status, - }); + if (outcome.status !== "applied") { + const reason = "reason" in outcome ? outcome.reason : outcome.status; + throw new Error(`Terminal Session Run lease release failed: ${reason}.`); + } + + return { link, released: true }; +} + +function runHasReclaimError(run: SessionRunSummary, runError: RunError): boolean { + return ( + run.error?.code === runError.code && + run.error.details["driverInstanceId"] === runError.details["driverInstanceId"] + ); +} + +type McpTerminalStatus = "cancelled" | "completed" | "failed"; +type CompletedMcpCommandRepair = Extract< + AcceptedMcpCommandRepair["terminal"], + { status: "completed" } +>; +type ReconciledMcpTerminal = + | { result: CompletedMcpCommandRepair["result"]; status: "completed" } + | { error: RunError; status: "failed" } + | { status: "cancelled" }; +interface PersistedMcpTerminalEvent extends Record { + contentText: string; + semanticHash: string | null; + sourceEventId: string; + toolOutputText: string | null; + toolStatus: McpTerminalStatus; +} + +function mcpTerminalSourceEventId( + commandId: string, + status: Exclude, +): string { + return `mcp.execute.${status}:${commandId}`; +} + +function createMcpTerminalEvent( + driverInstanceId: DriverInstanceId, + repair: AcceptedMcpCommandRepair, + terminal: ReconciledMcpTerminal, + traceId: string, +): RuntimeEventEnvelope { + const identity = + terminal.status === "failed" + ? createMcpExecuteFailedEventIdentity({ + commandId: repair.commandId, + rawInput: repair.command.argumentsJson, + rawOutput: terminal.error.message, + title: repair.command.toolName, + toolCallId: repair.command.toolCallId, + }) + : ({ + payload: { + kind: "mcp", + rawInput: repair.command.argumentsJson, + ...(terminal.status === "completed" ? { rawOutput: terminal.result.outputText } : {}), + status: terminal.status, + title: repair.command.toolName, + toolCallId: repair.command.toolCallId, + }, + sourceEventId: mcpTerminalSourceEventId(repair.commandId, terminal.status), + } as const); + + return createRuntimeEvent({ + correlationId: repair.commandId, + driverInstanceId, + id: createPlatformId(), + kind: "tool.call.updated", + occurredAt: new Date().toISOString(), + payload: identity.payload, + runId: parsePlatformId(repair.command.runId, "MCP command Session Run ID"), + runtimeId: repair.runtimeId, + sessionId: repair.sessionId, + sourceEventId: identity.sourceEventId, + traceId, + }); +} + +async function readExistingMcpTerminalEvent(input: { + bindings: ApiBindings; + repair: AcceptedMcpCommandRepair; +}): Promise { + const rows = await getAppDatabase(input.bindings.DB) + .select({ + contentText: sessionEventsTable.contentText, + eventType: sessionEventsTable.eventType, + family: sessionEventsTable.family, + mcpCommandId: sessionEventsTable.mcpCommandId, + processStatus: sessionEventsTable.processStatus, + processType: sessionEventsTable.processType, + runId: sessionEventsTable.runId, + semanticHash: sessionEventsTable.semanticHash, + source: sessionEventsTable.source, + sourceEventId: sessionEventsTable.sourceEventId, + streamId: sessionEventsTable.streamId, + toolCallId: sessionEventsTable.toolCallId, + toolInputDeltaJson: sessionEventsTable.toolInputDeltaJson, + toolInputJson: sessionEventsTable.toolInputJson, + toolName: sessionEventsTable.toolName, + toolOutputDeltaText: sessionEventsTable.toolOutputDeltaText, + toolOutputText: sessionEventsTable.toolOutputText, + toolParentMessageId: sessionEventsTable.toolParentMessageId, + toolResultMessageId: sessionEventsTable.toolResultMessageId, + toolStatus: sessionEventsTable.toolStatus, + tokens: sessionEventsTable.tokens, + traceId: sessionEventsTable.traceId, + visibility: sessionEventsTable.visibility, + }) + .from(sessionEventsTable) + .where( + and( + eq(sessionEventsTable.sessionId, input.repair.sessionId), + eq(sessionEventsTable.mcpCommandId, input.repair.commandId), + eq(sessionEventsTable.eventType, "tool.call.updated"), + inArray(sessionEventsTable.toolStatus, ["completed", "cancelled", "failed"]), + ), + ) + .all(); + + if (rows.length > 1) { + throw new Error("MCP command has conflicting durable terminal events."); + } + + return (rows[0] as PersistedMcpTerminalEvent | undefined) ?? null; +} + +function adoptExistingMcpTerminal(input: { + rawOutput: string | null; + repair: AcceptedMcpCommandRepair; + status: McpTerminalStatus; +}): ReconciledMcpTerminal { + if (input.status === "completed") { + if (input.repair.effectStatus !== "succeeded" || input.repair.terminal.status !== "completed") { + throw new Error("MCP completion event conflicts with its durable external effect."); + } + + return { result: input.repair.terminal.result, status: "completed" }; + } + + if (input.status === "cancelled") { + if (input.repair.effectStatus !== "intent") { + throw new Error("MCP cancellation event conflicts with its durable external effect."); + } + + return { status: "cancelled" }; + } + + if (input.repair.effectStatus === "succeeded" || input.repair.terminal.status !== "failed") { + throw new Error("MCP failure event conflicts with its durable external effect."); + } + + if (input.rawOutput === null) { + throw new Error("MCP failure event is missing its durable raw output."); + } + + return { + error: { ...input.repair.terminal.error, message: input.rawOutput }, + status: "failed", + }; +} + +async function assertMcpTerminalEventProjection( + row: Record, + event: RuntimeEventEnvelope, + commandId: AcceptedMcpCommandRepair["commandId"], +): Promise { + const expected = createSessionRuntimeEventProjection(event, { + provenMcpCommandId: commandId, + }); + + if (row["sourceEventId"] !== event.sourceEventId) { + throw new Error("MCP terminal event source identity conflicts with its command."); + } + + for (const [key, value] of Object.entries(expected)) { + if (row[key] !== value) { + throw new Error(`MCP terminal event projection ${key} conflicts with its command.`); + } + } + + if (row["semanticHash"] !== (await createRuntimeEventSemanticHash(event))) { + throw new Error("MCP terminal event semantic hash conflicts with its command."); + } +} + +async function reconcileAcceptedMcpCommand( + bindings: ApiBindings, + input: { + driverGeneration: number; + driverInstanceId: DriverInstanceId; + repair: AcceptedMcpCommandRepair; + }, +): Promise { + const runId = parsePlatformId( + input.repair.command.runId, + "MCP command Session Run ID", + ); + const link = await getRuntimeSessionLink(bindings.DB, input.driverInstanceId, { + sessionRunId: runId, + }); + + if ( + link.sessionId !== input.repair.sessionId || + link.sessionRunId !== runId || + link.runtimeId !== input.repair.runtimeId || + link.traceId === null + ) { + throw new Error("MCP command repair lost its authoritative Session Run identity."); + } + + const traceId = link.traceId; + const existing = await readExistingMcpTerminalEvent({ bindings, repair: input.repair }); + const proposedTerminal = + existing === null + ? input.repair.terminal.status === "completed" + ? ({ result: input.repair.terminal.result, status: "completed" } as const) + : ({ error: input.repair.terminal.error, status: "failed" } as const) + : adoptExistingMcpTerminal({ + rawOutput: existing.toolOutputText, + repair: input.repair, + status: existing.toolStatus, + }); + + if (existing === null) { + try { + await appendSessionRuntimeEvents({ + bindings, + events: [ + createMcpTerminalEvent(input.driverInstanceId, input.repair, proposedTerminal, traceId), + ], + provenMcpCommandId: input.repair.commandId, + sessionId: input.repair.sessionId, + }); + } catch (error) { + // The terminal tool partial-unique index chooses exactly one concurrent + // Driver/repair winner. Only suppress the insert error when that winner + // can now be read and validated below. + if ((await readExistingMcpTerminalEvent({ bindings, repair: input.repair })) === null) { + throw error; + } + } + } + + const persisted = + existing ?? (await readExistingMcpTerminalEvent({ bindings, repair: input.repair })); + + if (persisted === null) { + throw new Error("MCP terminal event was not durably persisted."); + } + + const terminal = adoptExistingMcpTerminal({ + rawOutput: persisted.toolOutputText, + repair: input.repair, + status: persisted.toolStatus, + }); + const event = createMcpTerminalEvent(input.driverInstanceId, input.repair, terminal, traceId); + await assertMcpTerminalEventProjection(persisted, event, input.repair.commandId); + const outcome = await updateRuntimeCommandRecord(bindings.DB, { + commandId: input.repair.commandId, + driverGeneration: input.driverGeneration, + driverInstanceId: input.driverInstanceId, + ...(terminal.status === "completed" + ? { result: terminal.result } + : terminal.status === "failed" + ? { error: terminal.error } + : {}), + status: terminal.status, + }); + + if (outcome.kind === "rejected") { + throw new Error(`MCP command terminal repair was rejected: ${outcome.reason}.`); + } +} + +async function assertInputTerminalEventPersisted( + bindings: ApiBindings, + repair: AcceptedInputStartCommandRepair, +): Promise { + const eventType = + repair.terminal.status === "completed" + ? "run.completed" + : repair.terminal.status === "cancelled" + ? "run.cancelled" + : "run.failed"; + const row = + (await getAppDatabase(bindings.DB) + .select({ id: sessionEventsTable.id }) + .from(sessionEventsTable) + .where( + and( + eq(sessionEventsTable.sessionId, repair.sessionId), + eq( + sessionEventsTable.runId, + parsePlatformId(repair.command.runId, "input command Session Run ID"), + ), + eq(sessionEventsTable.eventType, eventType), + ), + ) + .limit(1) + .get()) ?? null; + + if (row === null) { + throw new Error("Input command terminal Session Run event is not durably persisted."); + } +} + +async function reconcileAcceptedInputStartCommand( + bindings: ApiBindings, + input: { + driverGeneration: number; + driverInstanceId: DriverInstanceId; + repair: AcceptedInputStartCommandRepair; + }, +): Promise { + await assertInputTerminalEventPersisted(bindings, input.repair); + const outcome = await updateRuntimeCommandRecord(bindings.DB, { + commandId: input.repair.commandId, + driverGeneration: input.driverGeneration, + driverInstanceId: input.driverInstanceId, + ...(input.repair.terminal.status === "completed" + ? { result: input.repair.terminal.result } + : input.repair.terminal.status === "failed" + ? { error: input.repair.terminal.error } + : {}), + status: input.repair.terminal.status, + }); + + if (outcome.kind === "rejected") { + throw new Error(`Input command terminal repair was rejected: ${outcome.reason}.`); } +} - await closeReleasedTerminalRuntimeLeaseIfNeeded(bindings, { link, released }); +async function repairTerminalDriverMcpCommands( + bindings: ApiBindings, + input: { + driverGeneration: number; + driverInstanceId: DriverInstanceId; + nowMs?: number; + }, +): Promise { + await markClaimedExternalToolEffectsUnknownForDriver(bindings.DB, input); + const mcpRepairs = await listAcceptedMcpCommandRepairsForTerminalDriver(bindings.DB, input); - return { link, released }; + for (const repair of mcpRepairs) { + await reconcileAcceptedMcpCommand(bindings, { ...input, repair }); + } +} + +async function repairTerminalDriverInputCommands( + bindings: ApiBindings, + input: { + driverGeneration: number; + driverInstanceId: DriverInstanceId; + nowMs?: number; + }, +): Promise { + const inputRepairs = await listAcceptedInputStartCommandRepairsForTerminalDriver( + bindings.DB, + input, + ); + + for (const repair of inputRepairs) { + await reconcileAcceptedInputStartCommand(bindings, { ...input, repair }); + } } export async function repairFinalizedTerminalDriverRunState( bindings: ApiBindings, input: { + driverGeneration: number; driverInstanceId: DriverInstanceId; + sessionRunId: SessionRunId | null; status: "failed" | "stopped"; }, ): Promise { - await markExecutingExternalToolEffectsUnknownForDriver(bindings.DB, input.driverInstanceId); - await failAcceptedRuntimeCommandsForTerminalDriver(bindings.DB, { + const commandRepairInput = { + driverGeneration: input.driverGeneration, driverInstanceId: input.driverInstanceId, - }); + }; - const link = await getRuntimeSessionLink(bindings.DB, input.driverInstanceId); + // Close any durable tool UI state before the enclosing run is made terminal. + await repairTerminalDriverMcpCommands(bindings, commandRepairInput); - if (link.sessionRunId === null || link.sessionRunStatus === null) { - return { link, released: false }; + if (input.sessionRunId === null) { + await repairTerminalDriverInputCommands(bindings, commandRepairInput); + await repairAcceptedRuntimeCommandsForTerminalDriver(bindings.DB, commandRepairInput); + return { link: null, released: false }; + } + + const link = await getRuntimeSessionLink(bindings.DB, input.driverInstanceId, { + sessionRunId: input.sessionRunId, + }); + + if (link.sessionRunId !== input.sessionRunId || link.sessionRunStatus === null) { + throw new Error("Finalized Driver Session Run ownership was lost."); } - if (!isTerminalSessionRunStatus(link.sessionRunStatus)) { + const wasActive = !isTerminalSessionRunStatus(link.sessionRunStatus); + + if (wasActive || link.sessionRunStatus === "failed") { const runError = classifyReclaim({ driverInstanceId: input.driverInstanceId, driverTerminalStatus: input.status, reclaimReason: "socket_closed", }); - const outcome = await setSessionRunStatus(bindings.DB, { - error: runError, - runId: link.sessionRunId, - source: "driver", - status: "failed", - }); - const run = toFinalizedDriverRunTransitionRun(outcome); - - if (link.sessionId !== null && run !== null) { - await appendFinalizedDriverRunEvent(bindings, { - run, - runError, - sessionId: link.sessionId, - }); + const run = + link.sessionId === null || link.runtimeId === null + ? null + : await failFinalizedDriverRun(bindings, { + driverInstanceId: input.driverInstanceId, + runError, + runId: link.sessionRunId, + runtimeId: link.runtimeId, + sessionId: link.sessionId, + }); + if (link.sessionId !== null && run !== null && runHasReclaimError(run, runError)) { // Decide recovery for the reclaimed run. v1 records the decision so it is // observable and unit-testable; executing the auto-requeue (a fresh // `resume` run + re-dispatch) is a follow-up because this DO finalize // context lacks the viewer + requestUrl that enqueueSessionRunDispatchCommand // needs to rebuild the sandbox's action-token callback URLs. - const recovery = decideReclaimRecovery({ - driverTerminalStatus: input.status, - priorTrigger: run.trigger, - reclaimReason: "socket_closed", - runStatus: link.sessionRunStatus, - }); - logInfo("runtime.reclaim.recovery.decided", { - action: recovery.kind, - driverInstanceId: input.driverInstanceId, - priorTrigger: run.trigger, - runId: run.id, - sessionId: link.sessionId, - }); + if (wasActive) { + const recovery = decideReclaimRecovery({ + driverTerminalStatus: input.status, + priorTrigger: run.trigger, + reclaimReason: "socket_closed", + runStatus: link.sessionRunStatus, + }); + logInfo("runtime.reclaim.recovery.decided", { + action: recovery.kind, + driverInstanceId: input.driverInstanceId, + priorTrigger: run.trigger, + runId: run.id, + sessionId: link.sessionId, + }); + } } } + // input.start is derived from the authoritative terminal run and its durable + // event, so it must be repaired after the run transition above. + await repairTerminalDriverInputCommands(bindings, commandRepairInput); + await repairAcceptedRuntimeCommandsForTerminalDriver(bindings.DB, commandRepairInput); + return releaseTerminalDriverInstanceSessionRun(bindings, { + driverGeneration: input.driverGeneration, driverInstanceId: input.driverInstanceId, sessionRunId: link.sessionRunId, }); } +export async function repairTerminalDriverRuntimeCommandsGlobally( + bindings: ApiBindings, +): Promise { + const failures: unknown[] = []; + const claimedReleases = await getAppDatabase(bindings.DB) + .select({ + driverGeneration: driverInstancesTable.generation, + driverInstanceId: driverInstancesTable.id, + sessionRunId: sessionRunsTable.id, + }) + .from(driverInstancesTable) + .innerJoin( + sessionRunsTable, + and( + eq(sessionRunsTable.driverInstanceId, driverInstancesTable.id), + sql`${sessionRunsTable.id} = ${driverInstancesTable.statusOperationId}`, + ), + ) + .where( + and( + inArray(driverInstancesTable.status, ["ready", "stopped", "failed"]), + inArray(sessionRunsTable.status, ["cancelled", "completed", "expired", "failed"]), + ), + ) + .all(); + + for (const release of claimedReleases) { + try { + await releaseTerminalDriverInstanceSessionRun(bindings, release); + } catch (error) { + failures.push(error); + } + } + + const drivers = await listTerminalDriversWithPendingRuntimeCommands(bindings.DB); + + for (const driver of drivers) { + try { + const identity = await getDriverInstanceLifecycleIdentity(bindings, driver.id); + + if ( + identity === null || + identity.generation !== driver.generation || + (identity.status !== "failed" && identity.status !== "stopped") + ) { + continue; + } + + let link = await getRuntimeSessionLink(bindings.DB, driver.id); + + if (link.sessionRunId === null) { + link = await getRuntimeSessionLink(bindings.DB, driver.id, { + latestTerminalRun: true, + }); + } + await repairFinalizedTerminalDriverRunState(bindings, { + driverGeneration: driver.generation, + driverInstanceId: driver.id, + sessionRunId: link.sessionRunId, + status: identity.status, + }); + } catch (error) { + failures.push(error); + } + } + + if (failures[0] !== undefined) { + throw failures[0]; + } +} + export async function releaseLinkedTerminalDriverInstanceSessionRun( bindings: ApiBindings, driverInstanceId: DriverInstanceId, + driverGeneration: number, ): Promise { - const database = bindings.DB; - const row: LinkedSessionRunStatusRow | null = - (await getAppDatabase(database) - .select({ - sessionRunId: sessionRunsTable.id, - status: sessionRunsTable.status, - }) - .from(sessionRunsTable) - .where( - and( - eq(sessionRunsTable.driverInstanceId, driverInstanceId), - inArray(sessionRunsTable.status, ["cancelled", "completed", "expired", "failed"]), - ), - ) - .limit(1) - .get()) ?? null; + const link = await getRuntimeSessionLink(bindings.DB, driverInstanceId, { + latestTerminalRun: true, + }); - if (row === null || row.sessionRunId === null || !isTerminalSessionRunStatus(row.status)) { - return { link: null, released: false }; + if (link.sessionRunId === null) { + return { link, released: false }; } return releaseTerminalDriverInstanceSessionRun(bindings, { + driverGeneration, driverInstanceId, - sessionRunId: row.sessionRunId, + sessionRunId: link.sessionRunId, }); } diff --git a/apps/api/src/modules/runtime/infrastructure/driver-instance/terminal-runtime-lease.ts b/apps/api/src/modules/runtime/infrastructure/driver-instance/terminal-runtime-lease.ts index 96d7eaf7..21142797 100644 --- a/apps/api/src/modules/runtime/infrastructure/driver-instance/terminal-runtime-lease.ts +++ b/apps/api/src/modules/runtime/infrastructure/driver-instance/terminal-runtime-lease.ts @@ -5,21 +5,6 @@ import { recycleInactiveRuntimeSubjectNow } from "../runtime-subject-lifecycle/r import { closeSandboxConversationSession } from "../sandbox-session.service"; import type { RuntimeSessionLink } from "./event-types"; -export async function closeReleasedTerminalRuntimeLeaseIfNeeded( - bindings: ApiBindings, - input: { - readonly link: RuntimeSessionLink | null; - readonly released: boolean; - }, -): Promise { - if (!input.released || input.link === null) { - return; - } - - await closeTerminalRuntimeConversationIfNeeded(bindings, input.link); - await recycleReleasedTerminalRuntimeLeaseIfNeeded(bindings, input); -} - export async function closeTerminalRuntimeConversationIfNeeded( bindings: ApiBindings, link: RuntimeSessionLink, @@ -40,19 +25,10 @@ export async function closeTerminalRuntimeConversationIfNeeded( }); } -export async function recycleReleasedTerminalRuntimeLeaseIfNeeded( +export async function recycleTerminalRuntimeLeaseIfNeeded( bindings: ApiBindings, - input: { - readonly link: RuntimeSessionLink | null; - readonly released: boolean; - }, + link: RuntimeSessionLink, ): Promise { - if (!input.released || input.link === null) { - return; - } - - const link = input.link; - if (link.sandboxKind === null || link.sandboxId === null || link.sessionId === null) { return; } diff --git a/apps/api/src/modules/runtime/infrastructure/driver-instance/terminal-state-coordinator.ts b/apps/api/src/modules/runtime/infrastructure/driver-instance/terminal-state-coordinator.ts index e0c45ba9..7ac4cfca 100644 --- a/apps/api/src/modules/runtime/infrastructure/driver-instance/terminal-state-coordinator.ts +++ b/apps/api/src/modules/runtime/infrastructure/driver-instance/terminal-state-coordinator.ts @@ -10,16 +10,21 @@ import { } from "../../application/runtime-diagnostic-events"; import { resolvePendingRuntimeCommands } from "./commands"; import { runtimeSessionLinkNeedsRefresh } from "./event-types"; +import type { RuntimeSessionLink } from "./event-types"; import { finalizeDriverInstance } from "./lifecycle"; import type { RuntimeSessionViewCache } from "./runtime-session-view-cache"; import type { DriverInstanceRuntimeState } from "./runtime-state"; import { getRuntimeSessionLink } from "./session-link.repository"; import type { SessionViewerEventDeliveryBuffer } from "./session-viewer-event-delivery-buffer"; -import type { DriverInstanceCloseSnapshot } from "./state"; +import type { DriverInstanceCloseSnapshot, DriverInstanceConnectionEpoch } from "./state"; import { repairFinalizedTerminalDriverRunState } from "./terminal-run-release"; + interface DriverInstanceTerminalStateCoordinatorOptions { + appendDiagnosticEvent?: typeof appendRuntimeDiagnosticEvent; clearStorage: () => Promise; env: ApiBindings; + finalizeDriver?: typeof finalizeDriverInstance; + repairFinalizedRunState?: typeof repairFinalizedTerminalDriverRunState; state: DriverInstanceRuntimeState; viewCache: RuntimeSessionViewCache; viewerEventDelivery: SessionViewerEventDeliveryBuffer; @@ -27,115 +32,198 @@ interface DriverInstanceTerminalStateCoordinatorOptions { } export class DriverInstanceTerminalStateCoordinator { + readonly #appendDiagnosticEvent: typeof appendRuntimeDiagnosticEvent; readonly #clearStorage: () => Promise; readonly #env: ApiBindings; + readonly #finalizeDriver: typeof finalizeDriverInstance; + #finalizationTask: { epoch: DriverInstanceConnectionEpoch; task: Promise } | null = null; + readonly #repairFinalizedRunState: typeof repairFinalizedTerminalDriverRunState; readonly #state: DriverInstanceRuntimeState; + #resetTask: Promise | null = null; readonly #viewCache: RuntimeSessionViewCache; readonly #viewerEventDelivery: SessionViewerEventDeliveryBuffer; readonly #withRuntimeLogContext: (fn: () => T) => T; constructor(options: DriverInstanceTerminalStateCoordinatorOptions) { + this.#appendDiagnosticEvent = options.appendDiagnosticEvent ?? appendRuntimeDiagnosticEvent; this.#clearStorage = options.clearStorage; this.#env = options.env; + this.#finalizeDriver = options.finalizeDriver ?? finalizeDriverInstance; + this.#repairFinalizedRunState = + options.repairFinalizedRunState ?? repairFinalizedTerminalDriverRunState; this.#state = options.state; this.#viewCache = options.viewCache; this.#viewerEventDelivery = options.viewerEventDelivery; this.#withRuntimeLogContext = options.withRuntimeLogContext; } - async finalize(): Promise { - if (this.#state.terminalized) { + async finalize(epoch: DriverInstanceConnectionEpoch): Promise { + if (!this.#state.matchesConnectionEpoch(epoch)) { + return; + } + + if (this.#finalizationTask !== null && this.#epochsMatch(this.#finalizationTask.epoch, epoch)) { + return this.#finalizationTask.task; + } + + if (this.#state.terminalCleanupComplete) { return; } - this.#state.terminalized = true; - await this.#viewerEventDelivery.flushSafely(); + const task = this.#finalize(epoch).finally(() => { + if (this.#finalizationTask?.task === task) { + this.#finalizationTask = null; + } + }); + this.#finalizationTask = { epoch, task }; + return task; + } + + async #finalize(epoch: DriverInstanceConnectionEpoch): Promise { + if (!this.#state.matchesConnectionEpoch(epoch)) { + return; + } const driverInstanceId = this.#state.requireDriverInstanceId(); - const close = await this.#ensureCloseSnapshot(); - const status = getDriverInstanceTerminalStatus(this.#state.errorMessage, close.code); + const close = await this.#ensureCloseSnapshot(epoch); + + if (!this.#state.matchesConnectionEpoch(epoch)) { + return; + } + + const terminalSessionLink = await this.#captureTerminalSessionLink(driverInstanceId, epoch); + + if (!this.#state.matchesConnectionEpoch(epoch)) { + return; + } + + this.#viewerEventDelivery.requestStateSync(terminalSessionLink.sessionId); + const desiredStatus = getDriverInstanceTerminalStatus(this.#state.errorMessage, close.code); const closeResult = this.#state.closeResult(); - const connectionId = this.#state.connectionId; - const finalized = isTruthy(connectionId) - ? await finalizeDriverInstance(this.#env, driverInstanceId, { - closeCode: close.code, - closeReason: close.reason || null, - connectionId, - connectedAt: this.#state.connectedAt, - driverPid: this.#state.hello?.pid ?? null, - driverStartedAt: this.#state.hello?.startedAt ?? null, - errorMessage: this.#state.errorMessage, - generation: this.#state.requireDriverGeneration(), - heartbeatCount: this.#state.heartbeatCount, - lastHeartbeatAt: this.#state.lastHeartbeat?.at ?? null, - status, - }) - : false; - - if (finalized) { - await this.#repairFinalizedRunState({ - driverInstanceId, - status, - }); - await this.#appendDriverCrashedEventIfNeeded({ - close, - driverInstanceId, - status, - }); + const snapshot = { + connectedAt: this.#state.connectedAt, + driverPid: this.#state.hello?.pid ?? null, + driverStartedAt: this.#state.hello?.startedAt ?? null, + errorMessage: this.#state.errorMessage, + heartbeatCount: this.#state.heartbeatCount, + lastHeartbeatAt: this.#state.lastHeartbeat?.at ?? null, + terminalSessionRunId: this.#state.terminalSessionRunId, + traceId: this.#state.traceId, + }; + const terminalStatus = await this.#finalizeDriver(this.#env, driverInstanceId, { + closeCode: close.code, + closeReason: close.reason || null, + connectionId: epoch.connectionId, + connectedAt: snapshot.connectedAt, + driverPid: snapshot.driverPid, + driverStartedAt: snapshot.driverStartedAt, + errorMessage: snapshot.errorMessage, + generation: epoch.generation, + heartbeatCount: snapshot.heartbeatCount, + lastHeartbeatAt: snapshot.lastHeartbeatAt, + status: desiredStatus, + }); - this.#withRuntimeLogContext(() => { - logInfo("runtime.run.finalized", { - closeCode: close.code, - closeReason: close.reason || null, - connectedAt: this.#state.connectedAt, - connectionId, - driverInstanceId, - driverPid: this.#state.hello?.pid ?? null, - errorMessage: this.#state.errorMessage, - heartbeatCount: this.#state.heartbeatCount, - status, - }); - }); + if (terminalStatus === null || !this.#state.matchesConnectionEpoch(epoch)) { + return; } - this.#state.resolveCloseWaiters(closeResult); - this.#state.rejectHeartbeatWaiters(new Error(`Driver instance ${driverInstanceId} is closed.`)); + await this.#repairFinalizedRunState(this.#env, { + driverGeneration: epoch.generation, + driverInstanceId, + sessionRunId: snapshot.terminalSessionRunId, + status: terminalStatus, + }); - if (!this.#state.hello) { - this.#state.rejectHelloWaiters( - new Error(`Driver instance ${driverInstanceId} closed before hello.`), - ); + if (!this.#state.matchesConnectionEpoch(epoch)) { + return; + } + + await this.#appendDriverCrashedEventIfNeeded({ + close, + driverGeneration: epoch.generation, + driverInstanceId, + link: terminalSessionLink, + status: terminalStatus, + traceId: snapshot.traceId, + }); + + if (!this.#state.matchesConnectionEpoch(epoch)) { + return; } + this.#withRuntimeLogContext(() => { + logInfo("runtime.run.finalized", { + closeCode: close.code, + closeReason: close.reason || null, + connectedAt: snapshot.connectedAt, + connectionId: epoch.connectionId, + driverInstanceId, + driverPid: snapshot.driverPid, + errorMessage: snapshot.errorMessage, + heartbeatCount: snapshot.heartbeatCount, + status: terminalStatus, + }); + }); + + this.#state.resolveCloseWaiters(closeResult, epoch.generation); + if (!this.#state.ready) { this.#state.rejectReadyWaiters( new Error(`Driver instance ${driverInstanceId} closed before ready.`), + epoch.generation, ); } resolvePendingRuntimeCommands(this.#state.commandWaiters); - await this.#state.persistTerminalSnapshot(); + await this.#state.persistTerminalSnapshot(epoch); } - async resetForReuse(): Promise { + async resetForReuse(driverGeneration: number): Promise { + await this.prepareForReuse(); + + return (this.#resetTask ??= this.#resetForReuse(driverGeneration).finally(() => { + this.#resetTask = null; + })); + } + + async prepareForReuse(): Promise { + if (this.#finalizationTask !== null) { + await this.#finalizationTask.task; + } + + this.#viewerEventDelivery.resetAfterFlush(); + } + + async #resetForReuse(driverGeneration: number): Promise { await this.#state.resetForReuse({ beforeReset: async () => { - await this.#viewerEventDelivery.flushSafely(); this.#viewerEventDelivery.resetAfterFlush(); this.#viewCache.reset(); }, + driverGeneration, }); } async destroy(reason: string): Promise { - await this.#viewerEventDelivery.flushSafely(); + if (this.#finalizationTask !== null) { + await this.#finalizationTask.task; + } + + if (this.#resetTask !== null) { + await this.#resetTask; + } + this.#viewerEventDelivery.resetAfterFlush(); this.#viewCache.reset(); await this.#clearStorage(); this.#state.resetAfterDestroy(reason); } - async #ensureCloseSnapshot(): Promise { + async #ensureCloseSnapshot( + epoch: DriverInstanceConnectionEpoch, + ): Promise { + this.#state.assertConnectionEpoch(epoch); const close = this.#state.close ?? ({ @@ -145,7 +233,7 @@ export class DriverInstanceTerminalStateCoordinator { } satisfies DriverInstanceCloseSnapshot); if (!this.#state.close) { - await this.#state.persistClose(close); + await this.#state.persistClose(close, epoch); } return close; @@ -153,33 +241,32 @@ export class DriverInstanceTerminalStateCoordinator { async #appendDriverCrashedEventIfNeeded(input: { close: DriverInstanceCloseSnapshot; + driverGeneration: number; driverInstanceId: DriverInstanceId; + link: RuntimeSessionLink; status: "failed" | "stopped"; + traceId: string | null; }): Promise { if (input.status !== "failed") { return; } try { - const cachedLink = this.#state.runtimeSessionLink; - const link = - cachedLink !== null && !runtimeSessionLinkNeedsRefresh(cachedLink) - ? cachedLink - : await getRuntimeSessionLink(this.#env.DB, input.driverInstanceId); - this.#state.setRuntimeSessionLink(link); + const { link } = input; if (!isTruthy(link.agentId) || !isTruthy(link.sessionId)) { return; } - await appendRuntimeDiagnosticEvent(this.#env, { + await this.#appendDiagnosticEvent(this.#env, { eventName: RUNTIME_DIAGNOSTIC_EVENT.driverCrashed.name, sessionId: link.sessionId, + sourceEventId: `driver-terminal:${input.driverInstanceId}:${String(input.driverGeneration)}:crashed`, value: { ...toRuntimeDiagnosticBaseValue({ agentId: link.agentId, sessionId: link.sessionId, - traceId: this.#state.traceId, + traceId: input.traceId, }), driverInstanceId: input.driverInstanceId, status: input.close.reason || "failed", @@ -195,21 +282,39 @@ export class DriverInstanceTerminalStateCoordinator { } } - async #repairFinalizedRunState(input: { - driverInstanceId: DriverInstanceId; - status: "failed" | "stopped"; - }): Promise { - try { - await repairFinalizedTerminalDriverRunState(this.#env, input); - } catch (error) { - this.#withRuntimeLogContext(() => { - logWarn("runtime.driver.finalize_repair.failed", { - ...createErrorLogContext(error), - driverInstanceId: input.driverInstanceId, - status: input.status, - }); - }); + async #captureTerminalSessionLink( + driverInstanceId: DriverInstanceId, + epoch: DriverInstanceConnectionEpoch, + ): Promise { + this.#state.assertConnectionEpoch(epoch); + const sessionRunId = this.#state.terminalSessionRunId; + const cachedLink = this.#state.runtimeSessionLink; + const link = + sessionRunId !== null + ? cachedLink?.sessionRunId === sessionRunId && !runtimeSessionLinkNeedsRefresh(cachedLink) + ? cachedLink + : await getRuntimeSessionLink(this.#env.DB, driverInstanceId, { sessionRunId }) + : cachedLink !== null && !runtimeSessionLinkNeedsRefresh(cachedLink) + ? cachedLink + : await getRuntimeSessionLink(this.#env.DB, driverInstanceId); + + this.#state.assertConnectionEpoch(epoch); + + if (sessionRunId !== null && link.sessionRunId !== sessionRunId) { + throw new Error("Terminal Session Run ownership was lost."); } + + if (sessionRunId === null && link.sessionRunId !== null) { + await this.#state.setTerminalSessionRunId(link.sessionRunId, epoch); + } + + this.#state.assertConnectionEpoch(epoch); + this.#state.setRuntimeSessionLink(link); + return link; + } + + #epochsMatch(left: DriverInstanceConnectionEpoch, right: DriverInstanceConnectionEpoch): boolean { + return left.connectionId === right.connectionId && left.generation === right.generation; } } diff --git a/apps/api/src/modules/runtime/infrastructure/driver-session-startup.ts b/apps/api/src/modules/runtime/infrastructure/driver-session-startup.ts index f5acdb6e..961f099b 100644 --- a/apps/api/src/modules/runtime/infrastructure/driver-session-startup.ts +++ b/apps/api/src/modules/runtime/infrastructure/driver-session-startup.ts @@ -26,6 +26,7 @@ async function createDriverStartupExitError( bindings: ApiBindings, input: { driverInstanceId: DriverInstanceId; + driverGeneration: number; eventContext?: DriverRuntimeStartupEventContext; exitCode: number; logContext: Record; @@ -63,7 +64,7 @@ async function createDriverStartupExitError( task: () => input.markStartupFailed ? input.markStartupFailed(message) - : failDriverInstance(bindings, input.driverInstanceId, message), + : failDriverInstance(bindings, input.driverInstanceId, input.driverGeneration, message), }); return new Error(message); @@ -73,6 +74,7 @@ export async function waitForDriverReady( bindings: ApiBindings, input: { driverInstanceId: DriverInstanceId; + driverGeneration: number; eventContext: DriverRuntimeStartupEventContext; getStaleStartupError?: () => Promise; logContext: Record; @@ -85,6 +87,7 @@ export async function waitForDriverReady( const readyPromise = waitForDriverInstanceReady( bindings, input.driverInstanceId, + input.driverGeneration, DRIVER_COLD_READY_TIMEOUT_MS, ).then(() => { ready = true; @@ -102,6 +105,7 @@ export async function waitForDriverReady( throw await createDriverStartupExitError(bindings, { driverInstanceId: input.driverInstanceId, + driverGeneration: input.driverGeneration, eventContext: input.eventContext, exitCode: exit.exitCode, logContext: input.logContext, diff --git a/apps/api/src/modules/runtime/infrastructure/driver-session-state.ts b/apps/api/src/modules/runtime/infrastructure/driver-session-state.ts index 69db540d..a0695fd4 100644 --- a/apps/api/src/modules/runtime/infrastructure/driver-session-state.ts +++ b/apps/api/src/modules/runtime/infrastructure/driver-session-state.ts @@ -1,5 +1,5 @@ import { driverInstancesTable, sessionRunsTable } from "@mosoo/db"; -import type { DriverInstanceId, SessionRunId } from "@mosoo/id"; +import type { DriverInstanceId, SandboxId, SessionId, SessionRunId } from "@mosoo/id"; import { and, eq, inArray } from "drizzle-orm"; import type { ApiBindings } from "../../../platform/cloudflare/worker-types"; @@ -11,7 +11,13 @@ import type { DriverInstanceStatus } from "./driver-instance/status"; export async function getDriverUsage( database: D1Database, driverInstanceId: DriverInstanceId, + scope: { + readonly sandboxId: SandboxId; + readonly sandboxIncarnation: number; + readonly sandboxSessionId: SessionId; + }, ): Promise<{ + generation: number; sessionRunId: SessionRunId | null; status: DriverInstanceStatus; } | null> { @@ -19,10 +25,18 @@ export async function getDriverUsage( const row = (await appDb .select({ + generation: driverInstancesTable.generation, status: driverInstancesTable.status, }) .from(driverInstancesTable) - .where(eq(driverInstancesTable.id, driverInstanceId)) + .where( + and( + eq(driverInstancesTable.id, driverInstanceId), + eq(driverInstancesTable.sandboxId, scope.sandboxId), + eq(driverInstancesTable.sandboxIncarnation, scope.sandboxIncarnation), + eq(driverInstancesTable.sandboxSessionId, scope.sandboxSessionId), + ), + ) .limit(1) .get()) ?? null; @@ -44,6 +58,7 @@ export async function getDriverUsage( .get()) ?? null; return { + generation: row.generation, sessionRunId: activeRun?.id ?? null, status: row.status, }; diff --git a/apps/api/src/modules/runtime/infrastructure/driver-session-stop.service.ts b/apps/api/src/modules/runtime/infrastructure/driver-session-stop.service.ts index b0cb6509..09f6185b 100644 --- a/apps/api/src/modules/runtime/infrastructure/driver-session-stop.service.ts +++ b/apps/api/src/modules/runtime/infrastructure/driver-session-stop.service.ts @@ -1,129 +1,365 @@ -import type { RunError, SessionRunStatus } from "@mosoo/contracts/session-run"; -import { sessionRunsTable } from "@mosoo/db"; +import { driverInstancesTable, sessionRunsTable, sessionsTable } from "@mosoo/db"; import { createPlatformId } from "@mosoo/id"; import type { DriverInstanceId, RuntimeOperationId, SessionRunId } from "@mosoo/id"; -import { and, eq, inArray } from "drizzle-orm"; +import { and, eq, exists, inArray, isNotNull, isNull, ne, notExists, or, sql } from "drizzle-orm"; import { logWarn } from "../../../platform/cloudflare/logger"; import type { ApiBindings } from "../../../platform/cloudflare/worker-types"; import { getAppDatabase } from "../../../platform/db/drizzle"; +import { currentTimestampMs } from "../../../time"; +import { + ASSIGNABLE_DRIVER_INSTANCE_STATUSES, + toDriverInstanceStatusLifecycleEventName, +} from "../domain/driver-instance-lifecycle.machine"; import { RUNTIME_SOCKET_TIMEOUT_MS } from "../domain/runtime-config"; import { ACTIVE_SESSION_RUN_STATUSES } from "../domain/session-run-lifecycle.machine"; import { + destroyDriverInstanceDurableObject, failDriverInstance, sendDriverInstanceCommand, waitForDriverInstanceClose, } from "./driver-instance/client"; -import { getDriverInstanceRecord } from "./driver-instance/driver-instance-record.repository"; import { isDriverControlSocketMissingError } from "./driver-session-stop-errors"; import { recordRuntimeRunLeaseReleasedOutcome } from "./runtime-subject-lifecycle/runtime-run-lease-store"; -import { setSessionRunStatus } from "./session-runs/session-run-store.repository"; -import type { SessionRunTransitionOutcome } from "./session-runs/session-run-store.repository"; -function assertStoppedDriverRunTransition(outcome: SessionRunTransitionOutcome): void { - switch (outcome.kind) { - case "applied": - case "duplicate": { - return; - } - case "stale": { - if (outcome.reason === "terminal_run") { - return; - } - throw new Error("Driver stop lost a concurrent run transition."); - } - case "repair_needed": { - throw new Error("Driver stop left session projection stale."); - } - case "rejected": { - throw new Error(`Driver stop run transition was rejected: ${outcome.reason}.`); - } - } +interface DriverStopSnapshot { + readonly generation: number; + readonly status: (typeof driverInstancesTable.$inferSelect)["status"]; + readonly statusOperationId: RuntimeOperationId | null; +} + +async function readDriverStopSnapshot( + database: D1Database, + driverInstanceId: DriverInstanceId, +): Promise { + return ( + (await getAppDatabase(database) + .select({ + generation: driverInstancesTable.generation, + status: driverInstancesTable.status, + statusOperationId: driverInstancesTable.statusOperationId, + }) + .from(driverInstancesTable) + .where(eq(driverInstancesTable.id, driverInstanceId)) + .limit(1) + .get()) ?? null + ); } async function getActiveDriverSessionRunId( database: D1Database, driverInstanceId: DriverInstanceId, ): Promise { - const row = - (await getAppDatabase(database) - .select({ id: sessionRunsTable.id }) + const row = await getAppDatabase(database) + .select({ id: sessionRunsTable.id }) + .from(sessionRunsTable) + .where( + and( + eq(sessionRunsTable.driverInstanceId, driverInstanceId), + inArray(sessionRunsTable.status, ACTIVE_SESSION_RUN_STATUSES), + ), + ) + .limit(1) + .get(); + + return row?.id ?? null; +} + +async function claimDriverStop( + database: D1Database, + input: { + readonly driverGeneration: number; + readonly driverInstanceId: DriverInstanceId; + readonly expectedSessionRunId: SessionRunId | null; + readonly operationId: RuntimeOperationId; + }, +): Promise { + const db = getAppDatabase(database); + const unexpectedActiveRun = db + .select({ id: sessionRunsTable.id }) + .from(sessionRunsTable) + .where( + and( + eq(sessionRunsTable.driverInstanceId, input.driverInstanceId), + inArray(sessionRunsTable.status, ACTIVE_SESSION_RUN_STATUSES), + ...(input.expectedSessionRunId === null + ? [] + : [ne(sessionRunsTable.id, input.expectedSessionRunId)]), + ), + ); + const expectedRun = + input.expectedSessionRunId === null + ? null + : db + .select({ id: sessionRunsTable.id }) + .from(sessionRunsTable) + .where( + and( + eq(sessionRunsTable.id, input.expectedSessionRunId), + eq(sessionRunsTable.driverInstanceId, input.driverInstanceId), + ), + ); + const now = currentTimestampMs(); + const claimed = await db + .update(driverInstancesTable) + .set({ + status: "stopping", + statusChangedAt: now, + statusEvent: toDriverInstanceStatusLifecycleEventName("stopping"), + statusOperationId: input.operationId, + statusSeq: sql`${driverInstancesTable.statusSeq} + 1`, + statusSource: "api", + updatedAt: now, + }) + .where( + and( + eq(driverInstancesTable.id, input.driverInstanceId), + eq(driverInstancesTable.generation, input.driverGeneration), + inArray(driverInstancesTable.status, ASSIGNABLE_DRIVER_INSTANCE_STATUSES), + or( + isNull(driverInstancesTable.statusOperationId), + eq(driverInstancesTable.statusOperationId, input.operationId), + ), + notExists(unexpectedActiveRun), + ...(expectedRun === null ? [] : [exists(expectedRun)]), + ), + ) + .returning({ id: driverInstancesTable.id }) + .get(); + + if (claimed !== undefined) { + return true; + } + + const adopted = await db + .select({ id: driverInstancesTable.id }) + .from(driverInstancesTable) + .where( + and( + eq(driverInstancesTable.id, input.driverInstanceId), + eq(driverInstancesTable.generation, input.driverGeneration), + eq(driverInstancesTable.status, "stopping"), + eq(driverInstancesTable.statusOperationId, input.operationId), + notExists(unexpectedActiveRun), + ...(expectedRun === null ? [] : [exists(expectedRun)]), + ), + ) + .limit(1) + .get(); + + return adopted !== undefined; +} + +async function releaseDriverStopClaim( + bindings: ApiBindings, + input: { + readonly driverGeneration: number; + readonly driverInstanceId: DriverInstanceId; + readonly operationId: RuntimeOperationId; + readonly sessionRunId: SessionRunId | null; + }, +): Promise { + if (input.sessionRunId !== null) { + const terminalRun = await getAppDatabase(bindings.DB) + .select({ status: sessionRunsTable.status }) .from(sessionRunsTable) .where( and( - eq(sessionRunsTable.driverInstanceId, driverInstanceId), - inArray(sessionRunsTable.status, ACTIVE_SESSION_RUN_STATUSES), + eq(sessionRunsTable.id, input.sessionRunId), + eq(sessionRunsTable.driverInstanceId, input.driverInstanceId), + inArray(sessionRunsTable.status, ["cancelled", "completed", "expired", "failed"]), ), ) .limit(1) - .get()) ?? null; + .get(); + if (terminalRun !== undefined) { + const terminalOperationId = input.sessionRunId as unknown as RuntimeOperationId; + if (input.operationId !== terminalOperationId) { + const handedOff = await getAppDatabase(bindings.DB) + .update(driverInstancesTable) + .set({ statusOperationId: terminalOperationId }) + .where( + and( + eq(driverInstancesTable.id, input.driverInstanceId), + eq(driverInstancesTable.generation, input.driverGeneration), + inArray(driverInstancesTable.status, ["stopped", "failed"]), + eq(driverInstancesTable.statusOperationId, input.operationId), + ), + ) + .returning({ id: driverInstancesTable.id }) + .get(); + if (handedOff === undefined) { + throw new Error("Driver stop lost ownership before terminal cleanup handoff."); + } + } + const { releaseTerminalDriverInstanceSessionRun } = + await import("./driver-instance/terminal-run-release"); + await releaseTerminalDriverInstanceSessionRun(bindings, { + driverGeneration: input.driverGeneration, + driverInstanceId: input.driverInstanceId, + sessionRunId: input.sessionRunId, + }); + return; + } - return row?.id ?? null; + const outcome = await recordRuntimeRunLeaseReleasedOutcome(bindings.DB, { + driverInstanceId: input.driverInstanceId, + expectedDriverGeneration: input.driverGeneration, + expectedDriverOperationId: input.operationId, + expectedSessionRunId: input.sessionRunId, + }); + + if (outcome.status === "applied") { + return; + } + + logWarn("runtime.driver_stop.lease_release_skipped", { + driverInstanceId: input.driverInstanceId, + reason: "reason" in outcome ? outcome.reason : outcome.status, + sessionRunId: input.sessionRunId, + status: outcome.status, + }); + throw new Error("Driver stop could not release its exact Session Run lease."); + } + + const db = getAppDatabase(bindings.DB); + const activeRun = db + .select({ id: sessionRunsTable.id }) + .from(sessionRunsTable) + .where( + and( + eq(sessionRunsTable.driverInstanceId, input.driverInstanceId), + inArray(sessionRunsTable.status, ACTIVE_SESSION_RUN_STATUSES), + ), + ); + const released = await db + .update(driverInstancesTable) + .set({ statusOperationId: null }) + .where( + and( + eq(driverInstancesTable.id, input.driverInstanceId), + eq(driverInstancesTable.generation, input.driverGeneration), + eq(driverInstancesTable.statusOperationId, input.operationId), + notExists(activeRun), + ), + ) + .returning({ id: driverInstancesTable.id }) + .get(); + + if (released === undefined) { + throw new Error("Driver stop lost its exact operation ownership before release."); + } +} + +async function findCurrentTerminalDriverRunId( + database: D1Database, + driverInstanceId: DriverInstanceId, +): Promise { + const rows = await getAppDatabase(database) + .select({ id: sessionRunsTable.id }) + .from(sessionRunsTable) + .innerJoin(sessionsTable, eq(sessionsTable.lastRunId, sessionRunsTable.id)) + .where( + and( + eq(sessionRunsTable.driverInstanceId, driverInstanceId), + inArray(sessionRunsTable.status, ["cancelled", "completed", "expired", "failed"]), + ), + ) + .limit(2) + .all(); + + return rows.length === 1 ? (rows[0]?.id ?? null) : null; } export async function stopDriverSession( bindings: ApiBindings, input: { driverInstanceId: DriverInstanceId; + expectedDriverGeneration?: number; + expectedSessionRunId?: SessionRunId; operationId?: RuntimeOperationId; - preserveSessionLifecycle?: boolean; reason: string; - terminalRun?: { - error?: RunError | null; - status: Extract; - }; }, ): Promise { - const driver = await getDriverInstanceRecord(bindings.DB, input.driverInstanceId); + const driver = await readDriverStopSnapshot(bindings.DB, input.driverInstanceId); if (!driver) { return; } - const activeDriver = driver; - const activeSessionRunId = await getActiveDriverSessionRunId(bindings.DB, input.driverInstanceId); + if ( + input.expectedDriverGeneration !== undefined && + driver.generation !== input.expectedDriverGeneration + ) { + return; + } - async function releaseLinkedRun(): Promise { - if (activeSessionRunId === null) { - return; - } + const observedActiveRunId = await getActiveDriverSessionRunId( + bindings.DB, + input.driverInstanceId, + ); + const terminalOwnedRunId = + input.expectedSessionRunId === undefined && + observedActiveRunId === null && + driver.statusOperationId !== null + ? await findCurrentTerminalDriverRunId(bindings.DB, input.driverInstanceId) + : null; + const expectedSessionRunId = + input.expectedSessionRunId ?? observedActiveRunId ?? terminalOwnedRunId; - if (input.terminalRun !== undefined) { - const outcome = await setSessionRunStatus(bindings.DB, { - error: input.terminalRun.error ?? null, - ...(input.operationId !== undefined ? { operationId: input.operationId } : {}), - preserveSessionLifecycle: input.preserveSessionLifecycle === true, - runId: activeSessionRunId, - source: "runtime_operation", - status: input.terminalRun.status, + if (driver.status === "stopped" || driver.status === "failed") { + if (driver.statusOperationId !== null) { + if (input.operationId !== undefined && input.operationId !== driver.statusOperationId) { + return; + } + await destroyDriverInstanceDurableObject( + bindings, + input.driverInstanceId, + driver.generation, + input.reason, + ); + await releaseDriverStopClaim(bindings, { + driverGeneration: driver.generation, + driverInstanceId: input.driverInstanceId, + operationId: driver.statusOperationId, + sessionRunId: expectedSessionRunId, }); - assertStoppedDriverRunTransition(outcome); + return; } - - const outcome = await recordRuntimeRunLeaseReleasedOutcome(bindings.DB, { - driverInstanceId: input.driverInstanceId, - expectedSessionRunId: activeSessionRunId, - }); - - if (outcome.status !== "applied") { - logWarn("runtime.driver_stop.lease_release_skipped", { + if (observedActiveRunId !== null) { + const outcome = await recordRuntimeRunLeaseReleasedOutcome(bindings.DB, { driverInstanceId: input.driverInstanceId, - reason: "reason" in outcome ? outcome.reason : outcome.status, - sessionRunId: activeSessionRunId, - status: outcome.status, + expectedDriverGeneration: driver.generation, + expectedSessionRunId: observedActiveRunId, }); + if (outcome.status !== "applied") { + throw new Error("Terminal Driver lost its exact Session Run lease before release."); + } } + return; } - if (activeDriver.status === "stopped" || activeDriver.status === "failed") { - await releaseLinkedRun(); - return; + const operationId = + driver.status === "stopping" && driver.statusOperationId !== null + ? driver.statusOperationId + : ((expectedSessionRunId as unknown as RuntimeOperationId | null) ?? + input.operationId ?? + createPlatformId()); + const claimed = await claimDriverStop(bindings.DB, { + driverGeneration: driver.generation, + driverInstanceId: input.driverInstanceId, + expectedSessionRunId, + operationId, + }); + if (!claimed) { + throw new Error("Driver stop lost its exact Driver and Session Run ownership."); } + let stopped = false; try { - if (activeDriver.status === "ready") { + if (driver.status === "ready") { try { - await sendDriverInstanceCommand(bindings, input.driverInstanceId, { + await sendDriverInstanceCommand(bindings, input.driverInstanceId, driver.generation, { commandId: createPlatformId(), kind: "session.stop", reason: input.reason, @@ -131,19 +367,105 @@ export async function stopDriverSession( await waitForDriverInstanceClose( bindings, input.driverInstanceId, + driver.generation, RUNTIME_SOCKET_TIMEOUT_MS, ); } catch (error) { if (!isDriverControlSocketMissingError(error)) { throw error; } + await failDriverInstance(bindings, input.driverInstanceId, driver.generation, input.reason); + await waitForDriverInstanceClose( + bindings, + input.driverInstanceId, + driver.generation, + RUNTIME_SOCKET_TIMEOUT_MS, + ); } - - return; + } else { + await failDriverInstance(bindings, input.driverInstanceId, driver.generation, input.reason); + await waitForDriverInstanceClose( + bindings, + input.driverInstanceId, + driver.generation, + RUNTIME_SOCKET_TIMEOUT_MS, + ); } - - await failDriverInstance(bindings, input.driverInstanceId, input.reason); + stopped = true; } finally { - await releaseLinkedRun(); + if (stopped) { + await releaseDriverStopClaim(bindings, { + driverGeneration: driver.generation, + driverInstanceId: input.driverInstanceId, + operationId, + sessionRunId: expectedSessionRunId, + }); + } + } +} + +export async function repairClaimedDriverStopsGlobally(bindings: ApiBindings): Promise { + const db = getAppDatabase(bindings.DB); + const terminalReleaseRun = db + .select({ id: sessionRunsTable.id }) + .from(sessionRunsTable) + .where( + and( + sql`${sessionRunsTable.id} = ${driverInstancesTable.statusOperationId}`, + eq(sessionRunsTable.driverInstanceId, driverInstancesTable.id), + inArray(sessionRunsTable.status, ["cancelled", "completed", "expired", "failed"]), + ), + ); + const claims = await db + .select({ + driverGeneration: driverInstancesTable.generation, + driverInstanceId: driverInstancesTable.id, + operationId: driverInstancesTable.statusOperationId, + sessionRunId: sessionRunsTable.id, + }) + .from(driverInstancesTable) + .leftJoin( + sessionRunsTable, + and( + sql`${sessionRunsTable.id} = ${driverInstancesTable.statusOperationId}`, + eq(sessionRunsTable.driverInstanceId, driverInstancesTable.id), + ), + ) + .where( + and( + isNotNull(driverInstancesTable.statusOperationId), + or( + eq(driverInstancesTable.status, "stopping"), + and( + inArray(driverInstancesTable.status, ["stopped", "failed"]), + notExists(terminalReleaseRun), + ), + ), + ), + ) + .all(); + + for (const claim of claims) { + if (claim.operationId === null) { + continue; + } + try { + const sessionRunId = + claim.sessionRunId ?? + (await findCurrentTerminalDriverRunId(bindings.DB, claim.driverInstanceId)); + await stopDriverSession(bindings, { + driverInstanceId: claim.driverInstanceId, + expectedDriverGeneration: claim.driverGeneration, + ...(sessionRunId === null ? {} : { expectedSessionRunId: sessionRunId }), + operationId: claim.operationId, + reason: "runtime.driver_stop.repair", + }); + } catch (error) { + logWarn("runtime.driver_stop.repair_failed", { + driverInstanceId: claim.driverInstanceId, + error: error instanceof Error ? error.message : "Driver stop repair failed.", + operationId: claim.operationId, + }); + } } } diff --git a/apps/api/src/modules/runtime/infrastructure/driver-session.service.ts b/apps/api/src/modules/runtime/infrastructure/driver-session.service.ts index c9b64e3f..b3372a38 100644 --- a/apps/api/src/modules/runtime/infrastructure/driver-session.service.ts +++ b/apps/api/src/modules/runtime/infrastructure/driver-session.service.ts @@ -38,6 +38,7 @@ import { provisionSessionDriver, } from "./runtime-sandbox-provisioner"; import { stopProvisionProcess } from "./runtime-sandbox-provisioning/runtime-driver-process-cleanup"; +import type { RuntimeRunProvisioningLease } from "./runtime-subject-lifecycle/runtime-provisioning-lease-store"; import type { RuntimeRunLeaseTransitionOutcome } from "./runtime-subject-lifecycle/runtime-run-lease-store"; import { createRuntimeSubjectLifecycleService } from "./runtime-subject-lifecycle/runtime-subject-lifecycle.service"; import type { @@ -53,6 +54,7 @@ async function allocateDriverInstanceId( database: D1Database, input: { sandboxId: SandboxId; + sandboxIncarnation: number; sandboxSessionId: SessionId; }, ): Promise { @@ -97,6 +99,7 @@ async function waitForRetryableRunLeaseOutcome( } async function releasePreparedRunLeaseAfterFailure(input: { + driverGeneration: number; driverInstanceId: DriverInstanceId; runtimeSubjectLifecycle: ReturnType; sessionId: SessionId; @@ -106,6 +109,7 @@ async function releasePreparedRunLeaseAfterFailure(input: { try { const released = await input.runtimeSubjectLifecycle.releaseRunLease({ driverInstanceId: input.driverInstanceId, + expectedDriverGeneration: input.driverGeneration, expectedSessionRunId: input.sessionRunId, }); @@ -138,7 +142,9 @@ export async function ensureDriverSessionReady( resolvedMcpServers: DriverResolvedMcpServer[]; resolvedSkillCatalog: DriverSkillCatalogEntry[]; resolvedSkills: Omit[]; + runtimeProvisioningLease: RuntimeRunProvisioningLease; sandbox: SandboxHandle; + sandboxIncarnation: number; sandboxSessionId: SessionId; sessionId: SessionId; sessionRunId: SessionRunId; @@ -146,12 +152,14 @@ export async function ensureDriverSessionReady( onBootPayloadPrepared?: DriverBootPayloadPreparedHandler; }, ): Promise<{ + driverGeneration: number; driverInstanceId: DriverInstanceId; readiness(): Promise; timing: RuntimeTimingSnapshot; }> { let driverInstanceId = await allocateDriverInstanceId(bindings.DB, { sandboxId: input.profile.sandbox.id, + sandboxIncarnation: input.sandboxIncarnation, sandboxSessionId: input.sandboxSessionId, }); const timing = createRuntimeTimingRecorder({ @@ -173,7 +181,11 @@ export async function ensureDriverSessionReady( traceId: input.traceId, }; const usage = await timing.measure("driver.getUsage", () => - getDriverUsage(bindings.DB, driverInstanceId), + getDriverUsage(bindings.DB, driverInstanceId, { + sandboxId: input.profile.sandbox.id, + sandboxIncarnation: input.sandboxIncarnation, + sandboxSessionId: input.sandboxSessionId, + }), ); const usageSessionRunId = usage?.sessionRunId ?? null; @@ -200,9 +212,15 @@ export async function ensureDriverSessionReady( }); try { - await failDriverInstance(bindings, driverInstanceId, DRIVER_SOCKET_MISSING_MESSAGE); + await failDriverInstance( + bindings, + driverInstanceId, + usage.generation, + DRIVER_SOCKET_MISSING_MESSAGE, + ); await runtimeSubjectLifecycle.releaseRunLease({ driverInstanceId, + expectedDriverGeneration: usage.generation, expectedSessionRunId: input.sessionRunId, }); } catch (error) { @@ -219,8 +237,10 @@ export async function ensureDriverSessionReady( const runLeaseOutcome = await timing.measure("driver.bindRun", () => runtimeSubjectLifecycle.acquireRunLease({ + driverGeneration: usage.generation, driverInstanceId, runtimeSubjectId: input.profile.sandbox.id, + runtimeSubjectIncarnation: input.sandboxIncarnation, sessionId: input.sessionId, sessionRunId: input.sessionRunId, }), @@ -239,6 +259,7 @@ export async function ensureDriverSessionReady( const readyTiming = timing.snapshot({ path: "warm" }); return { + driverGeneration: usage.generation, driverInstanceId, readiness: async () => readyTiming, timing: readyTiming, @@ -248,8 +269,10 @@ export async function ensureDriverSessionReady( if (usage && (usage.status === "provisioning" || usage.status === "connecting")) { const runLeaseOutcome = await timing.measure("driver.bindProvisioningRun", () => runtimeSubjectLifecycle.acquireRunLease({ + driverGeneration: usage.generation, driverInstanceId, runtimeSubjectId: input.profile.sandbox.id, + runtimeSubjectIncarnation: input.sandboxIncarnation, sessionId: input.sessionId, sessionRunId: input.sessionRunId, }), @@ -261,12 +284,14 @@ export async function ensureDriverSessionReady( } return { + driverGeneration: usage.generation, driverInstanceId, readiness: async () => { try { await timing.measure("driver.waitProvisioningReady", () => waitForDriverReady(bindings, { driverInstanceId, + driverGeneration: usage.generation, eventContext, logContext: { driverInstanceId, @@ -284,6 +309,7 @@ export async function ensureDriverSessionReady( eventContext, }); await releasePreparedRunLeaseAfterFailure({ + driverGeneration: usage.generation, driverInstanceId, runtimeSubjectLifecycle, sessionId: input.sessionId, @@ -314,13 +340,16 @@ export async function ensureDriverSessionReady( resolvedSkillCatalog: input.resolvedSkillCatalog, resolvedSkills: input.resolvedSkills, runtime: input.profile.runtimeId, + runtimeProvisioningLease: input.runtimeProvisioningLease, sandbox: input.sandbox, + sandboxIncarnation: input.sandboxIncarnation, sandboxSessionId: input.sandboxSessionId, sessionRunId: input.sessionRunId, traceId: input.traceId, }; const { onBootPayloadPrepared } = input; let provisionProcess: RuntimeProcessHandle | null = null; + let provisionDriverGeneration: number | null = null; let reconnectFailureEventContext = eventContext; try { @@ -339,6 +368,7 @@ export async function ensureDriverSessionReady( timing.addPhase(`driver.provision.${phase.name}`, phase.durationMs); } provisionProcess = provision.process; + provisionDriverGeneration = provision.driverGeneration; const provisionEventContext = { ...eventContext, driverInstanceId: provision.driverInstanceId, @@ -347,8 +377,10 @@ export async function ensureDriverSessionReady( const provisioningRunLeaseOutcome = await timing.measure("driver.bindProvisioningRun", () => runtimeSubjectLifecycle.acquireRunLease({ + driverGeneration: provision.driverGeneration, driverInstanceId: provision.driverInstanceId, runtimeSubjectId: input.profile.sandbox.id, + runtimeSubjectIncarnation: input.sandboxIncarnation, sessionId: input.sessionId, sessionRunId: input.sessionRunId, }), @@ -363,12 +395,14 @@ export async function ensureDriverSessionReady( provisionProcess = null; return { + driverGeneration: provision.driverGeneration, driverInstanceId: provision.driverInstanceId, readiness: async () => { try { await timing.measure("driver.waitForReady", () => waitForDriverReady(bindings, { driverInstanceId: provision.driverInstanceId, + driverGeneration: provision.driverGeneration, eventContext: provisionEventContext, logContext: { driverInstanceId: provision.driverInstanceId, @@ -382,6 +416,7 @@ export async function ensureDriverSessionReady( ); } catch (error) { await releasePreparedRunLeaseAfterFailure({ + driverGeneration: provision.driverGeneration, driverInstanceId: provision.driverInstanceId, runtimeSubjectLifecycle, sessionId: input.sessionId, @@ -421,18 +456,22 @@ export async function ensureDriverSessionReady( }); driverInstanceId = await allocateDriverInstanceId(bindings.DB, { sandboxId: input.profile.sandbox.id, + sandboxIncarnation: input.sandboxIncarnation, sandboxSessionId: input.sandboxSessionId, }); reconnectAttempt = null; continue; } - await releasePreparedRunLeaseAfterFailure({ - driverInstanceId, - runtimeSubjectLifecycle, - sessionId: input.sessionId, - sessionRunId: input.sessionRunId, - traceId: input.traceId, - }); + if (provisionDriverGeneration !== null) { + await releasePreparedRunLeaseAfterFailure({ + driverGeneration: provisionDriverGeneration, + driverInstanceId, + runtimeSubjectLifecycle, + sessionId: input.sessionId, + sessionRunId: input.sessionRunId, + traceId: input.traceId, + }); + } await appendDriverSocketReconnectFailedIfNeeded(bindings, { attempt: reconnectAttempt, error, @@ -456,6 +495,7 @@ export async function prewarmDriverSession( resolvedSkillCatalog: DriverSkillCatalogEntry[]; resolvedSkills: Omit[]; sandbox: SandboxHandle; + sandboxIncarnation: number; sandboxSessionId: SessionId; sessionId: SessionId; }, @@ -465,6 +505,7 @@ export async function prewarmDriverSession( } | null> { let driverInstanceId = await allocateDriverInstanceId(bindings.DB, { sandboxId: input.profile.sandbox.id, + sandboxIncarnation: input.sandboxIncarnation, sandboxSessionId: input.sandboxSessionId, }); const timing = createRuntimeTimingRecorder({ @@ -485,7 +526,11 @@ export async function prewarmDriverSession( traceId: null, }; const usage = await timing.measure("driver.getUsage", () => - getDriverUsage(bindings.DB, driverInstanceId), + getDriverUsage(bindings.DB, driverInstanceId, { + sandboxId: input.profile.sandbox.id, + sandboxIncarnation: input.sandboxIncarnation, + sandboxSessionId: input.sandboxSessionId, + }), ); if ((usage?.sessionRunId ?? null) !== null) { @@ -506,7 +551,12 @@ export async function prewarmDriverSession( return { driverInstanceId, timing: timing.snapshot({ path: "warm" }) }; } - await failDriverInstance(bindings, driverInstanceId, DRIVER_SOCKET_MISSING_MESSAGE); + await failDriverInstance( + bindings, + driverInstanceId, + usage.generation, + DRIVER_SOCKET_MISSING_MESSAGE, + ); continue; } @@ -514,6 +564,7 @@ export async function prewarmDriverSession( await timing.measure("driver.waitProvisioningReady", () => waitForDriverReady(bindings, { driverInstanceId, + driverGeneration: usage.generation, eventContext, logContext: { driverInstanceId, @@ -539,6 +590,7 @@ export async function prewarmDriverSession( resolvedSkills: input.resolvedSkills, runtime: input.profile.runtimeId, sandbox: input.sandbox, + sandboxIncarnation: input.sandboxIncarnation, sandboxSessionId: input.sandboxSessionId, sessionRunId: null, traceId: null, @@ -557,6 +609,7 @@ export async function prewarmDriverSession( await timing.measure("driver.waitForReady", () => waitForDriverReady(bindings, { driverInstanceId: provision.driverInstanceId, + driverGeneration: provision.driverGeneration, eventContext: { ...eventContext, driverInstanceId: provision.driverInstanceId, @@ -631,6 +684,7 @@ export async function dispatchDriverTurn( bindings: ApiBindings, input: { attachmentIds: FileId[]; + driverGeneration: number; driverInstanceId: DriverInstanceId; prompt: string; sessionRunId: SessionRunId; @@ -647,6 +701,7 @@ export async function dispatchDriverTurn( requestId: createPlatformId(), runId: input.sessionRunId, }, + driverGeneration: input.driverGeneration, driverInstanceId: input.driverInstanceId, expiresAt: currentTimestampPlus(DRIVER_COLD_READY_TIMEOUT_MS), }); diff --git a/apps/api/src/modules/runtime/infrastructure/execution-plane/sandbox-execution-plane-adapter.ts b/apps/api/src/modules/runtime/infrastructure/execution-plane/sandbox-execution-plane-adapter.ts index 7a087f27..5ebddfb5 100644 --- a/apps/api/src/modules/runtime/infrastructure/execution-plane/sandbox-execution-plane-adapter.ts +++ b/apps/api/src/modules/runtime/infrastructure/execution-plane/sandbox-execution-plane-adapter.ts @@ -23,6 +23,19 @@ import { } from "../../application/runtime-diagnostic-events"; import { createRuntimeTimingRecorder } from "../../application/session-runs/session-runtime-timing"; import { dispatchDriverTurn, ensureDriverSessionReady } from "../driver-session.service"; +import { + cleanupRuntimeProvisioningResources, + retireRuntimeProvisioningIncarnation, +} from "../runtime-subject-lifecycle/runtime-provisioning-cleanup.service"; +import { + claimRuntimeRunProvisioningLease, + heartbeatRuntimeRunProvisioningLease, + recordRuntimeProvisioningSandboxIncarnation, + releaseAbortedRuntimeProvisioningLease, + releaseReadyRuntimeRunProvisioningLease, + renewRuntimeProvisioningLeaseOwnership, +} from "../runtime-subject-lifecycle/runtime-provisioning-lease-store"; +import type { RuntimeRunProvisioningLease } from "../runtime-subject-lifecycle/runtime-provisioning-lease-store"; import { createRuntimeSubjectLifecycleService, getRuntimeSubjectKeepAliveHandle, @@ -33,7 +46,10 @@ import { resetRuntimeSubjectAgentState, stopRuntimeSubjectDrivers, } from "../runtime-subject-lifecycle/runtime-subject-operations.service"; -import { getRuntimeConversationSession } from "../runtime-subject-lifecycle/runtime-subject-store"; +import { + ensureRuntimeSubjectId, + getRuntimeConversationSession, +} from "../runtime-subject-lifecycle/runtime-subject-store"; import type { ExecutionSessionHandle, SandboxHandle } from "../sandbox-handles"; import { ensureSandboxConversationSession } from "../sandbox-session.service"; import { ensureSessionResourcesMounted } from "../session-resources/session-resource-mount.service"; @@ -52,6 +68,8 @@ type TerminalSessionHandle = ExecutionSessionHandle & { terminal(request: Request, options?: PtyOptions): Promise; }; +const RUNTIME_PROVISIONING_HEARTBEAT_MS = 60_000; + function isSessionAlreadyExistsError(error: unknown): boolean { if (!(error instanceof Error)) { return false; @@ -103,7 +121,9 @@ export async function connectPreparedSandboxTerminal( ): Promise { if (input.terminalSessionId) { const terminalSession = await ensureTerminalSession(subject, input.terminalSessionId); - return terminalSession.terminal(input.request, input.options); + return withDisposedRpcResource(terminalSession, (session) => + session.terminal(input.request, input.options), + ); } return subject.terminal(input.request, input.options); @@ -123,6 +143,9 @@ class SandboxExecutionPlaneAdapter implements RuntimeExecutionPlaneAdapter { }); const sandboxProvisioningTimer = createStopwatch(); let sandboxProvisioned = false; + let provisioningLease: RuntimeRunProvisioningLease | null = null; + let provisioningLeaseReleased = false; + let provisioningHeartbeat: ReturnType | null = null; const handles: { executionSession: ExecutionSessionHandle | null; subject: SandboxHandle | null; @@ -144,6 +167,43 @@ class SandboxExecutionPlaneAdapter implements RuntimeExecutionPlaneAdapter { traceId: input.traceId, }); const runtimeSubjectLifecycle = createRuntimeSubjectLifecycleService(bindings); + const ensuredSandboxId = await ensureRuntimeSubjectId(bindings.DB, { + agentId: input.profile.agentId, + appId: input.profile.vendorCredential.appId, + executionOwnerUserId: input.profile.session.origin.executionOwnerUserId, + kind: input.profile.kind, + runtimeSubjectId: sandboxId, + subjectId: input.profile.sandbox.subjectId, + subjectKind: input.profile.sandbox.subjectKind, + }); + if (ensuredSandboxId !== sandboxId) { + throw new Error("Session Run provisioning resolved a different runtime subject."); + } + provisioningLease = await claimRuntimeRunProvisioningLease(bindings.DB, { + runId: input.sessionRunId, + sandboxId, + sessionId: input.sessionId, + }); + if (provisioningLease === null) { + throw new Error("Session Run provisioning could not acquire lifecycle ownership."); + } + const activationProvisioningLease = provisioningLease; + provisioningHeartbeat = setInterval(() => { + if (provisioningLease === null || provisioningLeaseReleased) { + return; + } + void heartbeatRuntimeRunProvisioningLease(bindings.DB, provisioningLease).catch( + () => undefined, + ); + }, RUNTIME_PROVISIONING_HEARTBEAT_MS); + const heartbeatProvisioning = async (): Promise => { + if ( + provisioningLease === null || + !(await heartbeatRuntimeRunProvisioningLease(bindings.DB, provisioningLease)) + ) { + throw new Error("Session Run provisioning lost lifecycle ownership."); + } + }; pendingDiagnostics = appendRuntimeDiagnosticEvent(bindings, { eventName: RUNTIME_DIAGNOSTIC_EVENT.sandboxProvisioningStarted.name, sessionId: input.sessionId, @@ -152,31 +212,46 @@ class SandboxExecutionPlaneAdapter implements RuntimeExecutionPlaneAdapter { sandboxId, }, }); - const { subject: sandbox } = await timing.measure("activateRuntimeSubject", () => - runtimeSubjectLifecycle.activate({ - agentId: input.profile.agentId, - diagnosticContext: { - agentId: input.profile.configRevision.agentId, - sessionId: input.sessionId, - traceId: input.traceId, - }, - executionOwnerUserId: input.profile.session.origin.executionOwnerUserId, - kind: input.profile.kind, - networkConstraints: resolveRuntimeSubjectNetworkConstraints(bindings, { - envVars: input.profile.envVars, + const { incarnation: sandboxIncarnation, subject: sandbox } = await timing.measure( + "activateRuntimeSubject", + () => + runtimeSubjectLifecycle.activate({ + agentId: input.profile.agentId, + diagnosticContext: { + agentId: input.profile.configRevision.agentId, + sessionId: input.sessionId, + traceId: input.traceId, + }, + executionOwnerUserId: input.profile.session.origin.executionOwnerUserId, kind: input.profile.kind, - network: input.profile.network, - requestUrl, + networkConstraints: resolveRuntimeSubjectNetworkConstraints(bindings, { + envVars: input.profile.envVars, + kind: input.profile.kind, + network: input.profile.network, + requestUrl, + subjectKind: input.profile.sandbox.subjectKind, + }), + runtimeSubjectId: sandboxId, + appId: input.profile.vendorCredential.appId, + subjectId: input.profile.sandbox.subjectId, subjectKind: input.profile.sandbox.subjectKind, + timing, + provisioningAuthority: { + operationId: activationProvisioningLease.operationId, + runId: activationProvisioningLease.runId, + sessionId: activationProvisioningLease.sessionId, + }, }), - runtimeSubjectId: sandboxId, - appId: input.profile.vendorCredential.appId, - subjectId: input.profile.sandbox.subjectId, - subjectKind: input.profile.sandbox.subjectKind, - timing, - }), ); handles.subject = sandbox; + provisioningLease = await recordRuntimeProvisioningSandboxIncarnation(bindings.DB, { + lease: provisioningLease, + sandboxIncarnation, + }); + if (provisioningLease === null) { + throw new Error("Session Run provisioning lost ownership before sandbox handoff."); + } + await heartbeatProvisioning(); const provisioningCompletedValue = { ...runtimeBase, coldStartMs: sandboxProvisioningTimer.elapsedMs(), @@ -198,13 +273,22 @@ class SandboxExecutionPlaneAdapter implements RuntimeExecutionPlaneAdapter { kind: input.profile.kind, mountSessionResources: input.attachmentIds.length > 0, origin: input.profile.session.origin, + ...(provisioningLease === null ? {} : { provisioningLease }), + replaceClosedExecutionSession: true, sandbox, sandboxId, + sandboxIncarnation, sessionId: input.sessionId, timing, }), ); handles.executionSession = executionSession.cloudflareSession; + provisioningLease = executionSession.provisioningLease ?? provisioningLease; + await heartbeatProvisioning(); + const driverProvisioningLease = provisioningLease; + if (driverProvisioningLease === null) { + throw new Error("Session Run provisioning lost ownership before Driver handoff."); + } const driverProfile = { ...input.profile, @@ -227,7 +311,9 @@ class SandboxExecutionPlaneAdapter implements RuntimeExecutionPlaneAdapter { resolvedMcpServers: input.resolvedMcpServers, resolvedSkillCatalog: input.resolvedSkillCatalog, resolvedSkills: input.resolvedSkills, + runtimeProvisioningLease: driverProvisioningLease, sandbox, + sandboxIncarnation, sandboxSessionId: input.sessionId, sessionId: input.sessionId, sessionRunId: input.sessionRunId, @@ -238,10 +324,19 @@ class SandboxExecutionPlaneAdapter implements RuntimeExecutionPlaneAdapter { timing.addPhase(phase.name, phase.durationMs); } const initialDriverPhaseCount = driver.timing.phases.length; + provisioningLeaseReleased = await releaseReadyRuntimeRunProvisioningLease(bindings.DB, { + driverGeneration: driver.driverGeneration, + driverInstanceId: driver.driverInstanceId, + lease: provisioningLease, + }); + if (!provisioningLeaseReleased) { + throw new Error("Session Run provisioning could not complete its durable handoff."); + } await pendingDiagnostics; return { + driverGeneration: driver.driverGeneration, driverInstanceId: driver.driverInstanceId, readiness: async () => { const driverTiming = await driver.readiness(); @@ -271,7 +366,29 @@ class SandboxExecutionPlaneAdapter implements RuntimeExecutionPlaneAdapter { }); } releaseRunResources(handles); + if (provisioningLease !== null && !provisioningLeaseReleased) { + const stillOwnsLease = await renewRuntimeProvisioningLeaseOwnership( + bindings.DB, + provisioningLease, + ); + if (stillOwnsLease) { + try { + if (provisioningLease.sandboxIncarnation === null) { + await cleanupRuntimeProvisioningResources(bindings, provisioningLease, "api"); + await releaseAbortedRuntimeProvisioningLease(bindings.DB, provisioningLease); + } else { + await retireRuntimeProvisioningIncarnation(bindings, provisioningLease, "api"); + } + } catch { + // Keep the durable lease so maintenance retries before cleanup can proceed. + } + } + } throw error; + } finally { + if (provisioningHeartbeat !== null) { + clearInterval(provisioningHeartbeat); + } } } @@ -290,7 +407,11 @@ class SandboxExecutionPlaneAdapter implements RuntimeExecutionPlaneAdapter { } await withDisposedRpcResource( - await getRuntimeSubjectKeepAliveHandle(bindings, sandboxSession.sandboxId), + await getRuntimeSubjectKeepAliveHandle( + bindings, + sandboxSession.sandboxId, + sandboxSession.sandboxIncarnation, + ), async (sandbox) => { await ensureSessionResourcesMounted({ bindings, diff --git a/apps/api/src/modules/runtime/infrastructure/native-resume-ref.repository.ts b/apps/api/src/modules/runtime/infrastructure/native-resume-ref.repository.ts index 0c7a307a..f55ca712 100644 --- a/apps/api/src/modules/runtime/infrastructure/native-resume-ref.repository.ts +++ b/apps/api/src/modules/runtime/infrastructure/native-resume-ref.repository.ts @@ -8,11 +8,12 @@ import { parseDriverNativeRuntimeRef, } from "@mosoo/agent-driver/runtime"; import { nativeResumeRefsTable, sessionsTable } from "@mosoo/db"; -import type { DriverInstanceId, SessionId, SessionRunId } from "@mosoo/id"; +import type { DriverInstanceId, RuntimeEventId, SessionId, SessionRunId } from "@mosoo/id"; import { eq, inArray, sql } from "drizzle-orm"; import { getAppDatabase } from "../../../platform/db/drizzle"; import { currentTimestampMs } from "../../../time"; +import { RUNTIME_KIND_POLICIES } from "../domain/runtime-kind-policy"; interface NativeResumeRefRow { committed_value: string | null; @@ -22,13 +23,141 @@ interface NativeResumeRefRow { value: string; } +interface ObservedNativeResumeRefRow { + kind: string; + observed_driver_instance_id: string | null; + observed_event_seq: number; + observed_session_run_id: string | null; + runtime_id: string; + value: string; +} + export interface NativeResumeRefObservation { driverInstanceId: DriverInstanceId; nativeResumeRef: DriverNativeRuntimeRef; + observedEventSeq: number; sessionId: SessionId; sessionRunId: SessionRunId; } +const PLATFORM_NATIVE_RESUME_KINDS = Object.values(RUNTIME_KIND_POLICIES) + .filter((policy) => policy.nativeResume.persistence === "platform") + .map((policy) => policy.kind); + +export function prepareNativeResumeRefProjection( + database: D1Database, + input: NativeResumeRefObservation & { + createdAt: number; + eventId: RuntimeEventId; + semanticHash: string; + }, +): D1PreparedStatement[] { + enforceNativeRuntimeRefShape(input.nativeResumeRef); + + if (!Number.isSafeInteger(input.observedEventSeq) || input.observedEventSeq < 0) { + throw new Error("Native resume ref event seq must be a non-negative safe integer."); + } + + const platformKinds = PLATFORM_NATIVE_RESUME_KINDS.map(() => "?").join(", "); + const eligibleReceipt = `EXISTS ( + SELECT 1 + FROM session_event AS receipt + JOIN session_run AS run + ON run.id = ? + AND run.session_id = receipt.session_id + AND run.driver_instance_id = ? + JOIN driver_instance AS driver + ON driver.id = ? + AND driver.sandbox_session_id = receipt.session_id + JOIN sandbox AS runtime_sandbox + ON runtime_sandbox.id = driver.sandbox_id + AND runtime_sandbox.kind IN (${platformKinds}) + WHERE receipt.id = ? + AND receipt.session_id = ? + AND receipt.event_type = 'runtime.resume.updated' + AND receipt.run_id = ? + AND receipt.semantic_hash = ? + AND receipt.seq = ? + )`; + const eligibilityBindings = [ + input.sessionRunId, + input.driverInstanceId, + input.driverInstanceId, + ...PLATFORM_NATIVE_RESUME_KINDS, + input.eventId, + input.sessionId, + input.sessionRunId, + input.semanticHash, + input.observedEventSeq, + ]; + const upsert = database + .prepare( + `INSERT INTO native_resume_ref ( + created_at, kind, observed_driver_instance_id, observed_event_seq, + observed_session_run_id, runtime_id, session_id, updated_at, value + ) + SELECT ?, ?, ?, ?, ?, ?, ?, ?, ? + WHERE ${eligibleReceipt} + ON CONFLICT (session_id) DO UPDATE SET + kind = excluded.kind, + observed_driver_instance_id = excluded.observed_driver_instance_id, + observed_event_seq = excluded.observed_event_seq, + observed_session_run_id = excluded.observed_session_run_id, + runtime_id = excluded.runtime_id, + updated_at = excluded.updated_at, + value = excluded.value + WHERE native_resume_ref.observed_event_seq < excluded.observed_event_seq`, + ) + .bind( + input.createdAt, + input.nativeResumeRef.kind, + input.driverInstanceId, + input.observedEventSeq, + input.sessionRunId, + input.nativeResumeRef.runtimeId, + input.sessionId, + input.createdAt, + input.nativeResumeRef.value, + ...eligibilityBindings, + ); + const guard = database + .prepare( + `INSERT INTO session_event (id) + SELECT ? + WHERE ${eligibleReceipt} + AND NOT EXISTS ( + SELECT 1 + FROM native_resume_ref AS stored + WHERE stored.session_id = ? + AND ( + stored.observed_event_seq > ? + OR ( + stored.observed_event_seq = ? + AND stored.kind = ? + AND stored.observed_driver_instance_id = ? + AND stored.observed_session_run_id = ? + AND stored.runtime_id = ? + AND stored.value = ? + ) + ) + )`, + ) + .bind( + input.eventId, + ...eligibilityBindings, + input.sessionId, + input.observedEventSeq, + input.observedEventSeq, + input.nativeResumeRef.kind, + input.driverInstanceId, + input.sessionRunId, + input.nativeResumeRef.runtimeId, + input.nativeResumeRef.value, + ); + + return [upsert, guard]; +} + function expectedNativeRuntimeRefKind( runtimeId: DriverNativeRuntimeRef["runtimeId"], ): DriverNativeRuntimeRefKind { @@ -113,14 +242,20 @@ export async function upsertNativeResumeRef( ): Promise { enforceNativeRuntimeRefShape(observation.nativeResumeRef); + if (!Number.isSafeInteger(observation.observedEventSeq) || observation.observedEventSeq < 0) { + throw new Error("Native resume ref event seq must be a non-negative safe integer."); + } + const timestampMs = currentTimestampMs(); + const appDatabase = getAppDatabase(database); - await getAppDatabase(database) + await appDatabase .insert(nativeResumeRefsTable) .values({ createdAt: timestampMs, kind: observation.nativeResumeRef.kind, observedDriverInstanceId: observation.driverInstanceId, + observedEventSeq: observation.observedEventSeq, observedSessionRunId: observation.sessionRunId, runtimeId: observation.nativeResumeRef.runtimeId, sessionId: observation.sessionId, @@ -131,12 +266,54 @@ export async function upsertNativeResumeRef( set: { kind: sql`excluded.kind`, observedDriverInstanceId: sql`excluded.observed_driver_instance_id`, + observedEventSeq: sql`excluded.observed_event_seq`, observedSessionRunId: sql`excluded.observed_session_run_id`, runtimeId: sql`excluded.runtime_id`, updatedAt: sql`excluded.updated_at`, value: sql`excluded.value`, }, + setWhere: sql`${nativeResumeRefsTable.observedEventSeq} < excluded.observed_event_seq`, target: nativeResumeRefsTable.sessionId, }) .run(); + + const stored = + (await appDatabase + .select({ + kind: nativeResumeRefsTable.kind, + observed_driver_instance_id: nativeResumeRefsTable.observedDriverInstanceId, + observed_event_seq: nativeResumeRefsTable.observedEventSeq, + observed_session_run_id: nativeResumeRefsTable.observedSessionRunId, + runtime_id: nativeResumeRefsTable.runtimeId, + value: nativeResumeRefsTable.value, + }) + .from(nativeResumeRefsTable) + .where(eq(nativeResumeRefsTable.sessionId, observation.sessionId)) + .limit(1) + .get()) ?? null; + + assertNativeResumeRefConverged(stored, observation); +} + +function assertNativeResumeRefConverged( + stored: ObservedNativeResumeRefRow | null, + observation: NativeResumeRefObservation, +): void { + if (stored === null || stored.observed_event_seq < observation.observedEventSeq) { + throw new Error("Native resume ref CAS did not persist the durable event."); + } + + if (stored.observed_event_seq > observation.observedEventSeq) { + return; + } + + if ( + stored.kind !== observation.nativeResumeRef.kind || + stored.observed_driver_instance_id !== observation.driverInstanceId || + stored.observed_session_run_id !== observation.sessionRunId || + stored.runtime_id !== observation.nativeResumeRef.runtimeId || + stored.value !== observation.nativeResumeRef.value + ) { + throw new Error("Native resume ref event seq was replayed with conflicting content."); + } } diff --git a/apps/api/src/modules/runtime/infrastructure/runtime-sandbox-provisioning/runtime-driver-files.service.ts b/apps/api/src/modules/runtime/infrastructure/runtime-sandbox-provisioning/runtime-driver-files.service.ts index 097f9faa..5b09d57c 100644 --- a/apps/api/src/modules/runtime/infrastructure/runtime-sandbox-provisioning/runtime-driver-files.service.ts +++ b/apps/api/src/modules/runtime/infrastructure/runtime-sandbox-provisioning/runtime-driver-files.service.ts @@ -1,6 +1,7 @@ import { SANDBOX_CACHE_PATH, SANDBOX_MEMORY_PATH } from "@mosoo/agent-driver/paths"; import { disposeRpcResource } from "../../../../platform/cloudflare/rpc-disposal"; +import { quoteShellArg } from "../../../../shared/shell"; import type { DriverProfileConfig } from "../../domain/driver-snapshot"; import type { ExecutionSessionHandle } from "../sandbox-handles"; import { @@ -32,13 +33,9 @@ const RUNTIME_MEMORY_MOUNTS: RuntimeMemoryMountsByRuntime = { ], }; -function quoteShellArg(value: string): string { - return `'${value.replaceAll("'", `'"'"'`)}'`; -} - async function sha256(value: string): Promise { const digest = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(value)); - return [...new Uint8Array(digest)].map((byte) => byte.toString(16).padStart(2, "0")).join(""); + return new Uint8Array(digest).toHex(); } async function readJsonFile( diff --git a/apps/api/src/modules/runtime/infrastructure/runtime-sandbox-provisioning/runtime-driver-process-cleanup.ts b/apps/api/src/modules/runtime/infrastructure/runtime-sandbox-provisioning/runtime-driver-process-cleanup.ts index 8ec2fcab..920949b7 100644 --- a/apps/api/src/modules/runtime/infrastructure/runtime-sandbox-provisioning/runtime-driver-process-cleanup.ts +++ b/apps/api/src/modules/runtime/infrastructure/runtime-sandbox-provisioning/runtime-driver-process-cleanup.ts @@ -1,6 +1,29 @@ +import { disposeRpcResource } from "../../../../platform/cloudflare/rpc-disposal"; import { runBestEffortRuntimeCleanup } from "../runtime-cleanup"; import type { RuntimeProcessHandle } from "../sandbox-handles"; +export async function startProvisionProcessWithOwnershipFence(input: { + assertOwned: () => Promise; + context: Record; + message: string; + startProcess: () => Promise; +}): Promise { + await input.assertOwned(); + const process = await input.startProcess(); + try { + await input.assertOwned(); + return process; + } catch (error) { + await stopProvisionProcess({ + context: input.context, + message: input.message, + process, + }); + disposeRpcResource(process); + throw error; + } +} + export async function stopProvisionProcess(input: { context: Record; message: string; diff --git a/apps/api/src/modules/runtime/infrastructure/runtime-sandbox-provisioning/runtime-driver-provisioning.service.ts b/apps/api/src/modules/runtime/infrastructure/runtime-sandbox-provisioning/runtime-driver-provisioning.service.ts index 97e8d2fe..c44abe89 100644 --- a/apps/api/src/modules/runtime/infrastructure/runtime-sandbox-provisioning/runtime-driver-provisioning.service.ts +++ b/apps/api/src/modules/runtime/infrastructure/runtime-sandbox-provisioning/runtime-driver-provisioning.service.ts @@ -28,6 +28,7 @@ import { createDriverInstanceRecord, markDriverInstanceFailedIfBootTokenMatches, recordRuntimeProcessStarted, + runtimeProvisioningDriverLaunchIsOwned, } from "../driver-instance/driver-instance-record.repository"; import { relayDriverProcessLogs } from "../driver-process-log-relay"; import { getNativeResumeRefForRuntime } from "../native-resume-ref.repository"; @@ -44,7 +45,10 @@ import { getLostPrewarmOwnershipError, usesInsertOnlyDriverRecord, } from "./runtime-driver-prewarm-ownership"; -import { stopProvisionProcess } from "./runtime-driver-process-cleanup"; +import { + startProvisionProcessWithOwnershipFence, + stopProvisionProcess, +} from "./runtime-driver-process-cleanup"; import { appendRuntimeEnvironmentInstallFailed, createRuntimeEnvironmentInstallState, @@ -158,6 +162,7 @@ async function provisionDriver( const { driverInstanceId } = input; const processId = sanitizeProcessId(driverInstanceId); + const bootPayloadPath = `/tmp/.mosoo-driver-boot-${processId}.json`; const sandboxId = input.profile.sandbox.id; const runtimeEntry = getRuntimeCatalogEntry(input.runtime); @@ -202,7 +207,11 @@ async function provisionDriver( mcpGrants: input.resolvedMcpServers.map(toDriverInstanceMcpGrantRecord), conflictStrategy: input.driverRecordConflictStrategy ?? "replace", runtime: input.runtime, + ...(input.runtimeProvisioningLease + ? { runtimeProvisioningLease: input.runtimeProvisioningLease } + : {}), sandboxId, + sandboxIncarnation: input.sandboxIncarnation, sandboxSessionId: input.sandboxSessionId, }), ); @@ -306,7 +315,6 @@ async function provisionDriver( sandboxId, traceparent, }); - const bootPayloadPath = `${input.profile.session.homePath}/driver-boot-payload-${processId}.json`; const bootPayloadJson = JSON.stringify(bootPayload); const bootPayloadPreparedPromise = timing.measure("onBootPayloadPrepared", async () => { @@ -329,15 +337,34 @@ async function provisionDriver( await timing.measure("writeBootPayload", () => input.cloudflareSession.writeFile(bootPayloadPath, bootPayloadJson), ); + const assertLaunchOwned = async (): Promise => { + if ( + input.runtimeProvisioningLease !== undefined && + !(await runtimeProvisioningDriverLaunchIsOwned(env.DB, { + bootTokenHash: bootToken.hash, + driverGeneration: activeDriverGeneration, + driverInstanceId, + lease: input.runtimeProvisioningLease, + })) + ) { + throw new Error("Runtime Driver provisioning lost launch ownership."); + } + }; const startedProcess = await timing.measure("startProcess", () => - input.cloudflareSession.startProcess(AGENT_DRIVER_PROCESS_COMMAND, { - autoCleanup: true, - cwd: organizationPath, - env: { - [DRIVER_BOOT_PAYLOAD_FILE_ENV_NAME]: bootPayloadPath, - ...toRuntimeProcessProxyEnv(env, runtimeProfile.network.networkPolicy), - }, - processId, + startProvisionProcessWithOwnershipFence({ + assertOwned: assertLaunchOwned, + context: { driverInstanceId, sandboxId }, + message: "runtime.driver.provision.late_process_cleanup_failed", + startProcess: () => + input.cloudflareSession.startProcess(AGENT_DRIVER_PROCESS_COMMAND, { + autoCleanup: true, + cwd: organizationPath, + env: { + [DRIVER_BOOT_PAYLOAD_FILE_ENV_NAME]: bootPayloadPath, + ...toRuntimeProcessProxyEnv(env, runtimeProfile.network.networkPolicy), + }, + processId, + }), }), ); process = startedProcess; @@ -363,6 +390,7 @@ async function provisionDriver( if (staleError !== null) { throw staleError; } + throw new Error("Runtime Driver process start lost durable ownership."); } }); void processRecordPromise.catch(() => undefined); @@ -432,6 +460,10 @@ async function provisionDriver( message: "runtime.driver.provision.skipped_process_cleanup_failed", process, }); + await removeProvisionBootPayload(input.cloudflareSession, bootPayloadPath, { + driverInstanceId, + sandboxId, + }); disposeRpcResource(process); logInfo("runtime.driver.provision.skipped", { driverInstanceId, @@ -503,6 +535,10 @@ async function provisionDriver( message: "runtime.driver.provision.process_cleanup_failed", process, }); + await removeProvisionBootPayload(input.cloudflareSession, bootPayloadPath, { + driverInstanceId, + sandboxId, + }); disposeRpcResource(process); @@ -524,3 +560,17 @@ async function provisionDriver( throw error; } } + +async function removeProvisionBootPayload( + session: ProvisionDriverInput["cloudflareSession"], + path: string, + context: Record, +): Promise { + await runBestEffortRuntimeCleanup({ + context, + message: "runtime.driver.provision.boot_payload_cleanup_failed", + task: async () => { + await session.exec(`rm -f -- '${path}'`); + }, + }); +} diff --git a/apps/api/src/modules/runtime/infrastructure/runtime-sandbox-provisioning/runtime-environment-artifact.ts b/apps/api/src/modules/runtime/infrastructure/runtime-sandbox-provisioning/runtime-environment-artifact.ts index dcc6a74b..f065aadd 100644 --- a/apps/api/src/modules/runtime/infrastructure/runtime-sandbox-provisioning/runtime-environment-artifact.ts +++ b/apps/api/src/modules/runtime/infrastructure/runtime-sandbox-provisioning/runtime-environment-artifact.ts @@ -2,15 +2,12 @@ import { discardPromiseResult } from "@mosoo/effects"; import { withDisposedRpcResult } from "../../../../platform/cloudflare/rpc-disposal"; import type { ApiBindings } from "../../../../platform/cloudflare/worker-types"; +import { quoteShellArg } from "../../../../shared/shell"; import type { DriverEnvironmentArtifactProfile } from "../../domain/driver-snapshot"; import { isRuntimeSandboxLocalBucketEnabled } from "../runtime-sandbox-bucket-mount"; import type { ExecutionSessionHandle, SandboxHandle } from "../sandbox-handles"; import { getParentDirectory } from "./runtime-sandbox-provisioning.paths"; -function quoteShellArg(value: string): string { - return `'${value.replaceAll("'", `'"'"'`)}'`; -} - export async function exposeEnvironmentNodeModules( session: Pick, input: { diff --git a/apps/api/src/modules/runtime/infrastructure/runtime-sandbox-provisioning/runtime-sandbox-provisioning.types.ts b/apps/api/src/modules/runtime/infrastructure/runtime-sandbox-provisioning/runtime-sandbox-provisioning.types.ts index fb01ceeb..460844bb 100644 --- a/apps/api/src/modules/runtime/infrastructure/runtime-sandbox-provisioning/runtime-sandbox-provisioning.types.ts +++ b/apps/api/src/modules/runtime/infrastructure/runtime-sandbox-provisioning/runtime-sandbox-provisioning.types.ts @@ -11,6 +11,7 @@ import type { DriverRuntime, DriverSkillCatalogEntry, } from "../../domain/driver-snapshot"; +import type { RuntimeRunProvisioningLease } from "../runtime-subject-lifecycle/runtime-provisioning-lease-store"; import type { ExecutionSessionHandle, RuntimeProcessHandle, @@ -40,7 +41,9 @@ export interface ProvisionDriverInput { resolvedSkillCatalog: DriverSkillCatalogEntry[]; resolvedSkills: Omit[]; runtime: DriverRuntime; + runtimeProvisioningLease?: RuntimeRunProvisioningLease; sandbox: SandboxHandle; + sandboxIncarnation: number; sandboxSessionId: SessionId; sessionRunId?: SessionRunId | null; traceId?: string | null; diff --git a/apps/api/src/modules/runtime/infrastructure/runtime-subject-lifecycle/lease-ownership-renewal.ts b/apps/api/src/modules/runtime/infrastructure/runtime-subject-lifecycle/lease-ownership-renewal.ts new file mode 100644 index 00000000..18f2e4b4 --- /dev/null +++ b/apps/api/src/modules/runtime/infrastructure/runtime-subject-lifecycle/lease-ownership-renewal.ts @@ -0,0 +1,34 @@ +export function createLeaseOwnershipRenewal( + renew: () => Promise, + lostMessage: string, +): () => Promise { + let ownershipLoss: Error | null = null; + let pending: Promise | null = null; + + return () => { + if (pending !== null) { + return pending; + } + if (ownershipLoss !== null) { + return Promise.reject(ownershipLoss); + } + + pending = renew() + .then( + (renewed) => { + if (!renewed) { + ownershipLoss = new Error(lostMessage); + throw ownershipLoss; + } + }, + (cause) => { + ownershipLoss = new Error(lostMessage, { cause }); + throw ownershipLoss; + }, + ) + .finally(() => { + pending = null; + }); + return pending; + }; +} diff --git a/apps/api/src/modules/runtime/infrastructure/runtime-subject-lifecycle/runtime-conversation-session-store.ts b/apps/api/src/modules/runtime/infrastructure/runtime-subject-lifecycle/runtime-conversation-session-store.ts index 8fa7c722..81f1f7fe 100644 --- a/apps/api/src/modules/runtime/infrastructure/runtime-subject-lifecycle/runtime-conversation-session-store.ts +++ b/apps/api/src/modules/runtime/infrastructure/runtime-subject-lifecycle/runtime-conversation-session-store.ts @@ -7,15 +7,19 @@ import { sessionsTable, } from "@mosoo/db"; import { createPlatformId } from "@mosoo/id"; -import type { SandboxId, SandboxSessionId, SessionId } from "@mosoo/id"; +import type { RuntimeOperationId, SandboxId, SandboxSessionId, SessionId } from "@mosoo/id"; import { and, desc, eq, exists, inArray, isNull, lte, notExists, or, sql } from "drizzle-orm"; -import { getAppDatabase, runAppDatabaseBatch } from "../../../../platform/db/drizzle"; +import { + getAppDatabase, + getD1ChangeCount, + runAppDatabaseBatch, +} from "../../../../platform/db/drizzle"; import { getRuntimeKindPolicy, getRuntimeSubjectInactiveDeadline, } from "../../domain/runtime-kind-policy"; -import { toRuntimeSubjectStatusLifecycleEventName } from "../../domain/runtime-subject-lifecycle.machine"; +import { ACTIVE_SESSION_RUN_STATUSES } from "../../domain/session-run-lifecycle.machine"; import { isCattleTerminalCheckpointReadyForNextRun } from "../session-runs/session-run-admission.repository"; import { activeConversationSessionQuery, @@ -25,6 +29,7 @@ import { runLeaseQueryForListedSubject, } from "./runtime-subject-store-queries"; import type { + PendingRuntimeConversationSessionCleanup, RuntimeConversationSessionRecord, RuntimeConversationSessionState, } from "./runtime-subject-store.types"; @@ -37,6 +42,7 @@ export async function getRuntimeConversationSession( (await getAppDatabase(database) .select({ sandboxSessionId: sandboxSessionsTable.sandboxSessionId, + sandboxIncarnation: sandboxSessionsTable.sandboxIncarnation, cwd: sandboxSessionsTable.cwd, latestReadyBackupDir: readyConversationBackupTable.dir, latestReadyBackupId: readyConversationBackupTable.id, @@ -52,6 +58,7 @@ export async function getRuntimeConversationSession( and( eq(readyConversationBackupTable.sandboxId, sandboxSessionsTable.sandboxId), eq(readyConversationBackupTable.dir, sandboxSessionsTable.cwd), + eq(readyConversationBackupTable.workspaceSessionId, sandboxSessionsTable.sessionId), eq(readyConversationBackupTable.status, "ready"), ), ) @@ -66,6 +73,7 @@ export async function getRuntimeConversationSession( return { sandboxSessionId: row.sandboxSessionId, + sandboxIncarnation: row.sandboxIncarnation, cwd: row.cwd, latestReadyBackup: mapReadyRuntimeSubjectBackup({ dir: row.latestReadyBackupDir, @@ -81,7 +89,10 @@ export async function getRuntimeConversationSession( export async function getRuntimeConversationSessionState( database: D1Database, input: { + readonly expectedProvisioningOperationId?: RuntimeOperationId; + readonly expectedSandboxSessionId?: SandboxSessionId; readonly runtimeSubjectId: SandboxId; + readonly expectedSandboxIncarnation?: number; readonly sessionId: SessionId; }, ): Promise { @@ -89,7 +100,9 @@ export async function getRuntimeConversationSessionState( (await getAppDatabase(database) .select({ agentId: sessionsTable.agentId, + cleanupOperationId: sandboxSessionsTable.cleanupOperationId, sandboxSessionId: sandboxSessionsTable.sandboxSessionId, + sandboxIncarnation: sandboxSessionsTable.sandboxIncarnation, kind: sandboxesTable.kind, status: sandboxSessionsTable.status, }) @@ -100,6 +113,20 @@ export async function getRuntimeConversationSessionState( and( eq(sandboxSessionsTable.sessionId, input.sessionId), eq(sandboxSessionsTable.sandboxId, input.runtimeSubjectId), + ...(input.expectedSandboxSessionId === undefined + ? [] + : [eq(sandboxSessionsTable.sandboxSessionId, input.expectedSandboxSessionId)]), + ...(input.expectedSandboxIncarnation === undefined + ? [] + : [eq(sandboxSessionsTable.sandboxIncarnation, input.expectedSandboxIncarnation)]), + ...(input.expectedProvisioningOperationId === undefined + ? [] + : [ + eq( + sessionsTable.runtimeProvisioningOperationId, + input.expectedProvisioningOperationId, + ), + ]), ), ) .limit(1) @@ -132,9 +159,22 @@ export async function listIdleSessionScopedConversationSessions( .where( and( eq(sandboxSessionsTable.status, "active"), + isNull(sandboxSessionsTable.cleanupOperationId), + isNull(sessionsTable.runtimeProvisioningOperationId), eq(sandboxesTable.kind, "cattle"), sql`${sandboxSessionsTable.updatedAt} <= ${input.idleSinceLte}`, notExists(runLeaseQueryForListedSubject(appDb)), + notExists( + appDb + .select({ id: sessionRunsTable.id }) + .from(sessionRunsTable) + .where( + and( + eq(sessionRunsTable.sessionId, sandboxSessionsTable.sessionId), + inArray(sessionRunsTable.status, ACTIVE_SESSION_RUN_STATUSES), + ), + ), + ), or( eq(sessionsTable.workspaceCheckpointRequired, false), isNull(sessionsTable.lastRunId), @@ -185,32 +225,158 @@ export async function claimIdleSessionScopedConversationForClose( readonly idleSinceLte: number; readonly now: number; readonly runtimeSubjectId: SandboxId; + readonly sandboxIncarnation: number; readonly sandboxSessionId: SandboxSessionId; readonly sessionId: SessionId; }, -): Promise { +): Promise { if (!(await isCattleTerminalCheckpointReadyForNextRun(database, input.sessionId))) { - return false; + return null; } const appDb = getAppDatabase(database); + const operationId = createPlatformId(); const claimed = await appDb .update(sandboxSessionsTable) - .set({ status: "closed", updatedAt: input.now }) + .set({ cleanupOperationId: operationId, status: "cleanup_pending", updatedAt: input.now }) .where( and( eq(sandboxSessionsTable.sessionId, input.sessionId), eq(sandboxSessionsTable.sandboxId, input.runtimeSubjectId), + eq(sandboxSessionsTable.sandboxIncarnation, input.sandboxIncarnation), eq(sandboxSessionsTable.sandboxSessionId, input.sandboxSessionId), - eq(sandboxSessionsTable.status, "active"), + inArray(sandboxSessionsTable.status, ["active", "error"]), + isNull(sandboxSessionsTable.cleanupOperationId), lte(sandboxSessionsTable.updatedAt, input.idleSinceLte), notExists(runLeaseQuery(appDb, input.runtimeSubjectId)), + notExists( + appDb + .select({ id: sessionRunsTable.id }) + .from(sessionRunsTable) + .where( + and( + eq(sessionRunsTable.sessionId, input.sessionId), + inArray(sessionRunsTable.status, ACTIVE_SESSION_RUN_STATUSES), + ), + ), + ), + exists( + appDb + .select({ id: sessionsTable.id }) + .from(sessionsTable) + .where( + and( + eq(sessionsTable.id, input.sessionId), + isNull(sessionsTable.runtimeProvisioningOperationId), + ), + ), + ), ), ) .returning({ sessionId: sandboxSessionsTable.sessionId }) .get(); - return claimed != null; + return claimed == null ? null : operationId; +} + +export async function claimRuntimeConversationSessionCleanup( + database: D1Database, + input: { + readonly expectedProvisioningOperationId?: RuntimeOperationId; + readonly now: number; + readonly runtimeSubjectId: SandboxId; + readonly sandboxIncarnation: number; + readonly sandboxSessionId: SandboxSessionId; + readonly sessionId: SessionId; + }, +): Promise { + const appDb = getAppDatabase(database); + const operationId = createPlatformId(); + const claimed = await appDb + .update(sandboxSessionsTable) + .set({ cleanupOperationId: operationId, status: "cleanup_pending", updatedAt: input.now }) + .where( + and( + eq(sandboxSessionsTable.sessionId, input.sessionId), + eq(sandboxSessionsTable.sandboxId, input.runtimeSubjectId), + eq(sandboxSessionsTable.sandboxIncarnation, input.sandboxIncarnation), + inArray(sandboxSessionsTable.status, ["active", "closed", "error"]), + isNull(sandboxSessionsTable.cleanupOperationId), + eq(sandboxSessionsTable.sandboxSessionId, input.sandboxSessionId), + ...(input.expectedProvisioningOperationId === undefined + ? [] + : [ + exists( + appDb + .select({ id: sessionsTable.id }) + .from(sessionsTable) + .where( + and( + eq(sessionsTable.id, input.sessionId), + eq( + sessionsTable.runtimeProvisioningOperationId, + input.expectedProvisioningOperationId, + ), + eq(sessionsTable.runtimeProvisioningSandboxId, input.runtimeSubjectId), + eq(sessionsTable.runtimeProvisioningSandboxSessionId, input.sandboxSessionId), + eq( + sessionsTable.runtimeProvisioningSandboxIncarnation, + input.sandboxIncarnation, + ), + ), + ), + ), + ]), + ), + ) + .returning({ id: sandboxSessionsTable.sessionId }) + .get(); + + return claimed === undefined ? null : operationId; +} + +export async function listPendingRuntimeConversationSessionCleanups( + database: D1Database, + limit: number, +): Promise { + return getAppDatabase(database) + .select({ + agentId: sessionsTable.agentId, + cleanupOperationId: sandboxSessionsTable.cleanupOperationId, + kind: sandboxesTable.kind, + sandboxId: sandboxSessionsTable.sandboxId, + sandboxSessionId: sandboxSessionsTable.sandboxSessionId, + sandboxIncarnation: sandboxSessionsTable.sandboxIncarnation, + sessionId: sandboxSessionsTable.sessionId, + status: sandboxSessionsTable.status, + }) + .from(sandboxSessionsTable) + .innerJoin(sessionsTable, eq(sessionsTable.id, sandboxSessionsTable.sessionId)) + .innerJoin(sandboxesTable, eq(sandboxesTable.id, sandboxSessionsTable.sandboxId)) + .where(eq(sandboxSessionsTable.status, "cleanup_pending")) + .limit(limit) + .all() as Promise; +} + +export async function retireRuntimeConversationSessionsForIncarnation( + database: D1Database, + input: { + readonly now: number; + readonly runtimeSubjectId: SandboxId; + readonly sandboxIncarnation: number; + }, +): Promise { + await getAppDatabase(database) + .update(sandboxSessionsTable) + .set({ cleanupOperationId: null, status: "closed", updatedAt: input.now }) + .where( + and( + eq(sandboxSessionsTable.sandboxId, input.runtimeSubjectId), + eq(sandboxSessionsTable.sandboxIncarnation, input.sandboxIncarnation), + inArray(sandboxSessionsTable.status, ["active", "cleanup_pending", "error"]), + ), + ) + .run(); } export async function ensureRuntimeConversationSessionRecord( @@ -220,6 +386,7 @@ export async function ensureRuntimeConversationSessionRecord( readonly now: number; readonly originJson: string; readonly runtimeSubjectId: SandboxId; + readonly sandboxIncarnation: number; readonly sessionId: SessionId; }, ): Promise { @@ -230,6 +397,10 @@ export async function ensureRuntimeConversationSessionRecord( throw new Error("Sandbox session is already bound to a different sandbox."); } + if (existing.status !== "closed" && existing.sandboxIncarnation !== input.sandboxIncarnation) { + throw new Error("Sandbox session belongs to a retired sandbox incarnation."); + } + return existing; } @@ -241,6 +412,7 @@ export async function ensureRuntimeConversationSessionRecord( cwd: input.cwd, originJson: input.originJson, sandboxId: input.runtimeSubjectId, + sandboxIncarnation: input.sandboxIncarnation, sessionId: input.sessionId, status: "closed", updatedAt: input.now, @@ -258,6 +430,10 @@ export async function ensureRuntimeConversationSessionRecord( throw new Error("Sandbox session is already bound to a different sandbox."); } + if (created.status !== "closed" && created.sandboxIncarnation !== input.sandboxIncarnation) { + throw new Error("Sandbox session belongs to a retired sandbox incarnation."); + } + return created; } @@ -265,95 +441,165 @@ export async function recordRuntimeConversationSessionError( database: D1Database, input: { readonly sandboxSessionId: SandboxSessionId; + readonly sandboxIncarnation: number; readonly cwd: string; readonly message: string; readonly errorCode: RuntimeSubjectErrorCode; readonly now: number; readonly originJson: string; + readonly expectedProvisioningOperationId?: RuntimeOperationId; readonly runtimeSubjectId: SandboxId; readonly sessionId: SessionId; }, -): Promise { - await runAppDatabaseBatch(database, (appDb) => [ - appDb - .insert(sandboxSessionsTable) - .values({ - sandboxSessionId: input.sandboxSessionId, - createdAt: input.now, - cwd: input.cwd, - originJson: input.originJson, - sandboxId: input.runtimeSubjectId, - sessionId: input.sessionId, - status: "error", - updatedAt: input.now, - }) - .onConflictDoUpdate({ - set: { - status: "error", - updatedAt: sql`excluded.updated_at`, - }, - target: sandboxSessionsTable.sessionId, - }), - appDb - .update(sandboxesTable) - .set({ - lastError: input.message, - lastErrorCode: input.errorCode, - status: "cold", - statusChangedAt: input.now, - statusEvent: toRuntimeSubjectStatusLifecycleEventName("cold"), - statusOperationId: null, - statusSeq: sql`${sandboxesTable.statusSeq} + 1`, - statusSource: "runtime", - updatedAt: input.now, - }) - .where( - and( - eq(sandboxesTable.id, input.runtimeSubjectId), - inArray(sandboxesTable.status, ["restoring", "active"]), +): Promise { + const appDb = getAppDatabase(database); + const updated = await appDb + .update(sandboxSessionsTable) + .set({ + cleanupOperationId: null, + sandboxSessionId: input.sandboxSessionId, + sandboxIncarnation: input.sandboxIncarnation, + status: "error", + updatedAt: input.now, + }) + .where( + and( + eq(sandboxSessionsTable.sessionId, input.sessionId), + eq(sandboxSessionsTable.sandboxId, input.runtimeSubjectId), + or( + and( + eq(sandboxSessionsTable.status, "active"), + eq(sandboxSessionsTable.sandboxIncarnation, input.sandboxIncarnation), + ), + inArray(sandboxSessionsTable.status, ["closed", "error"]), ), + isNull(sandboxSessionsTable.cleanupOperationId), + exists( + appDb + .select({ id: sandboxesTable.id }) + .from(sandboxesTable) + .where( + and( + eq(sandboxesTable.id, input.runtimeSubjectId), + eq(sandboxesTable.incarnation, input.sandboxIncarnation), + eq(sandboxesTable.status, "active"), + ), + ), + ), + ...(input.expectedProvisioningOperationId === undefined + ? [] + : [ + exists( + appDb + .select({ id: sessionsTable.id }) + .from(sessionsTable) + .where( + and( + eq(sessionsTable.id, input.sessionId), + eq( + sessionsTable.runtimeProvisioningOperationId, + input.expectedProvisioningOperationId, + ), + eq(sessionsTable.runtimeProvisioningSandboxId, input.runtimeSubjectId), + eq(sessionsTable.runtimeProvisioningSandboxSessionId, input.sandboxSessionId), + eq( + sessionsTable.runtimeProvisioningSandboxIncarnation, + input.sandboxIncarnation, + ), + ), + ), + ), + ]), ), - ]); + ) + .returning({ id: sandboxSessionsTable.sessionId }) + .get(); + + return updated !== undefined; } export async function recordRuntimeConversationSessionActive( database: D1Database, input: { readonly sandboxSessionId: SandboxSessionId; + readonly sandboxIncarnation: number; readonly cwd: string; readonly now: number; readonly originJson: string; + readonly expectedProvisioningOperationId?: RuntimeOperationId; readonly runtimeSubjectId: SandboxId; readonly sessionId: SessionId; }, -): Promise { +): Promise { const petInactiveDeadlineAt = getRuntimeSubjectInactiveDeadline( getRuntimeKindPolicy("pet"), input.now, ); - await runAppDatabaseBatch(database, (appDb) => [ + const results = await runAppDatabaseBatch(database, (appDb) => [ appDb - .insert(sandboxSessionsTable) - .values({ - sandboxSessionId: input.sandboxSessionId, - createdAt: input.now, + .update(sandboxSessionsTable) + .set({ + cleanupOperationId: null, cwd: input.cwd, - originJson: input.originJson, - sandboxId: input.runtimeSubjectId, - sessionId: input.sessionId, + sandboxSessionId: input.sandboxSessionId, + sandboxIncarnation: input.sandboxIncarnation, status: "active", updatedAt: input.now, }) - .onConflictDoUpdate({ - set: { - sandboxSessionId: sql`excluded.cloudflare_session_id`, - cwd: sql`excluded.cwd`, - status: "active", - updatedAt: sql`excluded.updated_at`, - }, - target: sandboxSessionsTable.sessionId, - }), + .where( + and( + eq(sandboxSessionsTable.sessionId, input.sessionId), + eq(sandboxSessionsTable.sandboxId, input.runtimeSubjectId), + or( + and( + eq(sandboxSessionsTable.status, "active"), + eq(sandboxSessionsTable.sandboxIncarnation, input.sandboxIncarnation), + ), + inArray(sandboxSessionsTable.status, ["closed", "error"]), + ), + isNull(sandboxSessionsTable.cleanupOperationId), + exists( + appDb + .select({ id: sandboxesTable.id }) + .from(sandboxesTable) + .where( + and( + eq(sandboxesTable.id, input.runtimeSubjectId), + eq(sandboxesTable.incarnation, input.sandboxIncarnation), + eq(sandboxesTable.status, "active"), + ), + ), + ), + ...(input.expectedProvisioningOperationId === undefined + ? [] + : [ + exists( + appDb + .select({ id: sessionsTable.id }) + .from(sessionsTable) + .where( + and( + eq(sessionsTable.id, input.sessionId), + eq( + sessionsTable.runtimeProvisioningOperationId, + input.expectedProvisioningOperationId, + ), + eq(sessionsTable.runtimeProvisioningSandboxId, input.runtimeSubjectId), + eq( + sessionsTable.runtimeProvisioningSandboxSessionId, + input.sandboxSessionId, + ), + eq( + sessionsTable.runtimeProvisioningSandboxIncarnation, + input.sandboxIncarnation, + ), + ), + ), + ), + ]), + ), + ), appDb .update(sandboxesTable) .set({ @@ -366,27 +612,88 @@ export async function recordRuntimeConversationSessionActive( `, updatedAt: input.now, }) - .where(eq(sandboxesTable.id, input.runtimeSubjectId)), + .where( + and( + eq(sandboxesTable.id, input.runtimeSubjectId), + eq(sandboxesTable.incarnation, input.sandboxIncarnation), + eq(sandboxesTable.status, "active"), + exists( + appDb + .select({ id: sandboxSessionsTable.sessionId }) + .from(sandboxSessionsTable) + .where( + and( + eq(sandboxSessionsTable.sessionId, input.sessionId), + eq(sandboxSessionsTable.sandboxSessionId, input.sandboxSessionId), + eq(sandboxSessionsTable.sandboxIncarnation, input.sandboxIncarnation), + eq(sandboxSessionsTable.status, "active"), + ), + ), + ), + ), + ), ]); + + return getD1ChangeCount(results[0]) === 1; } export async function recordRuntimeConversationSessionClosed( database: D1Database, input: { + readonly expectedProvisioningOperationId?: RuntimeOperationId; + readonly cleanupOperationId: RuntimeOperationId; readonly inactiveDeadlineAt: number | null; readonly now: number; readonly runtimeSubjectId: SandboxId; + readonly sandboxIncarnation: number; + readonly sandboxSessionId: SandboxSessionId; readonly sessionId: SessionId; }, -): Promise { - await runAppDatabaseBatch(database, (appDb) => [ +): Promise { + const results = await runAppDatabaseBatch(database, (appDb) => [ appDb .update(sandboxSessionsTable) .set({ status: "closed", + cleanupOperationId: null, updatedAt: input.now, }) - .where(eq(sandboxSessionsTable.sessionId, input.sessionId)), + .where( + and( + eq(sandboxSessionsTable.sessionId, input.sessionId), + eq(sandboxSessionsTable.sandboxSessionId, input.sandboxSessionId), + eq(sandboxSessionsTable.sandboxIncarnation, input.sandboxIncarnation), + eq(sandboxSessionsTable.status, "cleanup_pending"), + eq(sandboxSessionsTable.cleanupOperationId, input.cleanupOperationId), + ...(input.expectedProvisioningOperationId === undefined + ? [] + : [ + exists( + appDb + .select({ id: sessionsTable.id }) + .from(sessionsTable) + .where( + and( + eq(sessionsTable.id, input.sessionId), + eq( + sessionsTable.runtimeProvisioningOperationId, + input.expectedProvisioningOperationId, + ), + eq(sessionsTable.runtimeProvisioningSandboxId, input.runtimeSubjectId), + eq( + sessionsTable.runtimeProvisioningSandboxSessionId, + input.sandboxSessionId, + ), + eq( + sessionsTable.runtimeProvisioningSandboxIncarnation, + input.sandboxIncarnation, + ), + ), + ), + ), + ]), + ), + ), appDb .update(sandboxesTable) .set({ @@ -401,4 +708,6 @@ export async function recordRuntimeConversationSessionClosed( ), ), ]); + + return getD1ChangeCount(results[0]) === 1; } diff --git a/apps/api/src/modules/runtime/infrastructure/runtime-subject-lifecycle/runtime-provisioning-cleanup.service.ts b/apps/api/src/modules/runtime/infrastructure/runtime-subject-lifecycle/runtime-provisioning-cleanup.service.ts new file mode 100644 index 00000000..7e1fee07 --- /dev/null +++ b/apps/api/src/modules/runtime/infrastructure/runtime-subject-lifecycle/runtime-provisioning-cleanup.service.ts @@ -0,0 +1,160 @@ +import type { ApiBindings } from "../../../../platform/cloudflare/worker-types"; +import { LIVE_DRIVER_INSTANCE_STATUSES } from "../../domain/driver-instance-lifecycle.machine"; +import { destroyDriverInstanceDurableObject } from "../driver-instance/client"; +import { stopDriverSession } from "../driver-session-stop.service"; +import { closeSandboxConversationSession } from "../sandbox-session.service"; +import { deleteActiveSandboxConversationSession } from "../sandbox-session/sandbox-conversation-session-delete"; +import { createLeaseOwnershipRenewal } from "./lease-ownership-renewal"; +import type { RuntimeProvisioningLease } from "./runtime-provisioning-lease-store"; +import { + claimRuntimeProvisioningSubjectRetirement, + claimRuntimeProvisioningDriverCleanup, + readRuntimeProvisioningCleanupTargets, + releaseAbortedRuntimeProvisioningLease, + renewRuntimeProvisioningLeaseOwnership, +} from "./runtime-provisioning-lease-store"; +import { runRuntimeSubjectOperation } from "./runtime-subject-operations.service"; +import { getRuntimeSubject } from "./runtime-subject-store"; + +const PROVISIONING_CLEANUP_HEARTBEAT_MS = 60_000; + +export async function retireRuntimeProvisioningIncarnation( + bindings: ApiBindings, + lease: RuntimeProvisioningLease, + source: "api" | "maintenance", +): Promise<"released" | "stale" | "waiting"> { + if (lease.sandboxIncarnation === null) { + return "stale"; + } + + const retirement = await claimRuntimeProvisioningSubjectRetirement(bindings.DB, { + lease, + source, + }); + if (retirement.kind === "stale") { + return "stale"; + } + if (retirement.kind === "waiting" || retirement.kind === "repairing") { + return "waiting"; + } + if (retirement.kind === "destroying") { + const subject = await getRuntimeSubject(bindings.DB, lease.sandboxId); + if (subject === null) { + throw new Error("Runtime provisioning retirement lost its subject."); + } + await runRuntimeSubjectOperation(bindings, { + kind: subject.kind, + lease: retirement.lease, + reason: "runtime.provisioning_ambiguous", + runtimeSubjectId: lease.sandboxId, + }); + } + + return (await releaseAbortedRuntimeProvisioningLease(bindings.DB, lease)) ? "released" : "stale"; +} + +export async function cleanupRuntimeProvisioningResources( + bindings: ApiBindings, + lease: RuntimeProvisioningLease, + source: "api" | "maintenance", +): Promise { + const requireOwnership = createLeaseOwnershipRenewal( + () => renewRuntimeProvisioningLeaseOwnership(bindings.DB, lease), + "Runtime provisioning cleanup lost its lease ownership.", + ); + const heartbeat = setInterval(() => { + void requireOwnership().catch(() => undefined); + }, PROVISIONING_CLEANUP_HEARTBEAT_MS); + + try { + await requireOwnership(); + const targets = await readRuntimeProvisioningCleanupTargets(bindings.DB, lease); + if (targets === null) { + throw new Error("Runtime provisioning cleanup lost its lease ownership."); + } + + const failures: unknown[] = []; + const liveDrivers = targets.driverInstances.filter((driver) => + LIVE_DRIVER_INSTANCE_STATUSES.some((status) => status === driver.status), + ); + const driverClaims = await Promise.allSettled( + liveDrivers.map(async (driver) => ({ + claimed: await claimRuntimeProvisioningDriverCleanup(bindings.DB, { + driverGeneration: driver.generation, + driverInstanceId: driver.id, + lease, + source, + }), + driver, + })), + ); + failures.push( + ...driverClaims.flatMap((result) => (result.status === "rejected" ? [result.reason] : [])), + ); + const claimedDrivers = driverClaims.flatMap((result) => + result.status === "fulfilled" && result.value.claimed ? [result.value.driver] : [], + ); + await requireOwnership(); + const stopResults = await Promise.allSettled( + claimedDrivers.map((driver) => + stopDriverSession(bindings, { + driverInstanceId: driver.id, + expectedDriverGeneration: driver.generation, + reason: "runtime.provisioning_stale", + }), + ), + ); + failures.push( + ...stopResults.flatMap((result) => (result.status === "rejected" ? [result.reason] : [])), + ); + + await requireOwnership(); + if (targets.conversationSessionId !== null) { + if (lease.sandboxIncarnation === null) { + throw new Error("Runtime provisioning cleanup target has no sandbox incarnation."); + } + try { + await deleteActiveSandboxConversationSession(bindings, { + sandboxId: lease.sandboxId, + sandboxIncarnation: lease.sandboxIncarnation, + sandboxSessionId: targets.conversationSessionId, + }); + await closeSandboxConversationSession(bindings, { + expectedProvisioningOperationId: lease.operationId, + expectedSandboxSessionId: targets.conversationSessionId, + sandboxId: lease.sandboxId, + sessionId: lease.sessionId, + }); + } catch (error) { + failures.push(error); + } + } + + await requireOwnership(); + const liveDriverIds = new Set(liveDrivers.map((driver) => driver.id)); + const destructionTargets = [ + ...targets.driverInstances.filter((driver) => !liveDriverIds.has(driver.id)), + ...claimedDrivers, + ]; + const destroyResults = await Promise.allSettled( + destructionTargets.map((driver) => + destroyDriverInstanceDurableObject( + bindings, + driver.id, + driver.generation, + "runtime.provisioning_stale", + ), + ), + ); + failures.push( + ...destroyResults.flatMap((result) => (result.status === "rejected" ? [result.reason] : [])), + ); + + await requireOwnership(); + if (failures[0] !== undefined) { + throw failures[0]; + } + } finally { + clearInterval(heartbeat); + } +} diff --git a/apps/api/src/modules/runtime/infrastructure/runtime-subject-lifecycle/runtime-provisioning-lease-store.ts b/apps/api/src/modules/runtime/infrastructure/runtime-subject-lifecycle/runtime-provisioning-lease-store.ts new file mode 100644 index 00000000..26db433a --- /dev/null +++ b/apps/api/src/modules/runtime/infrastructure/runtime-subject-lifecycle/runtime-provisioning-lease-store.ts @@ -0,0 +1,841 @@ +import { + driverInstancesTable, + sandboxesTable, + sandboxSessionsTable, + sessionRunsTable, + sessionsTable, +} from "@mosoo/db"; +import { createPlatformId } from "@mosoo/id"; +import type { + DriverInstanceId, + RuntimeOperationId, + SandboxId, + SandboxSessionId, + SessionId, + SessionRunId, +} from "@mosoo/id"; +import { + and, + eq, + exists, + inArray, + isNotNull, + isNull, + lte, + ne, + notExists, + or, + sql, +} from "drizzle-orm"; +import { alias } from "drizzle-orm/sqlite-core"; + +import { getAppDatabase } from "../../../../platform/db/drizzle"; +import { currentTimestampMs } from "../../../../time"; +import { + ASSIGNABLE_DRIVER_INSTANCE_STATUSES, + toDriverInstanceStatusLifecycleEventName, +} from "../../domain/driver-instance-lifecycle.machine"; +import { toRuntimeSubjectStatusLifecycleEventName } from "../../domain/runtime-subject-lifecycle.machine"; +import { ACTIVE_SESSION_RUN_STATUSES } from "../../domain/session-run-lifecycle.machine"; +import type { RuntimeSubjectOperationLease } from "./runtime-subject-store.types"; + +export interface RuntimeProvisioningLease { + readonly heartbeatAt: number; + readonly operationId: RuntimeOperationId; + readonly runId: SessionRunId | null; + readonly sandboxId: SandboxId; + readonly sandboxIncarnation: number | null; + readonly sandboxSessionId: SandboxSessionId | null; + readonly sessionId: SessionId; +} + +export interface RuntimeRunProvisioningLease extends RuntimeProvisioningLease { + readonly runId: SessionRunId; +} + +export interface RuntimeProvisioningCleanupTargets { + readonly conversationSessionId: SandboxSessionId | null; + readonly driverInstances: readonly { + readonly generation: number; + readonly id: DriverInstanceId; + readonly status: (typeof driverInstancesTable.$inferSelect)["status"]; + }[]; +} + +export type RuntimeProvisioningSubjectRetirementOutcome = + | { readonly kind: "cold" } + | { readonly kind: "destroying"; readonly lease: RuntimeSubjectOperationLease } + | { readonly kind: "repairing" } + | { readonly kind: "stale" } + | { readonly kind: "waiting" }; + +const RUNTIME_PROVISIONING_RETIRE_CLAIM_TTL_MS = 30 * 60_000; + +function runtimeProvisioningRetireClaimOwner(sessionId: SessionId): string { + return `runtime-provisioning-retire:${sessionId}`; +} + +const otherProvisioningSessionsTable = alias(sessionsTable, "other_runtime_provisioning_session"); +const otherProvisioningRunsTable = alias(sessionRunsTable, "other_runtime_provisioning_run"); +const otherProvisioningDriversTable = alias( + driverInstancesTable, + "other_runtime_provisioning_driver", +); +const retirementOwnerSessionsTable = alias(sessionsTable, "runtime_retirement_owner_session"); + +/** + * The durable subject status closes admission. This predicate only decides + * whether the current worker may advance from draining to physical teardown. + */ +export async function runtimeSubjectActivationRetirementIsDrained( + database: D1Database, + input: { + readonly lease: RuntimeSubjectOperationLease; + readonly runtimeSubjectId: SandboxId; + }, +): Promise { + if (input.lease.kind !== "activate" || input.lease.status !== "destroying") { + return true; + } + + const db = getAppDatabase(database); + const otherProvisioning = db + .select({ id: otherProvisioningSessionsTable.id }) + .from(otherProvisioningSessionsTable) + .where( + and( + eq(otherProvisioningSessionsTable.runtimeProvisioningSandboxId, input.runtimeSubjectId), + isNotNull(otherProvisioningSessionsTable.runtimeProvisioningOperationId), + ne(otherProvisioningSessionsTable.runtimeProvisioningOperationId, input.lease.operationId), + ), + ); + const ownedRun = db + .select({ id: retirementOwnerSessionsTable.id }) + .from(retirementOwnerSessionsTable) + .where( + and( + eq(retirementOwnerSessionsTable.runtimeProvisioningOperationId, input.lease.operationId), + eq(retirementOwnerSessionsTable.runtimeProvisioningSandboxId, input.runtimeSubjectId), + eq(retirementOwnerSessionsTable.runtimeProvisioningRunId, otherProvisioningRunsTable.id), + ), + ); + const otherActiveRun = db + .select({ id: otherProvisioningRunsTable.id }) + .from(otherProvisioningRunsTable) + .innerJoin( + otherProvisioningDriversTable, + eq(otherProvisioningDriversTable.id, otherProvisioningRunsTable.driverInstanceId), + ) + .where( + and( + eq(otherProvisioningDriversTable.sandboxId, input.runtimeSubjectId), + eq(otherProvisioningDriversTable.sandboxIncarnation, input.lease.incarnation), + inArray(otherProvisioningRunsTable.status, ACTIVE_SESSION_RUN_STATUSES), + notExists(ownedRun), + ), + ); + const drained = await db + .select({ id: sandboxesTable.id }) + .from(sandboxesTable) + .where( + and( + eq(sandboxesTable.id, input.runtimeSubjectId), + eq(sandboxesTable.incarnation, input.lease.incarnation), + eq(sandboxesTable.status, "destroying"), + eq(sandboxesTable.operationKind, "activate"), + eq(sandboxesTable.statusOperationId, input.lease.operationId), + eq(sandboxesTable.claimOwner, input.lease.claimOwner), + notExists(otherProvisioning), + notExists(otherActiveRun), + ), + ) + .limit(1) + .get(); + + return drained !== undefined; +} + +export async function claimRuntimeProvisioningDriverCleanup( + database: D1Database, + input: { + readonly driverGeneration: number; + readonly driverInstanceId: DriverInstanceId; + readonly lease: RuntimeProvisioningLease; + readonly source: "api" | "maintenance"; + }, +): Promise { + if (input.lease.sandboxIncarnation === null) { + return false; + } + const db = getAppDatabase(database); + const ownedLease = db + .select({ id: sessionsTable.id }) + .from(sessionsTable) + .where(runtimeProvisioningLeaseCondition(input.lease)); + const now = currentTimestampMs(); + const claimed = await db + .update(driverInstancesTable) + .set({ + status: "stopping", + statusChangedAt: now, + statusEvent: toDriverInstanceStatusLifecycleEventName("stopping"), + statusOperationId: input.lease.operationId, + statusSeq: sql`${driverInstancesTable.statusSeq} + 1`, + statusSource: input.source, + updatedAt: now, + }) + .where( + and( + eq(driverInstancesTable.id, input.driverInstanceId), + eq(driverInstancesTable.generation, input.driverGeneration), + eq(driverInstancesTable.sandboxId, input.lease.sandboxId), + eq(driverInstancesTable.sandboxIncarnation, input.lease.sandboxIncarnation), + eq(driverInstancesTable.sandboxSessionId, input.lease.sessionId), + inArray(driverInstancesTable.status, ASSIGNABLE_DRIVER_INSTANCE_STATUSES), + isNull(driverInstancesTable.statusOperationId), + exists(ownedLease), + ), + ) + .returning({ id: driverInstancesTable.id }) + .get(); + if (claimed !== undefined) { + return true; + } + + const adopted = await db + .select({ id: driverInstancesTable.id }) + .from(driverInstancesTable) + .where( + and( + eq(driverInstancesTable.id, input.driverInstanceId), + eq(driverInstancesTable.generation, input.driverGeneration), + eq(driverInstancesTable.sandboxId, input.lease.sandboxId), + eq(driverInstancesTable.sandboxIncarnation, input.lease.sandboxIncarnation), + eq(driverInstancesTable.sandboxSessionId, input.lease.sessionId), + eq(driverInstancesTable.status, "stopping"), + eq(driverInstancesTable.statusOperationId, input.lease.operationId), + exists(ownedLease), + ), + ) + .limit(1) + .get(); + + return adopted !== undefined; +} + +export async function claimRuntimeRunProvisioningLease( + database: D1Database, + input: { + readonly runId: SessionRunId; + readonly sandboxId: SandboxId; + readonly sessionId: SessionId; + }, +): Promise { + const heartbeatAt = currentTimestampMs(); + const operationId = createPlatformId(); + const db = getAppDatabase(database); + const activeRun = db + .select({ id: sessionRunsTable.id }) + .from(sessionRunsTable) + .where( + and( + eq(sessionRunsTable.id, input.runId), + eq(sessionRunsTable.sessionId, input.sessionId), + inArray(sessionRunsTable.status, ACTIVE_SESSION_RUN_STATUSES), + ), + ); + const availableSubject = db + .select({ id: sandboxesTable.id }) + .from(sandboxesTable) + .where( + and( + eq(sandboxesTable.id, input.sandboxId), + inArray(sandboxesTable.status, ["active", "cold"]), + isNull(sandboxesTable.operationKind), + isNull(sandboxesTable.statusOperationId), + or( + isNull(sandboxesTable.claimOwner), + isNull(sandboxesTable.claimExpiresAt), + lte(sandboxesTable.claimExpiresAt, heartbeatAt), + ), + ), + ); + const otherProvisioning = db + .select({ id: otherProvisioningSessionsTable.id }) + .from(otherProvisioningSessionsTable) + .where( + and( + ne(otherProvisioningSessionsTable.id, input.sessionId), + eq(otherProvisioningSessionsTable.runtimeProvisioningSandboxId, input.sandboxId), + isNotNull(otherProvisioningSessionsTable.runtimeProvisioningOperationId), + ), + ); + const claimed = await db + .update(sessionsTable) + .set({ + runtimeProvisioningHeartbeatAt: heartbeatAt, + runtimeProvisioningOperationId: operationId, + runtimeProvisioningRunId: input.runId, + runtimeProvisioningSandboxId: input.sandboxId, + runtimeProvisioningSandboxIncarnation: null, + runtimeProvisioningSandboxSessionId: null, + }) + .where( + and( + eq(sessionsTable.id, input.sessionId), + eq(sessionsTable.lastRunId, input.runId), + eq(sessionsTable.status, "RUNNING"), + isNull(sessionsTable.archivedAt), + isNull(sessionsTable.cleanupOperationKind), + isNull(sessionsTable.statusOperationId), + isNull(sessionsTable.runtimeProvisioningOperationId), + exists(activeRun), + exists(availableSubject), + notExists(otherProvisioning), + ), + ) + .returning({ id: sessionsTable.id }) + .get(); + + return claimed === undefined + ? null + : { + heartbeatAt, + operationId, + runId: input.runId, + sandboxId: input.sandboxId, + sandboxIncarnation: null, + sandboxSessionId: null, + sessionId: input.sessionId, + }; +} + +export async function recordRuntimeProvisioningConversationTarget( + database: D1Database, + input: { + readonly lease: RuntimeRunProvisioningLease; + readonly sandboxIncarnation: number; + readonly sandboxSessionId: SandboxSessionId; + }, +): Promise { + if ( + (input.lease.sandboxIncarnation !== null && + input.lease.sandboxIncarnation !== input.sandboxIncarnation) || + (input.lease.sandboxSessionId !== null && + input.lease.sandboxSessionId !== input.sandboxSessionId) + ) { + throw new Error("Runtime provisioning cannot change its immutable conversation target."); + } + + if (input.lease.sandboxSessionId === input.sandboxSessionId) { + return input.lease; + } + + const heartbeatAt = currentTimestampMs(); + const updated = await getAppDatabase(database) + .update(sessionsTable) + .set({ + runtimeProvisioningHeartbeatAt: heartbeatAt, + runtimeProvisioningSandboxIncarnation: input.sandboxIncarnation, + runtimeProvisioningSandboxSessionId: input.sandboxSessionId, + }) + .where(runtimeProvisioningLeaseCondition(input.lease)) + .returning({ id: sessionsTable.id }) + .get(); + + return updated === undefined + ? null + : { + ...input.lease, + heartbeatAt, + sandboxIncarnation: input.sandboxIncarnation, + sandboxSessionId: input.sandboxSessionId, + }; +} + +export async function recordRuntimeProvisioningSandboxIncarnation( + database: D1Database, + input: { + readonly lease: RuntimeRunProvisioningLease; + readonly sandboxIncarnation: number; + }, +): Promise { + if (!Number.isSafeInteger(input.sandboxIncarnation) || input.sandboxIncarnation < 0) { + throw new Error("Runtime provisioning sandbox incarnation must be non-negative."); + } + if ( + input.lease.sandboxIncarnation !== null && + input.lease.sandboxIncarnation !== input.sandboxIncarnation + ) { + throw new Error("Runtime provisioning cannot change its immutable sandbox incarnation."); + } + if (input.lease.sandboxIncarnation === input.sandboxIncarnation) { + return input.lease; + } + + const heartbeatAt = currentTimestampMs(); + const updated = await getAppDatabase(database) + .update(sessionsTable) + .set({ + runtimeProvisioningHeartbeatAt: heartbeatAt, + runtimeProvisioningSandboxIncarnation: input.sandboxIncarnation, + }) + .where(runtimeProvisioningLeaseCondition(input.lease)) + .returning({ id: sessionsTable.id }) + .get(); + return updated === undefined + ? null + : { + ...input.lease, + heartbeatAt, + sandboxIncarnation: input.sandboxIncarnation, + }; +} + +/** Poison N durably first; the expiring claim only elects its teardown worker. */ +export async function claimRuntimeProvisioningSubjectRetirement( + database: D1Database, + input: { + readonly lease: RuntimeProvisioningLease; + readonly source: "api" | "maintenance"; + }, +): Promise { + const { lease } = input; + if (lease.sandboxIncarnation === null) { + return { kind: "stale" }; + } + + const now = currentTimestampMs(); + const claimExpiresAt = now + RUNTIME_PROVISIONING_RETIRE_CLAIM_TTL_MS; + const claimOwner = runtimeProvisioningRetireClaimOwner(lease.sessionId); + const db = getAppDatabase(database); + const ownedLease = db + .select({ id: sessionsTable.id }) + .from(sessionsTable) + .where(runtimeProvisioningLeaseCondition(lease)); + + await db + .update(sandboxesTable) + .set({ + claimExpiresAt, + claimOwner, + lastError: "Runtime provisioning left an ambiguous physical mutation.", + lastErrorCode: "runtime.subject_activation_failed", + operationKind: "activate", + status: "destroying", + statusChangedAt: now, + statusEvent: toRuntimeSubjectStatusLifecycleEventName("destroying"), + statusOperationId: lease.operationId, + statusSeq: sql`${sandboxesTable.statusSeq} + 1`, + statusSource: input.source, + updatedAt: now, + }) + .where( + and( + eq(sandboxesTable.id, lease.sandboxId), + eq(sandboxesTable.incarnation, lease.sandboxIncarnation), + eq(sandboxesTable.status, "active"), + isNull(sandboxesTable.operationKind), + isNull(sandboxesTable.statusOperationId), + or( + isNull(sandboxesTable.claimOwner), + isNull(sandboxesTable.claimExpiresAt), + lte(sandboxesTable.claimExpiresAt, now), + eq(sandboxesTable.claimOwner, claimOwner), + ), + exists(ownedLease), + ), + ) + .run(); + + // A stale provisioning takeover rotates its operation id. Adopt the durable + // poison only when no different worker still owns the subject claim. + await db + .update(sandboxesTable) + .set({ + claimExpiresAt, + claimOwner, + statusOperationId: lease.operationId, + statusSource: input.source, + updatedAt: now, + }) + .where( + and( + eq(sandboxesTable.id, lease.sandboxId), + eq(sandboxesTable.incarnation, lease.sandboxIncarnation), + eq(sandboxesTable.status, "destroying"), + eq(sandboxesTable.operationKind, "activate"), + or( + eq(sandboxesTable.claimOwner, claimOwner), + isNull(sandboxesTable.claimOwner), + isNull(sandboxesTable.claimExpiresAt), + lte(sandboxesTable.claimExpiresAt, now), + ), + exists(ownedLease), + ), + ) + .run(); + + const state = await db + .select({ + claimExpiresAt: sandboxesTable.claimExpiresAt, + claimOwner: sandboxesTable.claimOwner, + incarnation: sandboxesTable.incarnation, + operationId: sandboxesTable.statusOperationId, + operationKind: sandboxesTable.operationKind, + status: sandboxesTable.status, + }) + .from(sessionsTable) + .innerJoin(sandboxesTable, eq(sandboxesTable.id, sessionsTable.runtimeProvisioningSandboxId)) + .where(runtimeProvisioningLeaseCondition(lease)) + .limit(1) + .get(); + if (state === undefined || state.incarnation !== lease.sandboxIncarnation) { + return { kind: "stale" }; + } + if (state.status === "cold") { + return { kind: "cold" }; + } + if ( + state.status !== "destroying" || + state.operationKind !== "activate" || + state.operationId !== lease.operationId || + state.claimOwner !== claimOwner || + state.claimExpiresAt === null + ) { + return state.status === "destroying" && state.operationKind === "activate" + ? { kind: "repairing" } + : { kind: "waiting" }; + } + + const operationLease: RuntimeSubjectOperationLease = { + claimExpiresAt: state.claimExpiresAt, + claimOwner, + incarnation: lease.sandboxIncarnation, + kind: "activate", + operationId: lease.operationId, + status: "destroying", + }; + return (await runtimeSubjectActivationRetirementIsDrained(database, { + lease: operationLease, + runtimeSubjectId: lease.sandboxId, + })) + ? { kind: "destroying", lease: operationLease } + : { kind: "waiting" }; +} + +export async function heartbeatRuntimeRunProvisioningLease( + database: D1Database, + lease: RuntimeRunProvisioningLease, +): Promise { + const heartbeatAt = currentTimestampMs(); + const db = getAppDatabase(database); + const activeRun = db + .select({ id: sessionRunsTable.id }) + .from(sessionRunsTable) + .where( + and( + eq(sessionRunsTable.id, lease.runId), + eq(sessionRunsTable.sessionId, lease.sessionId), + inArray(sessionRunsTable.status, ACTIVE_SESSION_RUN_STATUSES), + ), + ); + const updated = await db + .update(sessionsTable) + .set({ runtimeProvisioningHeartbeatAt: heartbeatAt }) + .where( + and( + runtimeProvisioningLeaseCondition(lease), + eq(sessionsTable.lastRunId, lease.runId), + eq(sessionsTable.status, "RUNNING"), + isNull(sessionsTable.archivedAt), + isNull(sessionsTable.cleanupOperationKind), + isNull(sessionsTable.statusOperationId), + exists(activeRun), + ), + ) + .returning({ id: sessionsTable.id }) + .get(); + + return updated !== undefined; +} + +export async function releaseReadyRuntimeRunProvisioningLease( + database: D1Database, + input: { + readonly driverGeneration: number; + readonly driverInstanceId: DriverInstanceId; + readonly lease: RuntimeRunProvisioningLease; + }, +): Promise { + if (input.lease.sandboxIncarnation === null) { + return false; + } + const db = getAppDatabase(database); + const activeConversation = db + .select({ id: sandboxSessionsTable.sessionId }) + .from(sandboxSessionsTable) + .where( + and( + eq(sandboxSessionsTable.sessionId, input.lease.sessionId), + eq(sandboxSessionsTable.sandboxId, input.lease.sandboxId), + input.lease.sandboxIncarnation === null + ? isNull(sandboxSessionsTable.sandboxIncarnation) + : eq(sandboxSessionsTable.sandboxIncarnation, input.lease.sandboxIncarnation), + input.lease.sandboxSessionId === null + ? isNull(sandboxSessionsTable.sandboxSessionId) + : eq(sandboxSessionsTable.sandboxSessionId, input.lease.sandboxSessionId), + eq(sandboxSessionsTable.status, "active"), + ), + ); + const linkedRun = db + .select({ id: sessionRunsTable.id }) + .from(sessionRunsTable) + .where( + and( + eq(sessionRunsTable.id, input.lease.runId), + eq(sessionRunsTable.sessionId, input.lease.sessionId), + eq(sessionRunsTable.driverInstanceId, input.driverInstanceId), + inArray(sessionRunsTable.status, ACTIVE_SESSION_RUN_STATUSES), + ), + ); + const durableDriver = db + .select({ id: driverInstancesTable.id }) + .from(driverInstancesTable) + .where( + and( + eq(driverInstancesTable.id, input.driverInstanceId), + eq(driverInstancesTable.generation, input.driverGeneration), + eq(driverInstancesTable.sandboxId, input.lease.sandboxId), + eq(driverInstancesTable.sandboxIncarnation, input.lease.sandboxIncarnation), + eq(driverInstancesTable.sandboxSessionId, input.lease.sessionId), + inArray(driverInstancesTable.status, ASSIGNABLE_DRIVER_INSTANCE_STATUSES), + isNull(driverInstancesTable.statusOperationId), + ), + ); + const released = await db + .update(sessionsTable) + .set(runtimeProvisioningLeaseReleasePatch()) + .where( + and( + runtimeProvisioningLeaseCondition(input.lease), + eq(sessionsTable.lastRunId, input.lease.runId), + eq(sessionsTable.status, "RUNNING"), + isNull(sessionsTable.archivedAt), + isNull(sessionsTable.cleanupOperationKind), + isNull(sessionsTable.statusOperationId), + exists(activeConversation), + exists(linkedRun), + exists(durableDriver), + ), + ) + .returning({ id: sessionsTable.id }) + .get(); + + return released !== undefined; +} + +export async function adoptReadyRuntimeRunProvisioningLease( + database: D1Database, + lease: RuntimeProvisioningLease, +): Promise { + if (lease.runId === null || lease.sandboxIncarnation === null) { + return false; + } + const driver = await getAppDatabase(database) + .select({ + generation: driverInstancesTable.generation, + id: driverInstancesTable.id, + }) + .from(sessionRunsTable) + .innerJoin(driverInstancesTable, eq(driverInstancesTable.id, sessionRunsTable.driverInstanceId)) + .where( + and( + eq(sessionRunsTable.id, lease.runId), + eq(sessionRunsTable.sessionId, lease.sessionId), + inArray(sessionRunsTable.status, ACTIVE_SESSION_RUN_STATUSES), + eq(driverInstancesTable.sandboxId, lease.sandboxId), + eq(driverInstancesTable.sandboxIncarnation, lease.sandboxIncarnation), + eq(driverInstancesTable.sandboxSessionId, lease.sessionId), + inArray(driverInstancesTable.status, ASSIGNABLE_DRIVER_INSTANCE_STATUSES), + isNull(driverInstancesTable.statusOperationId), + ), + ) + .limit(1) + .get(); + + return driver === undefined + ? false + : releaseReadyRuntimeRunProvisioningLease(database, { + driverGeneration: driver.generation, + driverInstanceId: driver.id, + lease: { ...lease, runId: lease.runId }, + }); +} + +export async function releaseAbortedRuntimeProvisioningLease( + database: D1Database, + lease: RuntimeProvisioningLease, +): Promise { + const released = await getAppDatabase(database) + .update(sessionsTable) + .set(runtimeProvisioningLeaseReleasePatch()) + .where(runtimeProvisioningLeaseCondition(lease)) + .returning({ id: sessionsTable.id }) + .get(); + + return released !== undefined; +} + +export async function renewRuntimeProvisioningLeaseOwnership( + database: D1Database, + lease: RuntimeProvisioningLease, +): Promise { + const renewed = await getAppDatabase(database) + .update(sessionsTable) + .set({ runtimeProvisioningHeartbeatAt: currentTimestampMs() }) + .where(runtimeProvisioningLeaseCondition(lease)) + .returning({ id: sessionsTable.id }) + .get(); + + return renewed !== undefined; +} + +export async function readRuntimeProvisioningCleanupTargets( + database: D1Database, + lease: RuntimeProvisioningLease, +): Promise { + const rows = await getAppDatabase(database) + .select({ + driverGeneration: driverInstancesTable.generation, + driverId: driverInstancesTable.id, + driverStatus: driverInstancesTable.status, + }) + .from(sessionsTable) + .leftJoin( + driverInstancesTable, + and( + eq(driverInstancesTable.sandboxSessionId, lease.sessionId), + eq(driverInstancesTable.sandboxId, lease.sandboxId), + ...(lease.sandboxIncarnation === null + ? [sql`0`] + : [eq(driverInstancesTable.sandboxIncarnation, lease.sandboxIncarnation)]), + ), + ) + .where(runtimeProvisioningLeaseCondition(lease)) + .all(); + + if (rows.length === 0) { + return null; + } + + return { + conversationSessionId: lease.sandboxSessionId, + driverInstances: rows.flatMap((row) => + row.driverId === null || row.driverGeneration === null || row.driverStatus === null + ? [] + : [{ generation: row.driverGeneration, id: row.driverId, status: row.driverStatus }], + ), + }; +} + +export async function claimStaleRuntimeProvisioningLeases( + database: D1Database, + input: { readonly heartbeatAtLte: number; readonly limit: number }, +): Promise { + if (!Number.isSafeInteger(input.heartbeatAtLte) || input.heartbeatAtLte < 0) { + throw new Error("Runtime provisioning stale heartbeat must be a non-negative integer."); + } + if (!Number.isSafeInteger(input.limit) || input.limit <= 0) { + throw new Error("Runtime provisioning repair limit must be a positive integer."); + } + const db = getAppDatabase(database); + const rows = await db + .select({ + heartbeatAt: sessionsTable.runtimeProvisioningHeartbeatAt, + operationId: sessionsTable.runtimeProvisioningOperationId, + runId: sessionsTable.runtimeProvisioningRunId, + sandboxId: sessionsTable.runtimeProvisioningSandboxId, + sandboxIncarnation: sessionsTable.runtimeProvisioningSandboxIncarnation, + sandboxSessionId: sessionsTable.runtimeProvisioningSandboxSessionId, + sessionId: sessionsTable.id, + }) + .from(sessionsTable) + .where( + and( + isNotNull(sessionsTable.runtimeProvisioningOperationId), + isNotNull(sessionsTable.runtimeProvisioningSandboxId), + isNotNull(sessionsTable.runtimeProvisioningHeartbeatAt), + lte(sessionsTable.runtimeProvisioningHeartbeatAt, input.heartbeatAtLte), + ), + ) + .orderBy(sessionsTable.runtimeProvisioningHeartbeatAt, sessionsTable.id) + .limit(input.limit) + .all(); + const claims: RuntimeProvisioningLease[] = []; + + for (const row of rows) { + if (row.heartbeatAt === null || row.operationId === null || row.sandboxId === null) { + continue; + } + const previous: RuntimeProvisioningLease = { + heartbeatAt: row.heartbeatAt, + operationId: row.operationId, + runId: row.runId, + sandboxId: row.sandboxId, + sandboxIncarnation: row.sandboxIncarnation, + sandboxSessionId: row.sandboxSessionId, + sessionId: row.sessionId, + }; + const operationId = createPlatformId(); + const heartbeatAt = currentTimestampMs(); + const claimed = await db + .update(sessionsTable) + .set({ + runtimeProvisioningHeartbeatAt: heartbeatAt, + runtimeProvisioningOperationId: operationId, + }) + .where(runtimeProvisioningLeaseCondition(previous, true)) + .returning({ id: sessionsTable.id }) + .get(); + if (claimed !== undefined) { + claims.push({ ...previous, heartbeatAt, operationId }); + } + } + + return claims; +} + +function runtimeProvisioningLeaseCondition( + lease: RuntimeProvisioningLease, + includeHeartbeat = false, +) { + return and( + eq(sessionsTable.id, lease.sessionId), + eq(sessionsTable.runtimeProvisioningOperationId, lease.operationId), + lease.runId === null + ? isNull(sessionsTable.runtimeProvisioningRunId) + : eq(sessionsTable.runtimeProvisioningRunId, lease.runId), + eq(sessionsTable.runtimeProvisioningSandboxId, lease.sandboxId), + lease.sandboxIncarnation === null + ? isNull(sessionsTable.runtimeProvisioningSandboxIncarnation) + : eq(sessionsTable.runtimeProvisioningSandboxIncarnation, lease.sandboxIncarnation), + lease.sandboxSessionId === null + ? isNull(sessionsTable.runtimeProvisioningSandboxSessionId) + : eq(sessionsTable.runtimeProvisioningSandboxSessionId, lease.sandboxSessionId), + ...(includeHeartbeat + ? [eq(sessionsTable.runtimeProvisioningHeartbeatAt, lease.heartbeatAt)] + : []), + ); +} + +function runtimeProvisioningLeaseReleasePatch() { + return { + runtimeProvisioningHeartbeatAt: null, + runtimeProvisioningOperationId: null, + runtimeProvisioningRunId: null, + runtimeProvisioningSandboxId: null, + runtimeProvisioningSandboxIncarnation: null, + runtimeProvisioningSandboxSessionId: null, + } as const; +} diff --git a/apps/api/src/modules/runtime/infrastructure/runtime-subject-lifecycle/runtime-run-lease-store.ts b/apps/api/src/modules/runtime/infrastructure/runtime-subject-lifecycle/runtime-run-lease-store.ts index 49aea3df..1571b1ae 100644 --- a/apps/api/src/modules/runtime/infrastructure/runtime-subject-lifecycle/runtime-run-lease-store.ts +++ b/apps/api/src/modules/runtime/infrastructure/runtime-subject-lifecycle/runtime-run-lease-store.ts @@ -4,11 +4,21 @@ import { sandboxesTable, sessionRunsTable, } from "@mosoo/db"; -import type { DriverInstanceId, SandboxId, SessionId, SessionRunId } from "@mosoo/id"; -import { and, eq, inArray, isNull, ne, notExists, or, sql } from "drizzle-orm"; +import type { + DriverInstanceId, + RuntimeOperationId, + SandboxId, + SessionId, + SessionRunId, +} from "@mosoo/id"; +import { and, eq, exists, inArray, isNull, ne, notExists, or, sql } from "drizzle-orm"; import { alias } from "drizzle-orm/sqlite-core"; -import { getAppDatabase } from "../../../../platform/db/drizzle"; +import { + getAppDatabase, + getD1ChangeCount, + runAppDatabaseBatch, +} from "../../../../platform/db/drizzle"; import { currentTimestampMs } from "../../../../time"; import { ACTIVE_SESSION_RUN_STATUSES } from "../../domain/session-run-lifecycle.machine"; import { @@ -58,16 +68,24 @@ export type RuntimeRunLeaseTransitionOutcome = interface RuntimeRunLeaseAcquireSnapshot { readonly driverActiveSessionRunId: SessionRunId | null; + readonly driverGeneration: number; readonly driverSandboxId: SandboxId; + readonly driverSandboxIncarnation: number; readonly driverSandboxSessionId: SessionId; readonly driverStatus: string; + readonly driverStatusOperationId: RuntimeOperationId | null; readonly runDriverInstanceId: DriverInstanceId | null; readonly runId: SessionRunId | null; readonly runSessionId: SessionId | null; readonly runStatus: string | null; readonly runStatusSeq: number | null; readonly sandboxId: SandboxId; + readonly sandboxSessionIncarnation: number | null; readonly sandboxSessionStatus: string | null; + readonly subjectIncarnation: number | null; + readonly subjectClaimOwner: string | null; + readonly subjectOperationId: RuntimeOperationId | null; + readonly subjectStatus: string | null; } const activeDriverLeaseRunsTable = alias(sessionRunsTable, "active_driver_lease"); @@ -106,7 +124,7 @@ export async function recordRuntimeRunLeaseAcquiredOutcome( return admission; } - const linked = await recordRuntimeRunLeaseLinked(appDb, { + const linked = await recordRuntimeRunLeaseLinked(database, { ...input, now, sandboxId: snapshot.sandboxId, @@ -122,6 +140,14 @@ export async function recordRuntimeRunLeaseAcquiredOutcome( }; } + if (linked === "driver_changed") { + return { + reason: "driver_changed", + status: "stale", + transition: "acquire", + }; + } + return { reason: "run_changed", status: "stale", @@ -147,16 +173,24 @@ async function readRuntimeRunLeaseAcquireSnapshot( const row = (await appDb .select({ + driverGeneration: driverInstancesTable.generation, driverSandboxId: driverInstancesTable.sandboxId, + driverSandboxIncarnation: driverInstancesTable.sandboxIncarnation, driverSandboxSessionId: driverInstancesTable.sandboxSessionId, driverStatus: driverInstancesTable.status, + driverStatusOperationId: driverInstancesTable.statusOperationId, runDriverInstanceId: sessionRunsTable.driverInstanceId, runId: sessionRunsTable.id, runSessionId: sessionRunsTable.sessionId, runStatus: sessionRunsTable.status, runStatusSeq: sessionRunsTable.statusSeq, sandboxId: driverInstancesTable.sandboxId, + sandboxSessionIncarnation: sandboxSessionsTable.sandboxIncarnation, sandboxSessionStatus: sandboxSessionsTable.status, + subjectIncarnation: sandboxesTable.incarnation, + subjectClaimOwner: sandboxesTable.claimOwner, + subjectOperationId: sandboxesTable.statusOperationId, + subjectStatus: sandboxesTable.status, }) .from(driverInstancesTable) .leftJoin(sessionRunsTable, eq(sessionRunsTable.id, input.sessionRunId)) @@ -167,6 +201,7 @@ async function readRuntimeRunLeaseAcquireSnapshot( eq(sandboxSessionsTable.sessionId, input.sessionId), ), ) + .leftJoin(sandboxesTable, eq(sandboxesTable.id, input.runtimeSubjectId)) .where(eq(driverInstancesTable.id, input.driverInstanceId)) .limit(1) .get()) ?? null; @@ -198,6 +233,14 @@ function decideRuntimeRunLeaseAcquire( input: RuntimeRunLeaseInput, snapshot: RuntimeRunLeaseAcquireSnapshot, ): RuntimeRunLeaseTransitionOutcome { + if (snapshot.driverGeneration !== input.driverGeneration) { + return { + reason: "driver_changed", + status: "stale", + transition: "acquire", + }; + } + if (snapshot.runId === null) { return { reason: "run_not_found", @@ -208,6 +251,7 @@ function decideRuntimeRunLeaseAcquire( if ( snapshot.driverSandboxId !== input.runtimeSubjectId || + snapshot.driverSandboxIncarnation !== input.runtimeSubjectIncarnation || snapshot.driverSandboxSessionId !== input.sessionId ) { return { @@ -237,7 +281,14 @@ function decideRuntimeRunLeaseAcquire( }; } - if (snapshot.sandboxSessionStatus !== "active") { + if ( + snapshot.sandboxSessionStatus !== "active" || + snapshot.sandboxSessionIncarnation !== input.runtimeSubjectIncarnation || + snapshot.subjectIncarnation !== input.runtimeSubjectIncarnation || + snapshot.subjectStatus !== "active" || + snapshot.subjectClaimOwner !== null || + snapshot.subjectOperationId !== null + ) { return { reason: "sandbox_session_not_active", status: "rejected", @@ -246,6 +297,7 @@ function decideRuntimeRunLeaseAcquire( } if ( + snapshot.driverStatusOperationId !== null || !ASSIGNABLE_DRIVER_STATUSES.includes( snapshot.driverStatus as (typeof ASSIGNABLE_DRIVER_STATUSES)[number], ) @@ -294,57 +346,154 @@ function decideRuntimeRunLeaseAcquire( } async function recordRuntimeRunLeaseLinked( - appDb: AppDatabase, + database: D1Database, input: RuntimeRunLeaseInput & { readonly now: number; readonly sandboxId: SandboxId; readonly statusSeq: number | null; }, -): Promise<"linked" | "run_changed" | "run_link_conflict"> { +): Promise<"driver_changed" | "linked" | "run_changed" | "run_link_conflict"> { if (input.statusSeq === null) { return "run_changed"; } + const statusSeq = input.statusSeq; - const linked = - (await appDb - .update(sessionRunsTable) - .set({ - driverInstanceId: input.driverInstanceId, - updatedAt: sql` - CASE - WHEN ${sessionRunsTable.driverInstanceId} IS NULL THEN ${input.now} - ELSE ${sessionRunsTable.updatedAt} - END - `, - }) + const [linked] = await runAppDatabaseBatch(database, (db) => { + const activeConversation = db + .select({ id: sandboxSessionsTable.sessionId }) + .from(sandboxSessionsTable) + .where( + and( + eq(sandboxSessionsTable.sandboxId, input.runtimeSubjectId), + eq(sandboxSessionsTable.sandboxIncarnation, input.runtimeSubjectIncarnation), + eq(sandboxSessionsTable.sessionId, input.sessionId), + eq(sandboxSessionsTable.status, "active"), + ), + ); + const activeSubject = db + .select({ id: sandboxesTable.id }) + .from(sandboxesTable) + .where( + and( + eq(sandboxesTable.id, input.runtimeSubjectId), + eq(sandboxesTable.incarnation, input.runtimeSubjectIncarnation), + eq(sandboxesTable.status, "active"), + isNull(sandboxesTable.claimOwner), + isNull(sandboxesTable.operationKind), + isNull(sandboxesTable.statusOperationId), + ), + ); + const assignableDriver = db + .select({ id: driverInstancesTable.id }) + .from(driverInstancesTable) + .where( + and( + eq(driverInstancesTable.id, input.driverInstanceId), + eq(driverInstancesTable.generation, input.driverGeneration), + eq(driverInstancesTable.sandboxId, input.runtimeSubjectId), + eq(driverInstancesTable.sandboxIncarnation, input.runtimeSubjectIncarnation), + eq(driverInstancesTable.sandboxSessionId, input.sessionId), + inArray(driverInstancesTable.status, ASSIGNABLE_DRIVER_STATUSES), + isNull(driverInstancesTable.statusOperationId), + exists(activeConversation), + exists(activeSubject), + ), + ); + const exactLinkedRun = db + .select({ id: sessionRunsTable.id }) + .from(sessionRunsTable) .where( and( eq(sessionRunsTable.id, input.sessionRunId), eq(sessionRunsTable.sessionId, input.sessionId), - eq(sessionRunsTable.statusSeq, input.statusSeq), + eq(sessionRunsTable.statusSeq, statusSeq), + eq(sessionRunsTable.driverInstanceId, input.driverInstanceId), inArray(sessionRunsTable.status, ACTIVE_SESSION_RUN_STATUSES), - or( - isNull(sessionRunsTable.driverInstanceId), - eq(sessionRunsTable.driverInstanceId, input.driverInstanceId), - ), - notExists( - appDb - .select({ id: activeDriverLeaseRunsTable.id }) - .from(activeDriverLeaseRunsTable) - .where( - and( - eq(activeDriverLeaseRunsTable.driverInstanceId, input.driverInstanceId), - ne(activeDriverLeaseRunsTable.id, input.sessionRunId), - inArray(activeDriverLeaseRunsTable.status, ACTIVE_SESSION_RUN_STATUSES), + ), + ); + + return [ + db + .update(sessionRunsTable) + .set({ + driverInstanceId: input.driverInstanceId, + updatedAt: sql` + CASE + WHEN ${sessionRunsTable.driverInstanceId} IS NULL THEN ${input.now} + ELSE ${sessionRunsTable.updatedAt} + END + `, + }) + .where( + and( + eq(sessionRunsTable.id, input.sessionRunId), + eq(sessionRunsTable.sessionId, input.sessionId), + eq(sessionRunsTable.statusSeq, statusSeq), + inArray(sessionRunsTable.status, ACTIVE_SESSION_RUN_STATUSES), + or( + isNull(sessionRunsTable.driverInstanceId), + eq(sessionRunsTable.driverInstanceId, input.driverInstanceId), + ), + exists(assignableDriver), + notExists( + db + .select({ id: activeDriverLeaseRunsTable.id }) + .from(activeDriverLeaseRunsTable) + .where( + and( + eq(activeDriverLeaseRunsTable.driverInstanceId, input.driverInstanceId), + ne(activeDriverLeaseRunsTable.id, input.sessionRunId), + inArray(activeDriverLeaseRunsTable.status, ACTIVE_SESSION_RUN_STATUSES), + ), ), - ), + ), ), ), - ) - .returning({ id: sessionRunsTable.id }) - .get()) ?? null; + db + .update(sandboxesTable) + .set({ + inactiveDeadlineAt: null, + updatedAt: sql` + CASE + WHEN ${sandboxesTable.inactiveDeadlineAt} IS NULL THEN ${sandboxesTable.updatedAt} + ELSE ${input.now} + END + `, + }) + .where( + and( + eq(sandboxesTable.id, input.sandboxId), + eq(sandboxesTable.incarnation, input.runtimeSubjectIncarnation), + eq(sandboxesTable.status, "active"), + isNull(sandboxesTable.claimOwner), + isNull(sandboxesTable.operationKind), + isNull(sandboxesTable.statusOperationId), + exists(exactLinkedRun), + ), + ), + ]; + }); - if (linked === null) { + if (getD1ChangeCount(linked) === 0) { + const appDb = getAppDatabase(database); + const driver = await appDb + .select({ + generation: driverInstancesTable.generation, + sandboxIncarnation: driverInstancesTable.sandboxIncarnation, + statusOperationId: driverInstancesTable.statusOperationId, + }) + .from(driverInstancesTable) + .where(eq(driverInstancesTable.id, input.driverInstanceId)) + .limit(1) + .get(); + if ( + driver === undefined || + driver.generation !== input.driverGeneration || + driver.sandboxIncarnation !== input.runtimeSubjectIncarnation || + driver.statusOperationId !== null + ) { + return "driver_changed"; + } const current = (await appDb .select({ @@ -370,20 +519,6 @@ async function recordRuntimeRunLeaseLinked( return "run_link_conflict"; } - await appDb - .update(sandboxesTable) - .set({ - inactiveDeadlineAt: null, - updatedAt: sql` - CASE - WHEN ${sandboxesTable.inactiveDeadlineAt} IS NULL THEN ${sandboxesTable.updatedAt} - ELSE ${input.now} - END - `, - }) - .where(eq(sandboxesTable.id, input.sandboxId)) - .run(); - return "linked"; } @@ -391,7 +526,10 @@ export async function recordRuntimeRunLeaseReleased( database: D1Database, input: { readonly driverInstanceId: DriverInstanceId; + readonly expectedDriverGeneration: number; + readonly expectedDriverOperationId?: RuntimeOperationId; readonly expectedSessionRunId: SessionRunId; + readonly retainDriverOperationUntilTerminal?: boolean; }, ): Promise { const outcome = await recordRuntimeRunLeaseReleasedOutcome(database, input); @@ -402,14 +540,24 @@ export async function recordRuntimeRunLeaseReleasedOutcome( database: D1Database, input: { readonly driverInstanceId: DriverInstanceId; + readonly expectedDriverGeneration: number; + readonly expectedDriverOperationId?: RuntimeOperationId; readonly expectedSessionRunId: SessionRunId; + readonly retainDriverOperationUntilTerminal?: boolean; }, ): Promise { + if ( + input.retainDriverOperationUntilTerminal === true && + input.expectedDriverOperationId === undefined + ) { + throw new Error("A retained Driver release requires exact operation ownership."); + } const now = currentTimestampMs(); const appDb = getAppDatabase(database); const driver = (await appDb .select({ + generation: driverInstancesTable.generation, sandboxId: driverInstancesTable.sandboxId, }) .from(driverInstancesTable) @@ -425,6 +573,14 @@ export async function recordRuntimeRunLeaseReleasedOutcome( }; } + if (driver.generation !== input.expectedDriverGeneration) { + return { + reason: "driver_changed", + status: "stale", + transition: "release", + }; + } + const currentRun = (await appDb .select({ @@ -449,15 +605,15 @@ export async function recordRuntimeRunLeaseReleasedOutcome( .limit(1) .get()) ?? null; - if (currentRun === null || currentRun.driverInstanceId === null) { - if (activeDriverRun !== null && activeDriverRun.id !== input.expectedSessionRunId) { - return { - reason: "lease_mismatch", - status: "stale", - transition: "release", - }; - } + if (activeDriverRun !== null && activeDriverRun.id !== input.expectedSessionRunId) { + return { + reason: "lease_mismatch", + status: "stale", + transition: "release", + }; + } + if (currentRun === null || currentRun.driverInstanceId === null) { return { reason: "lease_missing", status: "rejected", @@ -473,13 +629,25 @@ export async function recordRuntimeRunLeaseReleasedOutcome( }; } - if ( - ACTIVE_SESSION_RUN_STATUSES.includes( - currentRun.status as (typeof ACTIVE_SESSION_RUN_STATUSES)[number], - ) - ) { - const released = - (await appDb + const currentRunIsActive = ACTIVE_SESSION_RUN_STATUSES.includes( + currentRun.status as (typeof ACTIVE_SESSION_RUN_STATUSES)[number], + ); + const [released, , driverFence] = await runAppDatabaseBatch(database, (db) => { + const exactDriverGeneration = db + .select({ id: driverInstancesTable.id }) + .from(driverInstancesTable) + .where( + and( + eq(driverInstancesTable.id, input.driverInstanceId), + eq(driverInstancesTable.generation, input.expectedDriverGeneration), + ...(input.expectedDriverOperationId === undefined + ? [] + : [eq(driverInstancesTable.statusOperationId, input.expectedDriverOperationId)]), + ), + ); + + return [ + db .update(sessionRunsTable) .set({ driverInstanceId: null, @@ -490,39 +658,106 @@ export async function recordRuntimeRunLeaseReleasedOutcome( eq(sessionRunsTable.id, input.expectedSessionRunId), eq(sessionRunsTable.driverInstanceId, input.driverInstanceId), inArray(sessionRunsTable.status, ACTIVE_SESSION_RUN_STATUSES), + exists(exactDriverGeneration), ), - ) - .returning({ - id: sessionRunsTable.id, + ), + db + .update(sandboxesTable) + .set({ + inactiveDeadlineAt: sql`COALESCE( + ${sandboxesTable.inactiveDeadlineAt}, + ${getRuntimeSubjectInactiveDeadlineSql(now)} + )`, + updatedAt: sql`CASE + WHEN ${sandboxesTable.inactiveDeadlineAt} IS NULL THEN ${now} + ELSE ${sandboxesTable.updatedAt} + END`, }) - .get()) ?? null; + .where( + and( + eq(sandboxesTable.id, driver.sandboxId), + exists(exactDriverGeneration), + or( + eq(sandboxesTable.kind, "pet"), + notExists(activeConversationSessionQuery(db, driver.sandboxId)), + ), + notExists(runLeaseQuery(db, driver.sandboxId)), + ), + ), + db + .update(driverInstancesTable) + .set( + input.retainDriverOperationUntilTerminal === true + ? { + status: sql`CASE + WHEN ${driverInstancesTable.status} IN ('provisioning', 'connecting', 'ready') + THEN 'stopping' + ELSE ${driverInstancesTable.status} + END`, + statusChangedAt: sql`CASE + WHEN ${driverInstancesTable.status} IN ('provisioning', 'connecting', 'ready') + THEN ${now} + ELSE ${driverInstancesTable.statusChangedAt} + END`, + statusEvent: sql`CASE + WHEN ${driverInstancesTable.status} IN ('provisioning', 'connecting', 'ready') + THEN 'driver.stopping' + ELSE ${driverInstancesTable.statusEvent} + END`, + statusOperationId: sql`CASE + WHEN ${driverInstancesTable.status} IN ('stopped', 'failed') THEN NULL + ELSE ${driverInstancesTable.statusOperationId} + END`, + statusSeq: sql`CASE + WHEN ${driverInstancesTable.status} IN ('provisioning', 'connecting', 'ready') + THEN ${driverInstancesTable.statusSeq} + 1 + ELSE ${driverInstancesTable.statusSeq} + END`, + statusSource: sql`CASE + WHEN ${driverInstancesTable.status} IN ('provisioning', 'connecting', 'ready') + THEN 'api' + ELSE ${driverInstancesTable.statusSource} + END`, + updatedAt: sql`CASE + WHEN ${driverInstancesTable.status} IN ('provisioning', 'connecting', 'ready') + THEN ${now} + ELSE ${driverInstancesTable.updatedAt} + END`, + } + : { + statusOperationId: + input.expectedDriverOperationId === undefined + ? sql`${driverInstancesTable.statusOperationId}` + : null, + }, + ) + .where( + and( + eq(driverInstancesTable.id, input.driverInstanceId), + eq(driverInstancesTable.generation, input.expectedDriverGeneration), + ...(input.expectedDriverOperationId === undefined + ? [] + : [eq(driverInstancesTable.statusOperationId, input.expectedDriverOperationId)]), + ), + ), + ]; + }); - if (!released) { - return { - reason: "run_changed", - status: "stale", - transition: "release", - }; - } + if (getD1ChangeCount(driverFence) === 0) { + return { + reason: "driver_changed", + status: "stale", + transition: "release", + }; } - await appDb - .update(sandboxesTable) - .set({ - inactiveDeadlineAt: getRuntimeSubjectInactiveDeadlineSql(now), - updatedAt: now, - }) - .where( - and( - eq(sandboxesTable.id, driver.sandboxId), - or( - eq(sandboxesTable.kind, "pet"), - notExists(activeConversationSessionQuery(appDb, driver.sandboxId)), - ), - notExists(runLeaseQuery(appDb, driver.sandboxId)), - ), - ) - .run(); + if (currentRunIsActive && getD1ChangeCount(released) === 0) { + return { + reason: "run_changed", + status: "stale", + transition: "release", + }; + } return { repaired: false, diff --git a/apps/api/src/modules/runtime/infrastructure/runtime-subject-lifecycle/runtime-subject-driver-stop.ts b/apps/api/src/modules/runtime/infrastructure/runtime-subject-lifecycle/runtime-subject-driver-stop.ts index 9d4934e7..2976d65e 100644 --- a/apps/api/src/modules/runtime/infrastructure/runtime-subject-lifecycle/runtime-subject-driver-stop.ts +++ b/apps/api/src/modules/runtime/infrastructure/runtime-subject-lifecycle/runtime-subject-driver-stop.ts @@ -2,41 +2,31 @@ import type { DriverInstanceId } from "@mosoo/id"; import type { ApiBindings } from "../../../../platform/cloudflare/worker-types"; import type { StopRuntimeSubjectDriversInput } from "../../application/execution-plane/execution-plane-adapter"; -import { listLiveDriverInstanceIdsForSandboxSessions } from "../driver-instance/live-driver-instance.repository"; import { stopDriverSession } from "../driver-session.service"; import { listRuntimeSubjectDriverIds } from "./runtime-subject-store"; -async function listRuntimeSubjectOperationDriverIds( - bindings: ApiBindings, - input: StopRuntimeSubjectDriversInput, -): Promise { - if (input.targets !== undefined) { - return listLiveDriverInstanceIdsForSandboxSessions( - bindings.DB, - input.targets.map((target) => target.sessionId), - ); - } - - return listRuntimeSubjectDriverIds(bindings.DB, input.runtimeSubjectId); -} - export async function stopRuntimeSubjectDrivers( bindings: ApiBindings, input: StopRuntimeSubjectDriversInput, ): Promise { - const driverIds = await listRuntimeSubjectOperationDriverIds(bindings, input); + const driverIds: DriverInstanceId[] = await listRuntimeSubjectDriverIds( + bindings.DB, + input.runtimeSubjectId, + input.sandboxIncarnation, + ); - await Promise.all( + const outcomes = await Promise.allSettled( driverIds.map((driverInstanceId) => stopDriverSession(bindings, { driverInstanceId, - ...(input.operationId !== undefined ? { operationId: input.operationId } : {}), - ...(input.preserveSessionLifecycle !== undefined - ? { preserveSessionLifecycle: input.preserveSessionLifecycle } - : {}), reason: input.reason, - ...(input.terminalRun ? { terminalRun: input.terminalRun } : {}), }), ), ); + const failure = outcomes.find( + (outcome): outcome is PromiseRejectedResult => outcome.status === "rejected", + ); + if (failure !== undefined) { + throw failure.reason; + } } diff --git a/apps/api/src/modules/runtime/infrastructure/runtime-subject-lifecycle/runtime-subject-errors.ts b/apps/api/src/modules/runtime/infrastructure/runtime-subject-lifecycle/runtime-subject-errors.ts index b52e4564..7968f815 100644 --- a/apps/api/src/modules/runtime/infrastructure/runtime-subject-lifecycle/runtime-subject-errors.ts +++ b/apps/api/src/modules/runtime/infrastructure/runtime-subject-lifecycle/runtime-subject-errors.ts @@ -57,6 +57,15 @@ export class RuntimeSubjectRestoreFailedError extends Error { } } +export class RuntimeSubjectPhysicalStateLostError extends Error { + constructor(runtimeSubjectId: string) { + super( + `Runtime subject ${runtimeSubjectId} physical incarnation was lost before its checkpoint completed.`, + ); + this.name = "RuntimeSubjectPhysicalStateLostError"; + } +} + export class RuntimeBucketMountConflictError extends Error { readonly bucket: string | null; readonly mountPath: string; diff --git a/apps/api/src/modules/runtime/infrastructure/runtime-subject-lifecycle/runtime-subject-lifecycle.service.ts b/apps/api/src/modules/runtime/infrastructure/runtime-subject-lifecycle/runtime-subject-lifecycle.service.ts index a7d1e6fe..40342bb4 100644 --- a/apps/api/src/modules/runtime/infrastructure/runtime-subject-lifecycle/runtime-subject-lifecycle.service.ts +++ b/apps/api/src/modules/runtime/infrastructure/runtime-subject-lifecycle/runtime-subject-lifecycle.service.ts @@ -21,6 +21,7 @@ import { SERVER_PRODUCT_ANALYTICS_EVENTS, } from "../../../../platform/analytics/product-analytics"; import { createErrorLogContext, logWarn } from "../../../../platform/cloudflare/logger"; +import { disposeRpcResource } from "../../../../platform/cloudflare/rpc-disposal"; import type { ApiBindings } from "../../../../platform/cloudflare/worker-types"; import { currentTimestampMs } from "../../../../time"; import { @@ -32,17 +33,17 @@ import type { RuntimeDiagnosticContext } from "../../application/runtime-diagnos import type { RuntimeTimingRecorder } from "../../application/session-runs/session-runtime-timing"; import { getRuntimeKindPolicy, - getRuntimeSubjectInactiveDeadline, runtimeCheckpointRulesInclude, } from "../../domain/runtime-kind-policy"; import type { SandboxNetworkConstraints } from "../../domain/sandbox-network-constraints"; +import { hashSandboxNetworkConstraints } from "../../domain/sandbox-network-constraints"; import type { SandboxHandle } from "../sandbox-handles"; -import { deleteActiveSandboxConversationSession } from "../sandbox-session/sandbox-conversation-session-delete"; import { recordRuntimeRunLeaseAcquiredOutcome, recordRuntimeRunLeaseReleased, } from "./runtime-run-lease-store"; import type { RuntimeRunLeaseTransitionOutcome } from "./runtime-run-lease-store"; +import { stopRuntimeSubjectDrivers } from "./runtime-subject-driver-stop"; import { getRuntimeSubjectErrorCode, RuntimeSubjectBackupNotReadyError, @@ -51,27 +52,34 @@ import { import { assertRuntimeSubjectNetworkPolicySupported } from "./runtime-subject-network"; import { configureRuntimeSubjectNetwork, + activateRuntimeSubjectIncarnation, destroyRuntimeSubjectContainer, getRuntimeSubjectKeepAliveHandle, + inspectRuntimeSubjectIncarnation, + markRuntimeSubjectIncarnationReady, prepareRuntimeSubjectFilesystem, restoreRuntimeSubjectBackup, } from "./runtime-subject-platform"; import { claimRuntimeSubjectActivation, + claimRuntimeSubjectOperationForRepair, ensureRuntimeSubjectId, - getRuntimeConversationSessionState, getRuntimeSubjectActivationRecord, markRuntimeSubjectActivationDestroying, markRuntimeSubjectActivationFailed, + markRuntimeSubjectActiveDestroying, markRuntimeSubjectActive, + markRuntimeSubjectOperationRepairNeeded, markRuntimeSubjectRestoreApplied, markRuntimeSubjectRestoring, preemptRuntimeSubjectActivationClaim, recordRuntimeConversationSessionActive, - recordRuntimeConversationSessionClosed, recordRuntimeConversationSessionError, + releaseRuntimeSubjectActivationClaim, + retireRuntimeConversationSessionsForIncarnation, } from "./runtime-subject-store"; import type { RuntimeSubjectActivationRecord } from "./runtime-subject-store"; +import type { RuntimeSubjectOperationLease } from "./runtime-subject-store"; import type { ReadyRuntimeSubjectBackupRecord } from "./runtime-subject-store"; const RUNTIME_SUBJECT_ACTIVATION_CLAIM_TTL_MS = 10 * 60_000; @@ -90,6 +98,11 @@ export interface ActivateRuntimeSubjectInput { readonly diagnosticContext?: RuntimeDiagnosticContext; readonly networkConstraints: SandboxNetworkConstraints; readonly purpose?: RuntimeSubjectActivationPurpose; + readonly provisioningAuthority?: { + readonly operationId: RuntimeOperationId; + readonly runId: SessionRunId; + readonly sessionId: SessionId; + }; readonly runtimeSubjectId: SandboxId; readonly appId: AppId; readonly subjectId: PlatformId; @@ -98,6 +111,7 @@ export interface ActivateRuntimeSubjectInput { } export interface ActiveRuntimeSubject { + readonly incarnation: number; readonly subject: SandboxHandle; } @@ -193,8 +207,8 @@ export class RuntimeSubjectLifecycleService { this.#bindings = bindings; } - async getHandle(runtimeSubjectId: SandboxId): Promise { - return getRuntimeSubjectKeepAliveHandle(this.#bindings, runtimeSubjectId); + async getHandle(runtimeSubjectId: SandboxId, incarnation: number): Promise { + return getRuntimeSubjectKeepAliveHandle(this.#bindings, runtimeSubjectId, incarnation); } async activate(input: ActivateRuntimeSubjectInput): Promise { @@ -206,55 +220,122 @@ export class RuntimeSubjectLifecycleService { const purpose = input.purpose ?? "interactive"; const claimOwner = createRuntimeSubjectActivationClaimOwner(purpose); + const networkConstraintsHash = await hashSandboxNetworkConstraints(input.networkConstraints); const record = await measureOptional(input.timing, "runtimeSubject.admitLifecycle", () => this.#admitActivation(input, claimOwner, purpose), ); - const subject = await this.getHandle(input.runtimeSubjectId); const isCold = record === null || record.status === "cold"; + let activationLease: RuntimeSubjectOperationLease | null = null; + let subject: SandboxHandle | null = null; + let subjectTransferred = false; + let reusedHealthyIncarnation = false; try { - // Network constraints must land before the first container-starting RPC - // below; a limited policy that cannot be applied fails the activation - // (and the catch path destroys the container) instead of running open. - // Stable Pet subjects support Full only and keep their existing runtime - // path unchanged. Cattle records Full as well as Limited so the - // session-scoped subject can never switch policy after admission. - if (input.kind === "cattle") { - await measureOptional(input.timing, "runtimeSubject.configureNetwork", () => - configureRuntimeSubjectNetwork(subject, input.networkConstraints), - ); - } - await measureOptional(input.timing, "runtimeSubject.prepareFilesystem", () => - prepareRuntimeSubjectFilesystem(subject), - ); - if (isCold) { - const restoring = await measureOptional(input.timing, "runtimeSubject.markRestoring", () => + activationLease = await measureOptional(input.timing, "runtimeSubject.markRestoring", () => markRuntimeSubjectRestoring(this.#bindings.DB, { claimOwner, + expectedIncarnation: record?.incarnation ?? 0, + expectedStatus: "cold", + networkConstraintsHash, + operationId: createPlatformId(), runtimeSubjectId: input.runtimeSubjectId, }), ); - if (!restoring) { + if (activationLease === null) { throw new Error("Runtime subject activation claim expired before restore."); } + } else if (record !== null) { + if (record.networkConstraintsHash !== networkConstraintsHash) { + const retired = await this.#retireActiveIncarnation({ + claimOwner, + errorCode: "runtime.subject_activation_failed", + message: "Runtime subject network constraints changed.", + record, + runtimeSubjectId: input.runtimeSubjectId, + provisioningAuthority: input.provisioningAuthority, + }); + if (!retired) { + throw new Error("Runtime subject activation claim expired before network retirement."); + } + throw new Error("Runtime subject network constraints changed; retry activation."); + } + subject = await this.getHandle(input.runtimeSubjectId, record.incarnation); + const health = await inspectRuntimeSubjectIncarnation( + subject, + record.incarnation, + networkConstraintsHash, + ); + if (health.kind === "healthy") { + reusedHealthyIncarnation = true; + } else if (health.kind === "unknown") { + throw new Error("Runtime subject active-container health is unknown."); + } else { + const retired = await this.#retireActiveIncarnation({ + claimOwner, + errorCode: "runtime.subject_activation_failed", + message: `Runtime subject active container is ${health.kind}.`, + record, + runtimeSubjectId: input.runtimeSubjectId, + provisioningAuthority: input.provisioningAuthority, + }); + if (!retired) { + throw new Error("Runtime subject activation claim expired before recovery."); + } + throw new Error("Runtime subject active container was retired; retry activation."); + } + } + + const incarnation = activationLease?.incarnation ?? record?.incarnation ?? 0; + subject ??= await this.getHandle(input.runtimeSubjectId, incarnation); + const activeSubject = subject; + if (activationLease !== null) { + await activateRuntimeSubjectIncarnation(activeSubject, incarnation, networkConstraintsHash); + } + + // A healthy active incarnation is already prepared. Re-running global + // container mutations during reuse is both redundant and unsafe: one + // caller timing out must never poison a Pet container used by another + // Run. Fresh incarnations still establish network policy before their + // first container-starting filesystem RPC. + if (!reusedHealthyIncarnation) { + if (input.kind === "cattle") { + await measureOptional(input.timing, "runtimeSubject.configureNetwork", () => + configureRuntimeSubjectNetwork(activeSubject, input.networkConstraints), + ); + } + await measureOptional(input.timing, "runtimeSubject.prepareFilesystem", () => + prepareRuntimeSubjectFilesystem(activeSubject), + ); + } + + if (activationLease !== null) { + const lease = activationLease; await measureOptional(input.timing, "runtimeSubject.restoreBackup", () => this.#restoreLastBackup({ - claimOwner, kind: input.kind, + lease, record, runtimeSubjectId: input.runtimeSubjectId, - subject, + subject: activeSubject, }), ); + await markRuntimeSubjectIncarnationReady( + activeSubject, + incarnation, + networkConstraintsHash, + ); } const activated = await measureOptional(input.timing, "runtimeSubject.markActive", () => markRuntimeSubjectActive(this.#bindings.DB, { claimOwner, + incarnation: activationLease?.incarnation ?? record?.incarnation ?? 0, kind: input.kind, + networkConstraintsHash, + operationId: activationLease?.operationId ?? null, runtimeSubjectId: input.runtimeSubjectId, }), ); @@ -283,25 +364,44 @@ export class RuntimeSubjectLifecycleService { }, }); } + + if (subject === null) { + throw new Error("Runtime subject activation completed without a Sandbox handle."); + } + + subjectTransferred = true; + return { + incarnation: activationLease?.incarnation ?? record?.incarnation ?? 0, + subject, + }; } catch (error) { const message = error instanceof Error ? error.message : "Runtime subject activation failed."; const errorCode = getRuntimeSubjectErrorCode(error); - const operationId = createPlatformId(); + let destroyingRecorded = false; - try { - destroyingRecorded = await markRuntimeSubjectActivationDestroying(this.#bindings.DB, { + if (activationLease === null) { + await releaseRuntimeSubjectActivationClaim(this.#bindings.DB, { claimOwner, errorCode, - message, - operationId, - runtimeSubjectId: input.runtimeSubjectId, - }); - } catch (recordError) { - logWarn("runtime.subject.activation_failure.destroy_record_failed", { - ...createErrorLogContext(recordError), + errorMessage: message, + incarnation: record?.incarnation ?? 0, runtimeSubjectId: input.runtimeSubjectId, }); + } else { + try { + destroyingRecorded = await markRuntimeSubjectActivationDestroying(this.#bindings.DB, { + errorCode, + lease: activationLease, + message, + runtimeSubjectId: input.runtimeSubjectId, + }); + } catch (recordError) { + logWarn("runtime.subject.activation_failure.destroy_record_failed", { + ...createErrorLogContext(recordError), + runtimeSubjectId: input.runtimeSubjectId, + }); + } } // Teardown is bounded by the provision timeout. Only confirmed teardown @@ -311,7 +411,11 @@ export class RuntimeSubjectLifecycleService { if (destroyingRecorded) { try { - await destroyRuntimeSubjectContainer(this.#bindings, input.runtimeSubjectId); + await destroyRuntimeSubjectContainer( + this.#bindings, + input.runtimeSubjectId, + activationLease?.incarnation ?? 0, + ); destroyed = true; } catch (destroyError) { logWarn("runtime.subject.activation_failure.destroy_failed", { @@ -321,12 +425,12 @@ export class RuntimeSubjectLifecycleService { } } - if (destroyingRecorded && destroyed) { + if (activationLease !== null && destroyingRecorded && destroyed) { try { await markRuntimeSubjectActivationFailed(this.#bindings.DB, { errorCode, + lease: activationLease, message, - operationId, runtimeSubjectId: input.runtimeSubjectId, }); } catch (finalizeError) { @@ -346,13 +450,16 @@ export class RuntimeSubjectLifecycleService { }); throw new Error(message, { cause: error }); + } finally { + if (!subjectTransferred) { + disposeRpcResource(subject); + } } - - return { subject }; } async activateConversationSession(input: { readonly sandboxSessionId: SandboxSessionId; + readonly sandboxIncarnation: number; readonly cwd: string; readonly now: number; readonly originJson: string; @@ -364,6 +471,7 @@ export class RuntimeSubjectLifecycleService { async failConversationSession(input: { readonly sandboxSessionId: SandboxSessionId; + readonly sandboxIncarnation: number; readonly cwd: string; readonly errorCode: RuntimeSubjectErrorCode; readonly message: string; @@ -379,45 +487,18 @@ export class RuntimeSubjectLifecycleService { readonly runtimeSubjectId: SandboxId; readonly sessionId: SessionId; }): Promise { - const state = await getRuntimeConversationSessionState(this.#bindings.DB, input); - - if (!state || state.status !== "active") { - return; - } - - const now = currentTimestampMs(); - - await deleteActiveSandboxConversationSession(this.#bindings, { - sandboxSessionId: state.sandboxSessionId, + const { closeSandboxConversationSession } = await import("../sandbox-session.service"); + await closeSandboxConversationSession(this.#bindings, { sandboxId: input.runtimeSubjectId, - }); - - if (state.agentId) { - await appendRuntimeDiagnosticEvent(this.#bindings, { - eventName: RUNTIME_DIAGNOSTIC_EVENT.sandboxSessionDestroyed.name, - sessionId: input.sessionId, - value: { - ...toRuntimeDiagnosticBaseValue({ - agentId: state.agentId, - sessionId: input.sessionId, - }), - reason: "runtime_subject_session_closed", - sandboxId: input.runtimeSubjectId, - }, - }); - } - - await recordRuntimeConversationSessionClosed(this.#bindings.DB, { - inactiveDeadlineAt: getRuntimeSubjectInactiveDeadline(getRuntimeKindPolicy(state.kind), now), - now, - runtimeSubjectId: input.runtimeSubjectId, sessionId: input.sessionId, }); } async acquireRunLease(input: { + readonly driverGeneration: number; readonly driverInstanceId: DriverInstanceId; readonly runtimeSubjectId: SandboxId; + readonly runtimeSubjectIncarnation: number; readonly sessionId: SessionId; readonly sessionRunId: SessionRunId; }): Promise { @@ -426,6 +507,7 @@ export class RuntimeSubjectLifecycleService { async releaseRunLease(input: { readonly driverInstanceId: DriverInstanceId; + readonly expectedDriverGeneration: number; readonly expectedSessionRunId: SessionRunId; }): Promise { return recordRuntimeRunLeaseReleased(this.#bindings.DB, input); @@ -498,8 +580,19 @@ export class RuntimeSubjectLifecycleService { }): Promise { let record = input.record; - if (record.kind !== input.activation.kind) { - throw new Error("Runtime subject kind does not match the requested runtime kind."); + if ( + record.kind !== input.activation.kind || + record.subjectKind !== input.activation.subjectKind || + record.subjectId !== input.activation.subjectId + ) { + throw new Error("Runtime subject identity does not match the activation request."); + } + if ( + record.agentId !== input.activation.agentId || + record.appId !== input.activation.appId || + record.ownerAccountId !== input.activation.executionOwnerUserId + ) { + throw new Error("Runtime subject ownership does not match the activation request."); } if (record.status === "backing_up" || record.status === "destroying") { @@ -543,8 +636,12 @@ export class RuntimeSubjectLifecycleService { throw new Error("Runtime subject activation could not refresh the lifecycle record."); } record = refreshed; - if (record.kind !== input.activation.kind) { - throw new Error("Runtime subject kind does not match the requested runtime kind."); + if ( + record.kind !== input.activation.kind || + record.subjectKind !== input.activation.subjectKind || + record.subjectId !== input.activation.subjectId + ) { + throw new Error("Runtime subject identity changed during activation."); } if (record.status === "backing_up" || record.status === "destroying") { throw new Error("Runtime subject is busy with lifecycle maintenance."); @@ -552,6 +649,43 @@ export class RuntimeSubjectLifecycleService { } if (record.status === "restoring") { + if ( + !hasActiveRuntimeSubjectClaim(record, currentTimestampMs()) && + record.operationId !== null && + record.operationKind === "activate" + ) { + const repairNow = currentTimestampMs(); + const lease = await claimRuntimeSubjectOperationForRepair(this.#bindings.DB, { + candidate: { + claimExpiresAt: record.claimExpiresAt, + claimOwner: record.claimOwner, + id: record.id, + incarnation: record.incarnation, + kind: record.kind, + operationId: record.operationId, + operationKind: "activate", + status: "restoring", + }, + claimExpiresAt: repairNow + RUNTIME_SUBJECT_ACTIVATION_CLAIM_TTL_MS, + claimOwner: `activation-repair-${crypto.randomUUID()}`, + now: repairNow, + }); + if (lease !== null) { + const { runRuntimeSubjectOperation } = + await import("./runtime-subject-operations.service"); + await runRuntimeSubjectOperation(this.#bindings, { + kind: record.kind, + lease, + reason: "runtime_subject.activation_takeover", + runtimeSubjectId: record.id, + }); + const repaired = await getRuntimeSubjectActivationRecord(this.#bindings.DB, record.id); + if (repaired === null) { + throw new Error("Runtime subject activation repair lost its lifecycle record."); + } + return this.#claimExistingActivation({ ...input, record: repaired }); + } + } throw new Error("Runtime subject is busy with lifecycle maintenance."); } @@ -635,9 +769,87 @@ export class RuntimeSubjectLifecycleService { }); } - async #restoreLastBackup(input: { + async #retireActiveIncarnation(input: { readonly claimOwner: string; + readonly errorCode: RuntimeSubjectErrorCode; + readonly message: string; + readonly provisioningAuthority: ActivateRuntimeSubjectInput["provisioningAuthority"]; + readonly record: RuntimeSubjectActivationRecord; + readonly runtimeSubjectId: SandboxId; + }): Promise { + if (input.provisioningAuthority === undefined) { + return false; + } + const lease = await markRuntimeSubjectActiveDestroying(this.#bindings.DB, { + claimOwner: input.claimOwner, + errorCode: input.errorCode, + expectedIncarnation: input.record.incarnation, + message: input.message, + operationId: createPlatformId(), + provisioningOperationId: input.provisioningAuthority.operationId, + provisioningRunId: input.provisioningAuthority.runId, + provisioningSessionId: input.provisioningAuthority.sessionId, + runtimeSubjectId: input.runtimeSubjectId, + }); + if (lease === null) { + return false; + } + + try { + await stopRuntimeSubjectDrivers(this.#bindings, { + operationId: lease.operationId, + reason: "runtime_subject.active_incarnation_retired", + runtimeSubjectId: input.runtimeSubjectId, + sandboxIncarnation: lease.incarnation, + }); + await destroyRuntimeSubjectContainer( + this.#bindings, + input.runtimeSubjectId, + lease.incarnation, + ); + await retireRuntimeConversationSessionsForIncarnation(this.#bindings.DB, { + now: currentTimestampMs(), + runtimeSubjectId: input.runtimeSubjectId, + sandboxIncarnation: lease.incarnation, + }); + if ( + !(await markRuntimeSubjectActivationFailed(this.#bindings.DB, { + errorCode: input.errorCode, + lease, + message: input.message, + runtimeSubjectId: input.runtimeSubjectId, + })) + ) { + throw new Error("Runtime subject active-incarnation retirement lost ownership."); + } + } catch (error) { + try { + await markRuntimeSubjectOperationRepairNeeded(this.#bindings.DB, { + errorCode: getRuntimeSubjectErrorCode(error), + errorMessage: error instanceof Error ? error.message : input.message, + expectedStatus: "destroying", + lease, + runtimeSubjectId: input.runtimeSubjectId, + source: "api", + }); + } catch (recordError) { + logWarn("runtime.subject.active_retire.repair_record_failed", { + ...createErrorLogContext(recordError), + runtimeSubjectId: input.runtimeSubjectId, + }); + } + logWarn("runtime.subject.active_retire.failed", { + ...createErrorLogContext(error), + runtimeSubjectId: input.runtimeSubjectId, + }); + } + + return true; + } + + async #restoreLastBackup(input: { readonly kind: AgentKind; + readonly lease: RuntimeSubjectOperationLease; readonly record: RuntimeSubjectActivationRecord | null; readonly runtimeSubjectId: SandboxId; readonly subject: SandboxHandle; @@ -664,11 +876,14 @@ export class RuntimeSubjectLifecycleService { runtimeSubjectId: input.runtimeSubjectId, }); } - await markRuntimeSubjectRestoreApplied(this.#bindings.DB, { + const recorded = await markRuntimeSubjectRestoreApplied(this.#bindings.DB, { backupId: readyBackup.id, - claimOwner: input.claimOwner, + lease: input.lease, runtimeSubjectId: input.runtimeSubjectId, }); + if (!recorded) { + throw new Error("Runtime subject restore lost lifecycle ownership."); + } } async #appendRestoreFailureDiagnostic(input: { @@ -712,7 +927,4 @@ export function createRuntimeSubjectLifecycleService( return new RuntimeSubjectLifecycleService(bindings); } -export { - getRuntimeSubjectKeepAliveHandle, - prepareRuntimeSubjectFilesystem, -} from "./runtime-subject-platform"; +export { getRuntimeSubjectKeepAliveHandle } from "./runtime-subject-platform"; diff --git a/apps/api/src/modules/runtime/infrastructure/runtime-subject-lifecycle/runtime-subject-maintenance-store.ts b/apps/api/src/modules/runtime/infrastructure/runtime-subject-lifecycle/runtime-subject-maintenance-store.ts index 55b86ace..0d14360d 100644 --- a/apps/api/src/modules/runtime/infrastructure/runtime-subject-lifecycle/runtime-subject-maintenance-store.ts +++ b/apps/api/src/modules/runtime/infrastructure/runtime-subject-lifecycle/runtime-subject-maintenance-store.ts @@ -8,13 +8,9 @@ import { import type { DriverInstanceId, SandboxId, SessionId } from "@mosoo/id"; import { and, asc, eq, exists, inArray, isNotNull, isNull, lte, notExists, or } from "drizzle-orm"; -import { - getAppDatabase, - getD1ChangeCount, - runAppDatabaseBatch, -} from "../../../../platform/db/drizzle"; +import { getAppDatabase, getD1ChangeCount } from "../../../../platform/db/drizzle"; import { currentTimestampMs } from "../../../../time"; -import { RUNTIME_SUBJECT_OPERATION_STATUSES } from "../../domain/runtime-subject-lifecycle.machine"; +import { RUNTIME_SUBJECT_RECOVERABLE_OPERATION_STATUSES } from "../../domain/runtime-subject-lifecycle.machine"; import { ACTIVE_SESSION_RUN_STATUSES } from "../../domain/session-run-lifecycle.machine"; import { activeConversationSessionQuery, @@ -25,52 +21,28 @@ import { liveDriverInstanceQueryForListedSubject, runLeaseQuery, runLeaseQueryForListedSubject, + runtimeProvisioningQuery, + runtimeProvisioningQueryForListedSubject, } from "./runtime-subject-store-queries"; import type { RuntimeSubjectMaintenanceCandidate, RuntimeSubjectOperationRepairCandidate, + RuntimeSubjectOperationLease, RuntimeSubjectStatus, } from "./runtime-subject-store.types"; function isRuntimeSubjectOperationStatus( status: RuntimeSubjectStatus, ): status is RuntimeSubjectOperationRepairCandidate["status"] { - return RUNTIME_SUBJECT_OPERATION_STATUSES.includes( + return RUNTIME_SUBJECT_RECOVERABLE_OPERATION_STATUSES.includes( status as RuntimeSubjectOperationRepairCandidate["status"], ); } -export async function closeRuntimeSubjectSessionsForRecycle( - database: D1Database, - runtimeSubjectId: SandboxId, -): Promise { - const now = currentTimestampMs(); - - await runAppDatabaseBatch(database, (appDb) => [ - appDb - .update(sandboxSessionsTable) - .set({ - status: "closed", - updatedAt: now, - }) - .where( - and( - eq(sandboxSessionsTable.sandboxId, runtimeSubjectId), - eq(sandboxSessionsTable.status, "active"), - ), - ), - appDb - .update(sandboxesTable) - .set({ - updatedAt: now, - }) - .where(eq(sandboxesTable.id, runtimeSubjectId)), - ]); -} - export async function listRuntimeSubjectDriverIds( database: D1Database, runtimeSubjectId: SandboxId, + sandboxIncarnation?: number, ): Promise { const appDb = getAppDatabase(database); const activeRunLeaseQuery = appDb @@ -88,6 +60,9 @@ export async function listRuntimeSubjectDriverIds( .where( and( eq(driverInstancesTable.sandboxId, runtimeSubjectId), + ...(sandboxIncarnation === undefined + ? [] + : [eq(driverInstancesTable.sandboxIncarnation, sandboxIncarnation)]), or(inArray(driverInstancesTable.status, LIVE_DRIVER_STATUSES), exists(activeRunLeaseQuery)), ), ) @@ -147,6 +122,7 @@ export async function listInactiveRuntimeSubjects( notExists(activeConversationSessionQueryForListedSubject(appDb)), ), notExists(runLeaseQueryForListedSubject(appDb)), + notExists(runtimeProvisioningQueryForListedSubject(appDb)), isNotNull(sandboxesTable.inactiveDeadlineAt), lte(sandboxesTable.inactiveDeadlineAt, input.now), ), @@ -179,6 +155,7 @@ export async function repairStrandedRuntimeSubjectDeadlines( isNull(sandboxesTable.inactiveDeadlineAt), notExists(activeConversationSessionQueryForListedSubject(appDb)), notExists(runLeaseQueryForListedSubject(appDb)), + notExists(runtimeProvisioningQueryForListedSubject(appDb)), ), ) .run(); @@ -203,6 +180,7 @@ export async function repairStrandedRuntimeSubjectDeadlines( notExists(liveDriverInstanceQueryForListedSubject(appDb)), notExists(activeSessionRunQueryForListedSubject(appDb)), notExists(runLeaseQueryForListedSubject(appDb)), + notExists(runtimeProvisioningQueryForListedSubject(appDb)), ), ) .run(); @@ -225,14 +203,23 @@ export async function listStaleRuntimeSubjectOperations( id: sandboxesTable.id, kind: sandboxesTable.kind, operationId: sandboxesTable.statusOperationId, + claimExpiresAt: sandboxesTable.claimExpiresAt, + claimOwner: sandboxesTable.claimOwner, + incarnation: sandboxesTable.incarnation, + operationKind: sandboxesTable.operationKind, status: sandboxesTable.status, }) .from(sandboxesTable) .where( and( - inArray(sandboxesTable.status, RUNTIME_SUBJECT_OPERATION_STATUSES), + inArray(sandboxesTable.status, RUNTIME_SUBJECT_RECOVERABLE_OPERATION_STATUSES), isNotNull(sandboxesTable.statusOperationId), - lte(sandboxesTable.statusChangedAt, input.staleChangedAtLte), + isNotNull(sandboxesTable.operationKind), + or( + isNull(sandboxesTable.claimOwner), + isNull(sandboxesTable.claimExpiresAt), + lte(sandboxesTable.claimExpiresAt, input.staleChangedAtLte), + ), ), ) .orderBy(asc(sandboxesTable.statusChangedAt), asc(sandboxesTable.id)) @@ -240,12 +227,18 @@ export async function listStaleRuntimeSubjectOperations( .all(); return rows.flatMap((row) => - row.operationId === null || !isRuntimeSubjectOperationStatus(row.status) + row.operationId === null || + row.operationKind === null || + !isRuntimeSubjectOperationStatus(row.status) ? [] : [ { + claimExpiresAt: row.claimExpiresAt, + claimOwner: row.claimOwner, id: row.id, + incarnation: row.incarnation, kind: row.kind, + operationKind: row.operationKind, operationId: row.operationId, status: row.status, }, @@ -253,6 +246,57 @@ export async function listStaleRuntimeSubjectOperations( ); } +export async function claimRuntimeSubjectOperationForRepair( + database: D1Database, + input: { + readonly candidate: RuntimeSubjectOperationRepairCandidate; + readonly claimExpiresAt: number; + readonly claimOwner: string; + readonly now: number; + }, +): Promise { + const row = await getAppDatabase(database) + .update(sandboxesTable) + .set({ + claimExpiresAt: input.claimExpiresAt, + claimOwner: input.claimOwner, + updatedAt: input.now, + }) + .where( + and( + eq(sandboxesTable.id, input.candidate.id), + eq(sandboxesTable.incarnation, input.candidate.incarnation), + eq(sandboxesTable.operationKind, input.candidate.operationKind), + eq(sandboxesTable.status, input.candidate.status), + eq(sandboxesTable.statusOperationId, input.candidate.operationId), + ...(input.candidate.claimOwner === null + ? [isNull(sandboxesTable.claimOwner)] + : [eq(sandboxesTable.claimOwner, input.candidate.claimOwner)]), + ...(input.candidate.claimExpiresAt === null + ? [isNull(sandboxesTable.claimExpiresAt)] + : [eq(sandboxesTable.claimExpiresAt, input.candidate.claimExpiresAt)]), + or( + isNull(sandboxesTable.claimOwner), + isNull(sandboxesTable.claimExpiresAt), + lte(sandboxesTable.claimExpiresAt, input.now), + ), + ), + ) + .returning({ id: sandboxesTable.id }) + .get(); + + return row === undefined + ? null + : { + claimExpiresAt: input.claimExpiresAt, + claimOwner: input.claimOwner, + incarnation: input.candidate.incarnation, + kind: input.candidate.operationKind, + operationId: input.candidate.operationId, + status: input.candidate.status, + }; +} + export async function claimInactiveRuntimeSubject( database: D1Database, input: { @@ -280,6 +324,7 @@ export async function claimInactiveRuntimeSubject( notExists(activeConversationSessionQuery(appDb, input.runtimeSubjectId)), ), notExists(runLeaseQuery(appDb, input.runtimeSubjectId)), + notExists(runtimeProvisioningQuery(appDb, input.runtimeSubjectId)), isNotNull(sandboxesTable.inactiveDeadlineAt), lte(sandboxesTable.inactiveDeadlineAt, input.now), or( diff --git a/apps/api/src/modules/runtime/infrastructure/runtime-subject-lifecycle/runtime-subject-maintenance.service.ts b/apps/api/src/modules/runtime/infrastructure/runtime-subject-lifecycle/runtime-subject-maintenance.service.ts index 325dd347..e3a52193 100644 --- a/apps/api/src/modules/runtime/infrastructure/runtime-subject-lifecycle/runtime-subject-maintenance.service.ts +++ b/apps/api/src/modules/runtime/infrastructure/runtime-subject-lifecycle/runtime-subject-maintenance.service.ts @@ -1,34 +1,51 @@ import { sessionsTable } from "@mosoo/db"; import type { SandboxId, SessionId, SessionRunId } from "@mosoo/id"; -import { and, asc, eq, inArray, isNull, lte } from "drizzle-orm"; +import { and, asc, eq, isNull, lte } from "drizzle-orm"; import { createErrorLogContext, logWarn } from "../../../../platform/cloudflare/logger"; import type { ApiBindings } from "../../../../platform/cloudflare/worker-types"; import { getAppDatabase } from "../../../../platform/db/drizzle"; -import { isTruthy } from "../../../../shared/truthiness"; import { toIsoString } from "../../../../time"; import { repairStaleSessionDeleteCleanups } from "../../../sessions/application/session-cleanup.service"; -import { appendSessionRuntimeEvents } from "../../../sessions/application/session-event-write.service"; +import { publishPersistedSessionRuntimeEvents } from "../../../sessions/application/session-event-write.service"; +import { repairStaleSessionArchiveCleanups } from "../../../sessions/application/session-lifecycle-mutation.service"; import { syncSessionViewerState } from "../../../sessions/application/session-viewer-events.service"; import { RESCHEDULING_RECONNECT_WINDOW_MS } from "../../../sessions/domain/session-lifecycle"; +import { writeRuntimeOperationTimedOutSnapshots } from "../../application/runtime-state-operation-target-events"; +import { + commitSessionLifecycleEventProjection, + listStaleRuntimeOperationTargets, +} from "../../application/runtime-state-operation-target-store"; +import { recordCanonicalSessionRunTerminal } from "../../application/session-runs/session-run-terminal-failure.service"; import { createSessionLifecycleTerminatedEvent } from "../../application/session-runs/session-run-view-events.service"; import { reconcileStaleActiveSessionRuns } from "../../application/session-runs/stale-run-reconciliation.service"; import { reconcileTerminalSessionRuns } from "../../application/session-runs/terminal-run-reconciliation.service"; import { getRuntimeKindPolicy } from "../../domain/runtime-kind-policy"; import { cleanupDriverInstances } from "../driver-instance/maintenance"; +import { cleanupRuntimeArtifactAttempts } from "../driver-instance/runtime-artifact-attempt.repository"; +import { repairStagedSandboxBackups } from "../sandbox-backup.service"; import { repairRuntimeCommandRecords } from "../session-runs/runtime-command-store.repository"; -import { createSessionStatusTransitionPatch } from "../session-runs/session-lifecycle-projection.repository"; -import { setSessionRunStatus } from "../session-runs/session-run-store.repository"; -import type { SessionRunTransitionOutcome } from "../session-runs/session-run-store.repository"; +import { getSessionRunSummariesByIds } from "../session-runs/session-run-store.repository"; import { listIdleSessionScopedConversationSessions } from "./runtime-conversation-session-store"; +import { + cleanupRuntimeProvisioningResources, + retireRuntimeProvisioningIncarnation, +} from "./runtime-provisioning-cleanup.service"; +import { + adoptReadyRuntimeRunProvisioningLease, + claimStaleRuntimeProvisioningLeases, + releaseAbortedRuntimeProvisioningLease, +} from "./runtime-provisioning-lease-store"; import { repairStrandedRuntimeSubjectDeadlines } from "./runtime-subject-maintenance-store"; import { claimInactiveRuntimeSubject, + claimRuntimeSubjectOperationForRepair, listInactiveRuntimeSubjects, listStaleRuntimeSubjectOperations, } from "./runtime-subject-store"; import type { RuntimeSubjectMaintenanceCandidate, + RuntimeSubjectOperationLease, RuntimeSubjectOperationRepairCandidate, } from "./runtime-subject-store"; @@ -51,16 +68,18 @@ type ResumeRuntimeSubjectRecycleOperation = ( bindings: ApiBindings, input: { readonly kind: RuntimeSubjectOperationRepairCandidate["kind"]; - readonly operationId: RuntimeSubjectOperationRepairCandidate["operationId"]; + readonly lease: RuntimeSubjectOperationLease; readonly reason: string; readonly runtimeSubjectId: RuntimeSubjectOperationRepairCandidate["id"]; - readonly status: RuntimeSubjectOperationRepairCandidate["status"]; }, ) => Promise; interface StaleReschedulingSessionRow { id: SessionId; last_run_id: SessionRunId | null; + runtime_event_seq_cursor: number; + status_seq: number; + updated_at: number; } const RESCHEDULING_TIMEOUT_ERROR = { @@ -70,34 +89,23 @@ const RESCHEDULING_TIMEOUT_ERROR = { retryable: false, } as const; -function assertMaintenanceRunTransition(outcome: SessionRunTransitionOutcome): void { - switch (outcome.kind) { - case "applied": - case "duplicate": { - return; - } - case "stale": { - if (outcome.reason === "terminal_run") { - return; - } - throw new Error("Rescheduling timeout lost a concurrent run transition."); - } - case "repair_needed": { - throw new Error("Rescheduling timeout left session projection stale."); - } - case "rejected": { - throw new Error(`Rescheduling timeout run transition was rejected: ${outcome.reason}.`); - } - } -} - async function processInBatches( items: readonly T[], batchSize: number, task: (item: T) => Promise, + onRejected: (item: T, reason: unknown) => void, ): Promise { for (let index = 0; index < items.length; index += batchSize) { - await Promise.all(items.slice(index, index + batchSize).map(task)); + const batch = items.slice(index, index + batchSize); + const outcomes = await Promise.allSettled(batch.map(task)); + for (const [outcomeIndex, outcome] of outcomes.entries()) { + if (outcome.status === "rejected") { + const item = batch[outcomeIndex]; + if (item !== undefined) { + onRejected(item, outcome.reason); + } + } + } } } @@ -146,13 +154,23 @@ async function repairRuntimeSubjectOperationCandidate( readonly resumeRuntimeSubjectRecycleOperation: ResumeRuntimeSubjectRecycleOperation; }, ): Promise { + const now = Date.now(); + const lease = await claimRuntimeSubjectOperationForRepair(bindings.DB, { + candidate: input.candidate, + claimExpiresAt: now + MAINTENANCE_CLAIM_TTL_MS, + claimOwner: `repair-${crypto.randomUUID()}`, + now, + }); + if (lease === null) { + return; + } + try { await input.resumeRuntimeSubjectRecycleOperation(bindings, { kind: input.candidate.kind, - operationId: input.candidate.operationId, + lease, reason: input.reason, runtimeSubjectId: input.candidate.id, - status: input.candidate.status, }); } catch (error) { logWarn("runtime.subject.maintenance.operation_repair_failed", { @@ -164,23 +182,37 @@ async function repairRuntimeSubjectOperationCandidate( } } -async function publishReschedulingTimeoutEvent( +async function commitReschedulingTimeoutProjection( bindings: ApiBindings, target: StaleReschedulingSessionRow, ): Promise { - const stoppedAt = Date.now(); + const stoppedAt = target.updated_at + RESCHEDULING_RECONNECT_WINDOW_MS; const event = createSessionLifecycleTerminatedEvent({ lastSeen: toIsoString(stoppedAt), message: RESCHEDULING_TIMEOUT_ERROR.message, + occurredAtMs: stoppedAt, reason: RESCHEDULING_TIMEOUT_ERROR.code, sessionId: target.id, + sourceEventId: `maintenance:rescheduling-timeout:${target.id}`, }); - await appendSessionRuntimeEvents({ - bindings, - events: [event], - sessionId: target.id, + const outcome = await commitSessionLifecycleEventProjection(bindings.DB, { + event, + status: "TERMINATED", + target: { + lastRunId: target.last_run_id, + sessionId: target.id, + sessionRuntimeEventSeqCursor: target.runtime_event_seq_cursor, + sessionStatus: "RESCHEDULING", + sessionStatusOperationId: null, + sessionStatusSeq: target.status_seq, + sessionUpdatedAt: target.updated_at, + }, + timestampMs: stoppedAt, }); + if (outcome.kind !== "stale") { + await publishPersistedSessionRuntimeEvents({ bindings, events: [event], sessionId: target.id }); + } } export async function expireStaleReschedulingSessions(bindings: ApiBindings): Promise { @@ -188,11 +220,16 @@ export async function expireStaleReschedulingSessions(bindings: ApiBindings): Pr const staleSessions = await getAppDatabase(bindings.DB) .select({ id: sessionsTable.id, + last_run_id: sessionsTable.lastRunId, + runtime_event_seq_cursor: sessionsTable.runtimeEventSeqCursor, + status_seq: sessionsTable.statusSeq, + updated_at: sessionsTable.updatedAt, }) .from(sessionsTable) .where( and( eq(sessionsTable.status, "RESCHEDULING"), + isNull(sessionsTable.archivedAt), isNull(sessionsTable.statusOperationId), lte(sessionsTable.updatedAt, now - RESCHEDULING_RECONNECT_WINDOW_MS), ), @@ -205,49 +242,77 @@ export async function expireStaleReschedulingSessions(bindings: ApiBindings): Pr return; } - const results = await getAppDatabase(bindings.DB) - .update(sessionsTable) - .set( - createSessionStatusTransitionPatch({ - status: "TERMINATED", - timestampMs: now, - }), - ) - .where( - and( - inArray( - sessionsTable.id, - staleSessions.map((session) => session.id), - ), - eq(sessionsTable.status, "RESCHEDULING"), - isNull(sessionsTable.statusOperationId), - lte(sessionsTable.updatedAt, now - RESCHEDULING_RECONNECT_WINDOW_MS), - ), - ) - .returning({ - id: sessionsTable.id, - last_run_id: sessionsTable.lastRunId, - }) - .all(); + const runIds = staleSessions.flatMap((target) => + target.last_run_id === null ? [] : [target.last_run_id], + ); + const runsById = await getSessionRunSummariesByIds(bindings.DB, runIds); await processInBatches( - results.map((target) => target.last_run_id).filter(isTruthy), + staleSessions, RESCHEDULING_TIMEOUT_IO_BATCH_SIZE, - async (runId) => { - const outcome = await setSessionRunStatus(bindings.DB, { - error: RESCHEDULING_TIMEOUT_ERROR, - preserveSessionLifecycle: true, - runId, - source: "maintenance", - status: "failed", + async (target) => { + const run = target.last_run_id === null ? null : (runsById.get(target.last_run_id) ?? null); + if (run !== null && !["cancelled", "completed", "expired", "failed"].includes(run.status)) { + await recordCanonicalSessionRunTerminal(bindings, { + assistantMessage: null, + error: RESCHEDULING_TIMEOUT_ERROR, + expectedSessionOperationId: null, + expectedSessionObservation: { + lastRunId: target.last_run_id, + status: "RESCHEDULING", + statusSeq: target.status_seq, + updatedAt: target.updated_at, + }, + lifecycle: "TERMINATED", + runId: run.id, + sessionId: target.id, + source: "maintenance", + status: "failed", + timestampMs: target.updated_at + RESCHEDULING_RECONNECT_WINDOW_MS, + }); + return; + } + + await commitReschedulingTimeoutProjection(bindings, target); + }, + (target, reason) => { + logWarn("runtime.session.rescheduling_timeout_repair_failed", { + ...createErrorLogContext(reason), + sessionId: target.id, }); - assertMaintenanceRunTransition(outcome); }, ); +} + +export async function repairStaleRuntimeOperationTargets( + bindings: ApiBindings, + input: { + readonly limit: number; + readonly staleUpdatedAtLte: number; + }, +): Promise { + const targets = await listStaleRuntimeOperationTargets(bindings.DB, input); - await processInBatches(results, RESCHEDULING_TIMEOUT_IO_BATCH_SIZE, async (target) => - publishReschedulingTimeoutEvent(bindings, target), + await Promise.all( + [...Map.groupBy(targets, (target) => target.operationId)].map( + async ([operationId, operationTargets]) => { + try { + await writeRuntimeOperationTimedOutSnapshots(bindings, { + operationId, + targets: operationTargets, + }); + } catch (error) { + logWarn("runtime.operation.timeout_repair_failed", { + ...createErrorLogContext(error), + operationId, + sessionIds: operationTargets.map((target) => target.sessionId), + }); + } + }, + ), ); + + return targets.length; } // Cattle conversations no longer close on run terminal (the resident driver is @@ -286,11 +351,84 @@ async function closeIdleSessionScopedConversationSessions( } } +async function repairPendingConversationSessionCleanups(bindings: ApiBindings): Promise { + const { repairPendingSandboxConversationSessionCleanups } = + await import("../sandbox-session.service"); + try { + await repairPendingSandboxConversationSessionCleanups(bindings, MAINTENANCE_BATCH_SIZE); + } catch (error) { + logWarn("runtime.conversation.cleanup_repair_failed", createErrorLogContext(error)); + } +} + +export async function repairStaleRuntimeProvisioningLeases( + bindings: ApiBindings, + input: { readonly heartbeatAtLte: number; readonly limit: number }, +): Promise { + const claims = await claimStaleRuntimeProvisioningLeases(bindings.DB, input); + + await Promise.allSettled( + claims.map(async (claim) => { + try { + if (await adoptReadyRuntimeRunProvisioningLease(bindings.DB, claim)) { + return; + } + if (claim.sandboxIncarnation === null) { + await cleanupRuntimeProvisioningResources(bindings, claim, "maintenance"); + if (!(await releaseAbortedRuntimeProvisioningLease(bindings.DB, claim))) { + throw new Error("Runtime provisioning repair lost its maintenance ownership."); + } + } else { + await retireRuntimeProvisioningIncarnation(bindings, claim, "maintenance"); + } + } catch (error) { + logWarn("runtime.provisioning.repair_failed", { + ...createErrorLogContext(error), + operationId: claim.operationId, + runId: claim.runId, + sandboxId: claim.sandboxId, + sessionId: claim.sessionId, + }); + } + }), + ); + + return claims.length; +} + export async function runSandboxMaintenance(bindings: ApiBindings): Promise { const now = Date.now(); - await repairRuntimeCommandRecords(bindings.DB, { nowMs: now }); + await repairStaleRuntimeProvisioningLeases(bindings, { + heartbeatAtLte: now - MAINTENANCE_OPERATION_REPAIR_AFTER_MS, + limit: MAINTENANCE_BATCH_SIZE, + }); + await repairStagedSandboxBackups(bindings, MAINTENANCE_BATCH_SIZE); + const cleanupRepairs = await Promise.allSettled([ + repairStaleSessionArchiveCleanups(bindings, { + limit: MAINTENANCE_BATCH_SIZE, + staleUpdatedAtLte: now - MAINTENANCE_OPERATION_REPAIR_AFTER_MS, + }), + repairStaleSessionDeleteCleanups(bindings, { + limit: MAINTENANCE_BATCH_SIZE, + staleUpdatedAtLte: now - MAINTENANCE_OPERATION_REPAIR_AFTER_MS, + }), + ]); + const cleanupRepairFailure = cleanupRepairs.find( + (result): result is PromiseRejectedResult => result.status === "rejected", + ); + if (cleanupRepairFailure !== undefined) { + logWarn( + "runtime.session.cleanup_repair_failed", + createErrorLogContext(cleanupRepairFailure.reason), + ); + } + await repairStaleRuntimeOperationTargets(bindings, { + limit: MAINTENANCE_BATCH_SIZE, + staleUpdatedAtLte: now - RESCHEDULING_RECONNECT_WINDOW_MS, + }); await cleanupDriverInstances(bindings); + await repairRuntimeCommandRecords(bindings.DB, { nowMs: now }); const staleRunReconciliation = await reconcileStaleActiveSessionRuns(bindings.DB, { limit: MAINTENANCE_BATCH_SIZE, }); @@ -306,12 +444,15 @@ export async function runSandboxMaintenance(bindings: ApiBindings): Promise syncSessionViewerState(bindings, sessionId), + (sessionId, reason) => { + logWarn("runtime.session.viewer_state_repair_failed", { + ...createErrorLogContext(reason), + sessionId, + }); + }, ); await expireStaleReschedulingSessions(bindings); - await repairStaleSessionDeleteCleanups(bindings, { - limit: MAINTENANCE_BATCH_SIZE, - staleUpdatedAtLte: now - MAINTENANCE_OPERATION_REPAIR_AFTER_MS, - }); + await repairPendingConversationSessionCleanups(bindings); await closeIdleSessionScopedConversationSessions(bindings, now); const repairedDeadlines = await repairStrandedRuntimeSubjectDeadlines(bindings.DB, { now }); @@ -332,10 +473,16 @@ export async function runSandboxMaintenance(bindings: ApiBindings): Promise; + +function operationWorkerOwner(prefix: string): string { + return `${prefix}-${crypto.randomUUID()}`; +} + function getRuntimeOperationErrorMessage(error: unknown): string { return error instanceof Error ? error.message : "Runtime state operation failed."; } @@ -53,214 +71,392 @@ async function appendCheckpointFailureDiagnostics( } await appendOneRuntimeDiagnosticEventPerSession(bindings, { - events: input.targets.flatMap((target) => { - if (target.agentId === null) { - return []; - } - - return [ - { - eventName: RUNTIME_DIAGNOSTIC_EVENT.sandboxCheckpointFailed.name, - sessionId: target.sessionId, - value: { - ...toRuntimeDiagnosticBaseValue({ - agentId: target.agentId, + events: input.targets.flatMap((target) => + target.agentId === null + ? [] + : [ + { + eventName: RUNTIME_DIAGNOSTIC_EVENT.sandboxCheckpointFailed.name, sessionId: target.sessionId, - }), - backupId: - input.error instanceof RuntimeSubjectCheckpointFailedError - ? input.error.backupId - : null, - dir: - input.error instanceof RuntimeSubjectCheckpointFailedError ? input.error.dir : null, - errorCode, - reason: toRuntimeDiagnosticReason(input.error, "Runtime subject checkpoint failed."), - sandboxId: input.runtimeSubjectId, - }, - }, - ]; - }), + value: { + ...toRuntimeDiagnosticBaseValue({ + agentId: target.agentId, + sessionId: target.sessionId, + }), + backupId: + input.error instanceof RuntimeSubjectCheckpointFailedError + ? input.error.backupId + : null, + dir: + input.error instanceof RuntimeSubjectCheckpointFailedError + ? input.error.dir + : null, + errorCode, + reason: toRuntimeDiagnosticReason( + input.error, + "Runtime subject checkpoint failed.", + ), + sandboxId: input.runtimeSubjectId, + }, + }, + ], + ), }); } -async function appendTerminatedEventsForRuntimeSubject( +function checkpointRules(kind: AgentKind, operationKind: DestructiveRuntimeSubjectOperationKind) { + const checkpoint = getRuntimeKindPolicy(kind).checkpoint; + + switch (operationKind) { + case "hibernate": + return checkpoint.createOnHibernate; + case "recreate": + return checkpoint.createOnRecreate; + case "reset": + return checkpoint.createOnReset; + } +} + +function clearsBackups(kind: AgentKind, operationKind: SandboxOperationKind): boolean { + if (operationKind === "reset") { + return true; + } + return operationKind === "recreate" + ? getRuntimeKindPolicy(kind).checkpoint.createOnRecreate.length === 0 + : false; +} + +type RuntimeSubjectPhysicalState = "gone" | "healthy" | "unknown"; + +async function inspectOperationRuntimeSubjectPhysicalState( bindings: ApiBindings, input: { - readonly reason: string; + readonly lease: RuntimeSubjectOperationLease; readonly runtimeSubjectId: SandboxId; - readonly targets: RuntimeSubjectOperationInput["targets"]; }, -): Promise { - await appendRuntimeSubjectTerminatedEvents(bindings, { - reason: input.reason, - runtimeSubjectId: input.runtimeSubjectId, - targets: input.targets, - }); +): Promise { + const record = await getRuntimeSubject(bindings.DB, input.runtimeSubjectId); + if ( + record === null || + record.incarnation !== input.lease.incarnation || + record.networkConstraintsHash === null + ) { + return "unknown"; + } + + const subject = await getRuntimeSubjectKeepAliveHandle( + bindings, + input.runtimeSubjectId, + input.lease.incarnation, + ); + try { + const result = await inspectRuntimeSubjectIncarnation( + subject, + input.lease.incarnation, + record.networkConstraintsHash, + ); + switch (result.kind) { + case "healthy": + return "healthy"; + case "missing": + case "retired": + return "gone"; + case "stale": + case "unknown": + return "unknown"; + } + } finally { + disposeRpcResource(subject); + } } -export async function recreateRuntimeSubjectPreservingState( +export async function runRuntimeSubjectOperation( bindings: ApiBindings, - input: RuntimeSubjectOperationInput, + input: { + readonly kind: AgentKind; + readonly lease: RuntimeSubjectOperationLease; + readonly reason: string; + readonly runtimeSubjectId: SandboxId; + readonly targets?: RuntimeSubjectOperationInput["targets"]; + }, ): Promise { - const subject = await getRuntimeSubject(bindings.DB, input.runtimeSubjectId); + let lease = input.lease; + let physicalStateLost = false; + const renew = createLeaseOwnershipRenewal( + () => + renewRuntimeSubjectOperationLease(bindings.DB, { + claimExpiresAt: Date.now() + RUNTIME_SUBJECT_OPERATION_LEASE_TTL_MS, + lease, + runtimeSubjectId: input.runtimeSubjectId, + }), + "Runtime subject operation lost lifecycle ownership.", + ); + const heartbeat = setInterval(() => { + void renew().catch(() => undefined); + }, RUNTIME_SUBJECT_OPERATION_HEARTBEAT_MS); - if (!subject) { - return; - } + try { + await renew(); - let destroyStarted = false; + if (lease.status === "restoring") { + const advanced = await advanceRuntimeSubjectOperationStatus(bindings.DB, { + expectedStatus: "restoring", + lease, + runtimeSubjectId: input.runtimeSubjectId, + source: "maintenance", + status: "destroying", + }); + if (!advanced) { + throw new Error("Runtime subject activation repair lost lifecycle ownership."); + } + lease = { ...lease, status: "destroying" }; + } - const policy = getRuntimeKindPolicy(subject.kind); - const started = await markRuntimeSubjectOperationStarted(bindings.DB, { - operationId: input.operationId, - runtimeSubjectId: input.runtimeSubjectId, - status: "backing_up", - }); + if ( + lease.kind === "activate" && + !(await runtimeSubjectActivationRetirementIsDrained(bindings.DB, { + lease, + runtimeSubjectId: input.runtimeSubjectId, + })) + ) { + throw new Error("Runtime subject incarnation is still draining active Runs."); + } - if (!started) { - throw new Error("Runtime subject is busy with lifecycle maintenance."); - } + if (lease.status === "backing_up") { + if (lease.kind === "activate") { + throw new Error("Activation repair cannot enter the backup phase."); + } - try { - await stopRuntimeSubjectDrivers(bindings, { - operationId: input.operationId, - runtimeSubjectId: input.runtimeSubjectId, - preserveSessionLifecycle: true, - reason: input.reason, - targets: input.targets, - terminalRun: input.terminalRun, - }); - await createSandboxCheckpoints(bindings, { - operationId: input.operationId, - rules: policy.checkpoint.createOnRecreate, - sandboxId: input.runtimeSubjectId, - }); - destroyStarted = await advanceRuntimeSubjectOperationStatus(bindings.DB, { - expectedStatus: "backing_up", - operationId: input.operationId, - runtimeSubjectId: input.runtimeSubjectId, - status: "destroying", - }); - if (!destroyStarted) { - throw new Error("Runtime subject changed before destroy."); + const before = await inspectOperationRuntimeSubjectPhysicalState(bindings, { + lease, + runtimeSubjectId: input.runtimeSubjectId, + }); + if (before === "unknown") { + throw new Error("Runtime subject physical state is unknown before checkpoint."); + } + physicalStateLost = before === "gone"; + + if (!physicalStateLost) { + try { + await renew(); + await stopRuntimeSubjectDrivers(bindings, { + operationId: lease.operationId, + reason: input.reason, + runtimeSubjectId: input.runtimeSubjectId, + sandboxIncarnation: lease.incarnation, + ...(input.targets === undefined ? {} : { targets: input.targets }), + }); + await renew(); + + if (lease.kind === "reset") { + const stateTargets = await listRuntimeSubjectSessionStateTargets(bindings.DB, { + runtimeSubjectId: input.runtimeSubjectId, + }); + await clearRuntimeSubjectAgentState(bindings, { + incarnation: lease.incarnation, + rules: getRuntimeKindPolicy(input.kind).checkpoint.clearOnReset, + runtimeSubjectId: input.runtimeSubjectId, + stateTargets, + }); + await renew(); + } + + await createSandboxCheckpoints(bindings, { + operationLease: lease, + rules: checkpointRules(input.kind, lease.kind), + sandboxId: input.runtimeSubjectId, + }); + await renew(); + } catch (error) { + let after: RuntimeSubjectPhysicalState = "unknown"; + try { + after = await inspectOperationRuntimeSubjectPhysicalState(bindings, { + lease, + runtimeSubjectId: input.runtimeSubjectId, + }); + } catch { + // Preserve the original failure unless the exact incarnation is + // durably known to be gone. + } + physicalStateLost = after === "gone"; + if (!physicalStateLost) { + throw error; + } + } + } + + const advanced = await advanceRuntimeSubjectOperationStatus(bindings.DB, { + expectedStatus: "backing_up", + lease, + runtimeSubjectId: input.runtimeSubjectId, + source: "maintenance", + status: "destroying", + }); + if (!advanced) { + throw new Error("Runtime subject changed before destroy."); + } + lease = { ...lease, status: "destroying" }; + } + + if (lease.kind === "activate") { + await renew(); + await stopRuntimeSubjectDrivers(bindings, { + operationId: lease.operationId, + reason: input.reason, + runtimeSubjectId: input.runtimeSubjectId, + sandboxIncarnation: lease.incarnation, + }); + await renew(); + } + + await renew(); + await destroyRuntimeSubjectContainer(bindings, input.runtimeSubjectId, lease.incarnation); + await renew(); + + if (input.targets !== undefined) { + await appendRuntimeSubjectTerminatedEvents(bindings, { + reason: input.reason, + runtimeSubjectId: input.runtimeSubjectId, + targets: input.targets, + }); } - await destroyRuntimeSubjectContainer(bindings, input.runtimeSubjectId); - await appendTerminatedEventsForRuntimeSubject(bindings, { - reason: input.reason, - runtimeSubjectId: input.runtimeSubjectId, - targets: input.targets, - }); - await closeRuntimeSubjectSessionsForRecycle(bindings.DB, input.runtimeSubjectId); const completed = await markRuntimeSubjectCold(bindings.DB, { - clearBackups: policy.checkpoint.createOnRecreate.length === 0, + clearBackups: !physicalStateLost && clearsBackups(input.kind, lease.kind), + clearNativeResumeRefs: !physicalStateLost && lease.kind === "reset", + ...(physicalStateLost + ? { + errorCode: "runtime.subject_operation_failed" as const, + errorMessage: + "Runtime subject physical incarnation was lost before its checkpoint completed.", + } + : {}), expectedStatus: "destroying", - operationId: input.operationId, + lease, runtimeSubjectId: input.runtimeSubjectId, + source: "maintenance", }); if (!completed) { - throw new Error("Runtime subject changed before recreate completion."); + throw new Error("Runtime subject changed before operation completion."); + } + if (physicalStateLost) { + throw new RuntimeSubjectPhysicalStateLostError(input.runtimeSubjectId); } } catch (error) { - await appendCheckpointFailureDiagnostics(bindings, { - error, - runtimeSubjectId: input.runtimeSubjectId, - targets: input.targets, - }); - await markRuntimeSubjectFailed(bindings.DB, { - errorCode: getRuntimeSubjectOperationErrorCode(error), - errorMessage: getRuntimeOperationErrorMessage(error), - expectedStatus: destroyStarted ? "destroying" : "backing_up", - operationId: input.operationId, - runtimeSubjectId: input.runtimeSubjectId, - status: "cold", - }); + if (!(error instanceof RuntimeSubjectPhysicalStateLostError)) { + await markRuntimeSubjectOperationRepairNeeded(bindings.DB, { + errorCode: getRuntimeSubjectOperationErrorCode(error), + errorMessage: getRuntimeOperationErrorMessage(error), + expectedStatus: lease.status, + lease, + runtimeSubjectId: input.runtimeSubjectId, + source: "maintenance", + }); + } throw error; + } finally { + clearInterval(heartbeat); } } -export async function resetRuntimeSubjectAgentState( +async function startRuntimeSubjectOperation( bindings: ApiBindings, - input: RuntimeSubjectOperationInput, + input: { + readonly operationId: RuntimeOperationId; + readonly operationKind: DestructiveRuntimeSubjectOperationKind; + readonly runtimeSubjectId: SandboxId; + }, +): Promise { + const now = Date.now(); + const lease = await markRuntimeSubjectOperationStarted(bindings.DB, { + claimExpiresAt: now + RUNTIME_SUBJECT_OPERATION_LEASE_TTL_MS, + claimOwner: operationWorkerOwner("runtime-operation"), + now, + operationId: input.operationId, + operationKind: input.operationKind, + runtimeSubjectId: input.runtimeSubjectId, + source: "runtime", + }); + if (lease === null) { + throw new Error("Runtime subject is busy with lifecycle maintenance."); + } + return lease; +} + +async function executeRequestedRuntimeSubjectOperation( + bindings: ApiBindings, + input: RuntimeSubjectOperationInput & { + readonly operationKind: "recreate" | "reset"; + }, ): Promise { const subject = await getRuntimeSubject(bindings.DB, input.runtimeSubjectId); - - if (!subject) { + if (subject === null) { return; } - - const policy = getRuntimeKindPolicy(subject.kind); - - if (!policy.operations.resetSubjectState) { + if ( + input.operationKind === "reset" && + !getRuntimeKindPolicy(subject.kind).operations.resetSubjectState + ) { throw new Error("This runtime kind does not have resettable subject state."); } - const started = await markRuntimeSubjectOperationStarted(bindings.DB, { - operationId: input.operationId, - runtimeSubjectId: input.runtimeSubjectId, - status: "destroying", - }); + if (subject.status === "cold") { + if (input.operationKind === "recreate") { + return; + } + if (subject.agentId === null || subject.appId === null || subject.ownerAccountId === null) { + throw new Error("Cold runtime subject has no complete activation identity."); + } - if (!started) { + const activated = await createRuntimeSubjectLifecycleService(bindings).activate({ + agentId: subject.agentId, + appId: subject.appId, + executionOwnerUserId: subject.ownerAccountId, + kind: subject.kind, + networkConstraints: { allowedHosts: [], networkPolicy: "full" }, + runtimeSubjectId: subject.id, + subjectId: subject.subjectId, + subjectKind: subject.subjectKind, + }); + disposeRpcResource(activated.subject); + } else if (subject.status !== "active") { throw new Error("Runtime subject is busy with lifecycle maintenance."); } + const lease = await startRuntimeSubjectOperation(bindings, input); try { - await stopRuntimeSubjectDrivers(bindings, { - operationId: input.operationId, - runtimeSubjectId: input.runtimeSubjectId, - preserveSessionLifecycle: true, - reason: input.reason, - targets: input.targets, - terminalRun: input.terminalRun, - }); - const stateTargets = await listRuntimeSubjectSessionStateTargets(bindings.DB, { - runtimeSubjectId: input.runtimeSubjectId, - sessionIds: input.targets.map((target) => target.sessionId), - }); - await clearRuntimeSubjectAgentState(bindings, { - runtimeSubjectId: input.runtimeSubjectId, - rules: policy.checkpoint.clearOnReset, - stateTargets, - }); - await deleteNativeResumeRefsForSessions( - bindings.DB, - input.targets.map((target) => target.sessionId), - ); - await createSandboxCheckpoints(bindings, { - operationId: input.operationId, - rules: policy.checkpoint.createOnReset, - sandboxId: input.runtimeSubjectId, - }); - await destroyRuntimeSubjectContainer(bindings, input.runtimeSubjectId); - await appendTerminatedEventsForRuntimeSubject(bindings, { + await runRuntimeSubjectOperation(bindings, { + kind: subject.kind, + lease, reason: input.reason, runtimeSubjectId: input.runtimeSubjectId, targets: input.targets, }); - await closeRuntimeSubjectSessionsForRecycle(bindings.DB, input.runtimeSubjectId); - const completed = await markRuntimeSubjectCold(bindings.DB, { - clearBackups: true, - expectedStatus: "destroying", - operationId: input.operationId, - runtimeSubjectId: input.runtimeSubjectId, - }); - if (!completed) { - throw new Error("Runtime subject changed before reset completion."); - } } catch (error) { await appendCheckpointFailureDiagnostics(bindings, { error, runtimeSubjectId: input.runtimeSubjectId, targets: input.targets, }); - await markRuntimeSubjectFailed(bindings.DB, { - errorCode: getRuntimeSubjectOperationErrorCode(error), - errorMessage: getRuntimeOperationErrorMessage(error), - expectedStatus: "destroying", - operationId: input.operationId, - runtimeSubjectId: input.runtimeSubjectId, - status: "cold", - }); throw error; } } + +export function recreateRuntimeSubjectPreservingState( + bindings: ApiBindings, + input: RuntimeSubjectOperationInput, +): Promise { + return executeRequestedRuntimeSubjectOperation(bindings, { + ...input, + operationKind: "recreate", + }); +} + +export function resetRuntimeSubjectAgentState( + bindings: ApiBindings, + input: RuntimeSubjectOperationInput, +): Promise { + return executeRequestedRuntimeSubjectOperation(bindings, { + ...input, + operationKind: "reset", + }); +} diff --git a/apps/api/src/modules/runtime/infrastructure/runtime-subject-lifecycle/runtime-subject-platform.ts b/apps/api/src/modules/runtime/infrastructure/runtime-subject-lifecycle/runtime-subject-platform.ts index dc3968e0..47735af1 100644 --- a/apps/api/src/modules/runtime/infrastructure/runtime-subject-lifecycle/runtime-subject-platform.ts +++ b/apps/api/src/modules/runtime/infrastructure/runtime-subject-lifecycle/runtime-subject-platform.ts @@ -11,27 +11,56 @@ import { } from "../../../../platform/cloudflare/rpc-disposal"; import { requireCloudflareSandboxBinding } from "../../../../platform/cloudflare/sandbox-binding"; import type { ApiBindings } from "../../../../platform/cloudflare/worker-types"; +import { quoteShellArg } from "../../../../shared/shell"; import type { RuntimeStateClearRule } from "../../domain/runtime-kind-policy"; import type { SandboxNetworkConstraints } from "../../domain/sandbox-network-constraints"; import { withRuntimeProvisionTimeout } from "../runtime-provision-timeout"; import { decodeSandboxBackupIdForPlatform } from "../sandbox-backup-id"; -import { toSandboxHandle } from "../sandbox-handles"; +import { toRuntimeSubjectIncarnationHandle, toSandboxHandle } from "../sandbox-handles"; import type { SandboxHandle } from "../sandbox-handles"; import type { ReadyRuntimeSubjectBackupRecord } from "./runtime-subject-store"; -function quoteShellArg(value: string): string { - return `'${value.replaceAll("'", `'"'"'`)}'`; -} - export function getRuntimeSubjectKeepAliveHandle( bindings: ApiBindings, runtimeSubjectId: string, + incarnation: number, +): Promise { + const physicalId = + incarnation === 0 ? runtimeSubjectId : `${runtimeSubjectId}-i${incarnation.toString(36)}`; + if (bindings.runtimeSubjectHandleFactory) { + return Promise.resolve(toSandboxHandle(bindings.runtimeSubjectHandleFactory(physicalId))); + } + + return getCloudflareRuntimeSubjectKeepAliveHandle(bindings, physicalId); +} + +function getUnversionedSandboxHandle( + bindings: ApiBindings, + sandboxId: string, +): Promise { + if (bindings.runtimeSubjectHandleFactory) { + return Promise.resolve(toSandboxHandle(bindings.runtimeSubjectHandleFactory(sandboxId))); + } + return getCloudflareRuntimeSubjectKeepAliveHandle(bindings, sandboxId); +} + +export function getEphemeralUnversionedSandboxHandle( + bindings: ApiBindings, + sandboxId: string, + sleepAfter: string | number, ): Promise { if (bindings.runtimeSubjectHandleFactory) { - return Promise.resolve(toSandboxHandle(bindings.runtimeSubjectHandleFactory(runtimeSubjectId))); + return Promise.resolve(toSandboxHandle(bindings.runtimeSubjectHandleFactory(sandboxId))); } + return getCloudflareEphemeralSandboxHandle(bindings, sandboxId, sleepAfter); +} - return getCloudflareRuntimeSubjectKeepAliveHandle(bindings, runtimeSubjectId); +export function createEphemeralSandboxOptions(sleepAfter: string | number): { + readonly keepAlive: false; + readonly normalizeId: true; + readonly sleepAfter: string | number; +} { + return { keepAlive: false, normalizeId: true, sleepAfter }; } async function getCloudflareRuntimeSubjectKeepAliveHandle( @@ -46,6 +75,21 @@ async function getCloudflareRuntimeSubjectKeepAliveHandle( return toSandboxHandle(sandbox); } +async function getCloudflareEphemeralSandboxHandle( + bindings: ApiBindings, + sandboxId: string, + sleepAfter: string | number, +): Promise { + const { getSandbox } = await import("@cloudflare/sandbox"); + return toSandboxHandle( + getSandbox( + requireCloudflareSandboxBinding(bindings), + sandboxId, + createEphemeralSandboxOptions(sleepAfter), + ), + ); +} + /** * Pushes the environment egress policy into the sandbox Durable Object. Must * run before any container-starting call (mkdir/exec/session create): the @@ -83,6 +127,48 @@ export async function prepareRuntimeSubjectFilesystem(subject: SandboxHandle): P ); } +export async function activateRuntimeSubjectIncarnation( + subject: SandboxHandle, + incarnation: number, + networkConstraintsHash: string, +): Promise { + await withRuntimeProvisionTimeout( + toRuntimeSubjectIncarnationHandle(subject).activateRuntimeSubjectIncarnation( + incarnation, + networkConstraintsHash, + ), + `Runtime subject incarnation ${incarnation} activation`, + ); +} + +export async function inspectRuntimeSubjectIncarnation( + subject: SandboxHandle, + incarnation: number, + networkConstraintsHash: string, +): Promise<{ kind: "healthy" | "missing" | "retired" | "stale" | "unknown" }> { + return withRuntimeProvisionTimeout( + toRuntimeSubjectIncarnationHandle(subject).inspectRuntimeSubjectIncarnation( + incarnation, + networkConstraintsHash, + ), + `Runtime subject incarnation ${incarnation} inspection`, + ); +} + +export async function markRuntimeSubjectIncarnationReady( + subject: SandboxHandle, + incarnation: number, + networkConstraintsHash: string, +): Promise { + await withRuntimeProvisionTimeout( + toRuntimeSubjectIncarnationHandle(subject).markRuntimeSubjectIncarnationReady( + incarnation, + networkConstraintsHash, + ), + `Runtime subject incarnation ${incarnation} readiness`, + ); +} + export async function restoreRuntimeSubjectBackup( subject: SandboxHandle, input: { @@ -105,15 +191,21 @@ export async function restoreRuntimeSubjectBackup( export async function destroyRuntimeSubjectContainer( bindings: ApiBindings, runtimeSubjectId: string, + incarnation: number, timeoutMs?: number, ): Promise { await withRuntimeProvisionTimeout( (async () => withDisposedRpcResource( - await getRuntimeSubjectKeepAliveHandle(bindings, runtimeSubjectId), + await getRuntimeSubjectKeepAliveHandle(bindings, runtimeSubjectId, incarnation), async (subject) => { - await subject.setKeepAlive(false); - await subject.destroy(); + const outcome = + await toRuntimeSubjectIncarnationHandle(subject).destroyRuntimeSubjectIncarnation( + incarnation, + ); + if (outcome.kind === "stale") { + throw new Error("Runtime subject destroy targeted a stale incarnation."); + } }, ))(), `Runtime subject destroy for ${runtimeSubjectId}`, @@ -121,16 +213,35 @@ export async function destroyRuntimeSubjectContainer( ); } +export async function destroyUnversionedSandboxContainer( + bindings: ApiBindings, + sandboxId: string, + timeoutMs?: number, +): Promise { + await withRuntimeProvisionTimeout( + withDisposedRpcResource( + await getUnversionedSandboxHandle(bindings, sandboxId), + async (sandbox) => { + await sandbox.setKeepAlive(false); + await sandbox.destroy(); + }, + ), + `Sandbox destroy for ${sandboxId}`, + timeoutMs, + ); +} + export async function clearRuntimeSubjectAgentState( bindings: ApiBindings, input: { readonly rules: readonly RuntimeStateClearRule[]; + readonly incarnation: number; readonly runtimeSubjectId: string; readonly stateTargets: readonly string[]; }, ): Promise { await withDisposedRpcResource( - await getRuntimeSubjectKeepAliveHandle(bindings, input.runtimeSubjectId), + await getRuntimeSubjectKeepAliveHandle(bindings, input.runtimeSubjectId, input.incarnation), async (subject) => { const commands = input.rules.flatMap((rule) => { switch (rule.type) { diff --git a/apps/api/src/modules/runtime/infrastructure/runtime-subject-lifecycle/runtime-subject-record-store.ts b/apps/api/src/modules/runtime/infrastructure/runtime-subject-lifecycle/runtime-subject-record-store.ts index 8f02a2fc..47f2b562 100644 --- a/apps/api/src/modules/runtime/infrastructure/runtime-subject-lifecycle/runtime-subject-record-store.ts +++ b/apps/api/src/modules/runtime/infrastructure/runtime-subject-lifecycle/runtime-subject-record-store.ts @@ -1,6 +1,17 @@ import type { AgentKind } from "@mosoo/contracts/agent"; -import type { RuntimeSubjectErrorCode, SandboxSubjectKind } from "@mosoo/contracts/sandbox"; -import { sandboxesTable } from "@mosoo/db"; +import type { + RuntimeSubjectErrorCode, + SandboxOperationKind, + SandboxSubjectKind, +} from "@mosoo/contracts/sandbox"; +import { + driverInstancesTable, + nativeResumeRefsTable, + sandboxesTable, + sandboxSessionsTable, + sessionRunsTable, + sessionsTable, +} from "@mosoo/db"; import { createPlatformId } from "@mosoo/id"; import type { AccountId, @@ -10,11 +21,30 @@ import type { RuntimeOperationId, SandboxBackupId, SandboxId, + SessionId, + SessionRunId, } from "@mosoo/id"; -import { and, eq, inArray, isNull, lte, notExists, or, sql } from "drizzle-orm"; +import { + and, + eq, + exists, + inArray, + isNotNull, + isNull, + lte, + ne, + notExists, + or, + sql, +} from "drizzle-orm"; import type { SQL } from "drizzle-orm"; +import { alias } from "drizzle-orm/sqlite-core"; -import { getAppDatabase, getD1ChangeCount } from "../../../../platform/db/drizzle"; +import { + getAppDatabase, + getD1ChangeCount, + runAppDatabaseBatch, +} from "../../../../platform/db/drizzle"; import { currentTimestampMs } from "../../../../time"; import { getRuntimeKindPolicy, @@ -25,6 +55,7 @@ import { toRuntimeSubjectStatusLifecycleEventName, } from "../../domain/runtime-subject-lifecycle.machine"; import type { RuntimeSubjectOperationStatus } from "../../domain/runtime-subject-lifecycle.machine"; +import { ACTIVE_SESSION_RUN_STATUSES } from "../../domain/session-run-lifecycle.machine"; import { activeSessionRunQueryForListedSubject, lastBackupTable, @@ -32,9 +63,11 @@ import { mapReadyRuntimeSubjectBackup, readyLastBackupTable, runLeaseQuery, + runtimeProvisioningQuery, } from "./runtime-subject-store-queries"; import type { RuntimeSubjectActivationRecord, + RuntimeSubjectOperationLease, RuntimeSubjectRecord, RuntimeSubjectStatus, } from "./runtime-subject-store.types"; @@ -68,6 +101,7 @@ function runtimeSubjectAccountCapacityPredicate(input: { function runtimeSubjectStatusPatch(input: { readonly now: number; + readonly operationKind: SandboxOperationKind | null; readonly operationId: RuntimeOperationId | null; readonly source: "api" | "maintenance" | "runtime"; readonly status: RuntimeSubjectStatus; @@ -77,24 +111,13 @@ function runtimeSubjectStatusPatch(input: { statusChangedAt: input.now, statusEvent: toRuntimeSubjectStatusLifecycleEventName(input.status), statusOperationId: input.operationId ?? null, + operationKind: input.operationKind, statusSeq: sql`${sandboxesTable.statusSeq} + 1`, statusSource: input.source, updatedAt: input.now, } as const; } -function runtimeSubjectStatusOperationCondition( - operationId: RuntimeOperationId | null | undefined, -): SQL[] { - if (operationId === undefined) { - return []; - } - - return operationId === null - ? [isNull(sandboxesTable.statusOperationId)] - : [eq(sandboxesTable.statusOperationId, operationId)]; -} - export async function getRuntimeSubject( database: D1Database, runtimeSubjectId: SandboxId, @@ -102,9 +125,15 @@ export async function getRuntimeSubject( const row = (await getAppDatabase(database) .select({ + agentId: sandboxesTable.agentId, + appId: sandboxesTable.appId, id: sandboxesTable.id, + incarnation: sandboxesTable.incarnation, kind: sandboxesTable.kind, + networkConstraintsHash: sandboxesTable.networkConstraintsHash, + ownerAccountId: sandboxesTable.ownerAccountId, status: sandboxesTable.status, + subjectId: sandboxesTable.subjectId, subjectKind: sandboxesTable.subjectKind, }) .from(sandboxesTable) @@ -156,6 +185,16 @@ export async function ensureRuntimeSubjectId( const existing = await getRuntimeSubjectIdByTuple(database, input); if (existing !== null) { + const record = await getRuntimeSubject(database, existing); + if ( + record === null || + (input.runtimeSubjectId !== undefined && record.id !== input.runtimeSubjectId) || + record.agentId !== input.agentId || + record.appId !== input.appId || + record.ownerAccountId !== input.executionOwnerUserId + ) { + throw new Error("Runtime subject identity does not match the allocation request."); + } return existing; } @@ -173,8 +212,11 @@ export async function ensureRuntimeSubjectId( globalMountsJson: "[]", id: runtimeSubjectId, inactiveDeadlineAt: getRuntimeSubjectInactiveDeadline(getRuntimeKindPolicy(input.kind), now), + incarnation: 0, kind: input.kind, ownerAccountId: input.executionOwnerUserId, + networkConstraintsHash: null, + operationKind: null, status: "cold", statusChangedAt: now, statusEvent: toRuntimeSubjectStatusLifecycleEventName("cold"), @@ -198,6 +240,17 @@ export async function ensureRuntimeSubjectId( throw new Error("Runtime subject could not be allocated."); } + const concurrentRecord = await getRuntimeSubject(database, createdByConcurrentRequest); + if ( + concurrentRecord === null || + (input.runtimeSubjectId !== undefined && concurrentRecord.id !== input.runtimeSubjectId) || + concurrentRecord.agentId !== input.agentId || + concurrentRecord.appId !== input.appId || + concurrentRecord.ownerAccountId !== input.executionOwnerUserId + ) { + throw new Error("Runtime subject identity does not match the allocation request."); + } + return createdByConcurrentRequest; } @@ -208,9 +261,12 @@ export async function getRuntimeSubjectActivationRecord( const row = (await getAppDatabase(database) .select({ + agentId: sandboxesTable.agentId, + appId: sandboxesTable.appId, claimExpiresAt: sandboxesTable.claimExpiresAt, claimOwner: sandboxesTable.claimOwner, id: sandboxesTable.id, + incarnation: sandboxesTable.incarnation, kind: sandboxesTable.kind, lastError: sandboxesTable.lastError, lastErrorCode: sandboxesTable.lastErrorCode, @@ -219,7 +275,13 @@ export async function getRuntimeSubjectActivationRecord( lastBackupStatus: lastBackupTable.status, lastReadyBackupDir: readyLastBackupTable.dir, lastReadyBackupId: readyLastBackupTable.id, + networkConstraintsHash: sandboxesTable.networkConstraintsHash, + ownerAccountId: sandboxesTable.ownerAccountId, + operationId: sandboxesTable.statusOperationId, + operationKind: sandboxesTable.operationKind, status: sandboxesTable.status, + subjectId: sandboxesTable.subjectId, + subjectKind: sandboxesTable.subjectKind, }) .from(sandboxesTable) .leftJoin( @@ -246,9 +308,12 @@ export async function getRuntimeSubjectActivationRecord( } return { + agentId: row.agentId, + appId: row.appId, claimExpiresAt: row.claimExpiresAt, claimOwner: row.claimOwner, id: row.id, + incarnation: row.incarnation, kind: row.kind, lastError: row.lastError, lastErrorCode: row.lastErrorCode, @@ -261,7 +326,13 @@ export async function getRuntimeSubjectActivationRecord( dir: row.lastReadyBackupDir, id: row.lastReadyBackupId, }), + networkConstraintsHash: row.networkConstraintsHash, + ownerAccountId: row.ownerAccountId, + operationId: row.operationId, + operationKind: row.operationKind, status: row.status, + subjectId: row.subjectId, + subjectKind: row.subjectKind, }; } @@ -280,16 +351,16 @@ export async function claimRuntimeSubjectActivation( (await getAppDatabase(database) .update(sandboxesTable) .set({ - agentId: input.agentId, - appId: input.appId, claimExpiresAt: input.claimExpiresAt, claimOwner: input.claimOwner, - ownerAccountId: input.executionOwnerUserId, updatedAt: input.now, }) .where( and( eq(sandboxesTable.id, input.runtimeSubjectId), + eq(sandboxesTable.agentId, input.agentId), + eq(sandboxesTable.appId, input.appId), + eq(sandboxesTable.ownerAccountId, input.executionOwnerUserId), eq(sandboxesTable.status, input.expectedStatus), inArray(sandboxesTable.status, RUNTIME_SUBJECT_CLAIMABLE_STATUSES), or( @@ -347,18 +418,25 @@ export async function markRuntimeSubjectRestoring( database: D1Database, input: { readonly claimOwner: string; + readonly expectedIncarnation: number; + readonly expectedStatus: "active" | "cold"; + readonly networkConstraintsHash: string; + readonly operationId: RuntimeOperationId; readonly runtimeSubjectId: SandboxId; }, -): Promise { +): Promise { const now = currentTimestampMs(); - const result = await getAppDatabase(database) + const row = await getAppDatabase(database) .update(sandboxesTable) .set({ + incarnation: sql`${sandboxesTable.incarnation} + 1`, lastError: null, lastErrorCode: null, + networkConstraintsHash: input.networkConstraintsHash, ...runtimeSubjectStatusPatch({ now, - operationId: null, + operationId: input.operationId, + operationKind: "activate", source: "api", status: "restoring", }), @@ -367,23 +445,145 @@ export async function markRuntimeSubjectRestoring( and( eq(sandboxesTable.id, input.runtimeSubjectId), eq(sandboxesTable.claimOwner, input.claimOwner), - eq(sandboxesTable.status, "cold"), + eq(sandboxesTable.incarnation, input.expectedIncarnation), + isNotNull(sandboxesTable.claimExpiresAt), + eq(sandboxesTable.status, input.expectedStatus), + isNull(sandboxesTable.operationKind), + isNull(sandboxesTable.statusOperationId), ), ) - .run(); + .returning({ + claimExpiresAt: sandboxesTable.claimExpiresAt, + claimOwner: sandboxesTable.claimOwner, + incarnation: sandboxesTable.incarnation, + operationId: sandboxesTable.statusOperationId, + }) + .get(); - return getD1ChangeCount(result) > 0; + return row?.claimExpiresAt === null || row?.claimOwner === null || row?.operationId === null + ? null + : { + claimExpiresAt: row.claimExpiresAt, + claimOwner: row.claimOwner, + incarnation: row.incarnation, + kind: "activate", + operationId: row.operationId, + status: "restoring", + }; +} + +export async function markRuntimeSubjectActiveDestroying( + database: D1Database, + input: { + readonly claimOwner: string; + readonly errorCode: RuntimeSubjectErrorCode; + readonly expectedIncarnation: number; + readonly message: string; + readonly operationId: RuntimeOperationId; + readonly provisioningOperationId: RuntimeOperationId; + readonly provisioningRunId: SessionRunId; + readonly provisioningSessionId: SessionId; + readonly runtimeSubjectId: SandboxId; + }, +): Promise { + const now = currentTimestampMs(); + const db = getAppDatabase(database); + const retirementRuns = alias(sessionRunsTable, "runtime_subject_retirement_run"); + const retirementDrivers = alias(driverInstancesTable, "runtime_subject_retirement_driver"); + const retirementProvisioning = alias(sessionsTable, "runtime_subject_retirement_provisioning"); + const ownedProvisioning = db + .select({ id: sessionsTable.id }) + .from(sessionsTable) + .where( + and( + eq(sessionsTable.id, input.provisioningSessionId), + eq(sessionsTable.runtimeProvisioningOperationId, input.provisioningOperationId), + eq(sessionsTable.runtimeProvisioningRunId, input.provisioningRunId), + eq(sessionsTable.runtimeProvisioningSandboxId, input.runtimeSubjectId), + or( + isNull(sessionsTable.runtimeProvisioningSandboxIncarnation), + eq(sessionsTable.runtimeProvisioningSandboxIncarnation, input.expectedIncarnation), + ), + ), + ); + const otherProvisioning = db + .select({ id: retirementProvisioning.id }) + .from(retirementProvisioning) + .where( + and( + eq(retirementProvisioning.runtimeProvisioningSandboxId, input.runtimeSubjectId), + isNotNull(retirementProvisioning.runtimeProvisioningOperationId), + ne(retirementProvisioning.runtimeProvisioningOperationId, input.provisioningOperationId), + ), + ); + const otherRun = db + .select({ id: retirementRuns.id }) + .from(retirementRuns) + .innerJoin(retirementDrivers, eq(retirementDrivers.id, retirementRuns.driverInstanceId)) + .where( + and( + ne(retirementRuns.id, input.provisioningRunId), + eq(retirementDrivers.sandboxId, input.runtimeSubjectId), + eq(retirementDrivers.sandboxIncarnation, input.expectedIncarnation), + inArray(retirementRuns.status, ACTIVE_SESSION_RUN_STATUSES), + ), + ); + const row = await db + .update(sandboxesTable) + .set({ + lastError: input.message, + lastErrorCode: input.errorCode, + ...runtimeSubjectStatusPatch({ + now, + operationId: input.operationId, + operationKind: "activate", + source: "api", + status: "destroying", + }), + }) + .where( + and( + eq(sandboxesTable.id, input.runtimeSubjectId), + eq(sandboxesTable.claimOwner, input.claimOwner), + eq(sandboxesTable.incarnation, input.expectedIncarnation), + isNotNull(sandboxesTable.claimExpiresAt), + eq(sandboxesTable.status, "active"), + isNull(sandboxesTable.operationKind), + isNull(sandboxesTable.statusOperationId), + exists(ownedProvisioning), + notExists(otherProvisioning), + notExists(otherRun), + ), + ) + .returning({ + claimExpiresAt: sandboxesTable.claimExpiresAt, + claimOwner: sandboxesTable.claimOwner, + incarnation: sandboxesTable.incarnation, + operationId: sandboxesTable.statusOperationId, + }) + .get(); + + return row?.claimExpiresAt === null || row?.claimOwner === null || row?.operationId === null + ? null + : { + claimExpiresAt: row.claimExpiresAt, + claimOwner: row.claimOwner, + incarnation: row.incarnation, + kind: "activate", + operationId: row.operationId, + status: "destroying", + }; } export async function markRuntimeSubjectRestoreApplied( database: D1Database, input: { readonly backupId: SandboxBackupId; - readonly claimOwner: string; + readonly lease: RuntimeSubjectOperationLease; readonly runtimeSubjectId: SandboxId; }, -): Promise { - await getAppDatabase(database) +): Promise { + const result = await getAppDatabase(database) .update(sandboxesTable) .set({ lastRestoreBackupId: input.backupId, @@ -392,17 +592,26 @@ export async function markRuntimeSubjectRestoreApplied( .where( and( eq(sandboxesTable.id, input.runtimeSubjectId), - eq(sandboxesTable.claimOwner, input.claimOwner), + eq(sandboxesTable.claimOwner, input.lease.claimOwner), + eq(sandboxesTable.incarnation, input.lease.incarnation), + eq(sandboxesTable.operationKind, "activate"), + eq(sandboxesTable.status, "restoring"), + eq(sandboxesTable.statusOperationId, input.lease.operationId), ), ) .run(); + + return getD1ChangeCount(result) === 1; } export async function markRuntimeSubjectActive( database: D1Database, input: { readonly claimOwner: string; + readonly incarnation: number; readonly kind: AgentKind; + readonly networkConstraintsHash: string; + readonly operationId: RuntimeOperationId | null; readonly runtimeSubjectId: SandboxId; }, ): Promise { @@ -417,6 +626,7 @@ export async function markRuntimeSubjectActive( inactiveDeadlineAt: getRuntimeSubjectInactiveDeadline(getRuntimeKindPolicy(input.kind), now), lastError: null, lastErrorCode: null, + operationKind: null, status: "active", statusChangedAt: sql` CASE @@ -449,7 +659,15 @@ export async function markRuntimeSubjectActive( and( eq(sandboxesTable.id, input.runtimeSubjectId), eq(sandboxesTable.claimOwner, input.claimOwner), - inArray(sandboxesTable.status, ["restoring", "active"]), + eq(sandboxesTable.incarnation, input.incarnation), + eq(sandboxesTable.networkConstraintsHash, input.networkConstraintsHash), + ...(input.operationId === null + ? [eq(sandboxesTable.status, "active"), isNull(sandboxesTable.statusOperationId)] + : [ + eq(sandboxesTable.status, "restoring"), + eq(sandboxesTable.operationKind, "activate"), + eq(sandboxesTable.statusOperationId, input.operationId), + ]), ), ) .run(); @@ -460,10 +678,9 @@ export async function markRuntimeSubjectActive( export async function markRuntimeSubjectActivationDestroying( database: D1Database, input: { - readonly claimOwner: string; readonly message: string; readonly errorCode: RuntimeSubjectErrorCode; - readonly operationId: RuntimeOperationId; + readonly lease: RuntimeSubjectOperationLease; readonly runtimeSubjectId: SandboxId; }, ): Promise { @@ -472,13 +689,12 @@ export async function markRuntimeSubjectActivationDestroying( const result = await getAppDatabase(database) .update(sandboxesTable) .set({ - claimExpiresAt: null, - claimOwner: null, lastError: input.message, lastErrorCode: input.errorCode, ...runtimeSubjectStatusPatch({ now, - operationId: input.operationId, + operationId: input.lease.operationId, + operationKind: "activate", source: "api", status: "destroying", }), @@ -486,10 +702,11 @@ export async function markRuntimeSubjectActivationDestroying( .where( and( eq(sandboxesTable.id, input.runtimeSubjectId), - eq(sandboxesTable.claimOwner, input.claimOwner), - // Activation can fail at any point after the claim: still cold (during - // prepareFilesystem), restoring (during restore), or active. - inArray(sandboxesTable.status, ["cold", "restoring", "active"]), + eq(sandboxesTable.claimOwner, input.lease.claimOwner), + eq(sandboxesTable.incarnation, input.lease.incarnation), + eq(sandboxesTable.operationKind, "activate"), + eq(sandboxesTable.status, "restoring"), + eq(sandboxesTable.statusOperationId, input.lease.operationId), ), ) .run(); @@ -502,7 +719,7 @@ export async function markRuntimeSubjectActivationFailed( input: { readonly message: string; readonly errorCode: RuntimeSubjectErrorCode; - readonly operationId: RuntimeOperationId; + readonly lease: RuntimeSubjectOperationLease; readonly runtimeSubjectId: SandboxId; }, ): Promise { @@ -520,6 +737,7 @@ export async function markRuntimeSubjectActivationFailed( ...runtimeSubjectStatusPatch({ now, operationId: null, + operationKind: null, source: "api", status: "cold", }), @@ -527,8 +745,11 @@ export async function markRuntimeSubjectActivationFailed( .where( and( eq(sandboxesTable.id, input.runtimeSubjectId), + eq(sandboxesTable.claimOwner, input.lease.claimOwner), + eq(sandboxesTable.incarnation, input.lease.incarnation), + eq(sandboxesTable.operationKind, "activate"), eq(sandboxesTable.status, "destroying"), - eq(sandboxesTable.statusOperationId, input.operationId), + eq(sandboxesTable.statusOperationId, input.lease.operationId), ), ) .run(); @@ -539,62 +760,72 @@ export async function markRuntimeSubjectActivationFailed( export async function markRuntimeSubjectOperationStarted( database: D1Database, input: { - readonly claimOwner?: string; + readonly claimExpiresAt: number; + readonly claimOwner: string; readonly now?: number; - readonly operationId?: RuntimeOperationId | null; + readonly operationId: RuntimeOperationId; + readonly operationKind: Exclude; readonly runtimeSubjectId: SandboxId; readonly source?: "api" | "maintenance" | "runtime"; - readonly status: RuntimeSubjectOperationStatus; + readonly status?: "backing_up"; }, -): Promise { +): Promise { const now = input.now ?? currentTimestampMs(); const appDb = getAppDatabase(database); - const claimPredicate = - input.claimOwner === undefined - ? or( - isNull(sandboxesTable.claimOwner), - isNull(sandboxesTable.claimExpiresAt), - lte(sandboxesTable.claimExpiresAt, now), - ) - : eq(sandboxesTable.claimOwner, input.claimOwner); - const result = await appDb + const claimPredicate = or( + eq(sandboxesTable.claimOwner, input.claimOwner), + isNull(sandboxesTable.claimOwner), + isNull(sandboxesTable.claimExpiresAt), + lte(sandboxesTable.claimExpiresAt, now), + ); + const row = await appDb .update(sandboxesTable) .set({ - claimExpiresAt: null, - claimOwner: null, + claimExpiresAt: input.claimExpiresAt, + claimOwner: input.claimOwner, inactiveDeadlineAt: null, lastError: null, lastErrorCode: null, ...runtimeSubjectStatusPatch({ now, - operationId: input.operationId ?? null, + operationId: input.operationId, + operationKind: input.operationKind, source: input.source ?? "api", - status: input.status, + status: "backing_up", }), }) .where( and( eq(sandboxesTable.id, input.runtimeSubjectId), - inArray(sandboxesTable.status, RUNTIME_SUBJECT_CLAIMABLE_STATUSES), + eq(sandboxesTable.status, "active"), + isNull(sandboxesTable.operationKind), + isNull(sandboxesTable.statusOperationId), claimPredicate, - ...(input.source === "maintenance" - ? [ - notExists(activeSessionRunQueryForListedSubject(appDb)), - notExists(runLeaseQuery(appDb, input.runtimeSubjectId)), - ] - : []), + notExists(runtimeProvisioningQuery(appDb, input.runtimeSubjectId)), + notExists(activeSessionRunQueryForListedSubject(appDb)), + notExists(runLeaseQuery(appDb, input.runtimeSubjectId)), ), ) - .run(); + .returning({ incarnation: sandboxesTable.incarnation }) + .get(); - return getD1ChangeCount(result) > 0; + return row === undefined + ? null + : { + claimExpiresAt: input.claimExpiresAt, + claimOwner: input.claimOwner, + incarnation: row.incarnation, + kind: input.operationKind, + operationId: input.operationId, + status: "backing_up", + }; } export async function advanceRuntimeSubjectOperationStatus( database: D1Database, input: { - readonly expectedStatus: RuntimeSubjectOperationStatus; - readonly operationId?: RuntimeOperationId | null; + readonly expectedStatus: RuntimeSubjectOperationLease["status"]; + readonly lease: RuntimeSubjectOperationLease; readonly runtimeSubjectId: SandboxId; readonly source?: "api" | "maintenance" | "runtime"; readonly status: RuntimeSubjectOperationStatus; @@ -606,7 +837,8 @@ export async function advanceRuntimeSubjectOperationStatus( .set({ ...runtimeSubjectStatusPatch({ now, - operationId: input.operationId ?? null, + operationId: input.lease.operationId, + operationKind: input.lease.kind, source: input.source ?? "api", status: input.status, }), @@ -615,7 +847,10 @@ export async function advanceRuntimeSubjectOperationStatus( and( eq(sandboxesTable.id, input.runtimeSubjectId), eq(sandboxesTable.status, input.expectedStatus), - ...runtimeSubjectStatusOperationCondition(input.operationId), + eq(sandboxesTable.claimOwner, input.lease.claimOwner), + eq(sandboxesTable.incarnation, input.lease.incarnation), + eq(sandboxesTable.operationKind, input.lease.kind), + eq(sandboxesTable.statusOperationId, input.lease.operationId), ), ) .run(); @@ -627,8 +862,11 @@ export async function markRuntimeSubjectCold( database: D1Database, input: { readonly clearBackups: boolean; - readonly expectedStatus: RuntimeSubjectOperationStatus; - readonly operationId?: RuntimeOperationId | null; + readonly clearNativeResumeRefs?: boolean; + readonly errorCode?: RuntimeSubjectErrorCode; + readonly errorMessage?: string; + readonly expectedStatus: "destroying"; + readonly lease: RuntimeSubjectOperationLease; readonly runtimeSubjectId: SandboxId; readonly source?: "api" | "maintenance" | "runtime"; }, @@ -641,27 +879,101 @@ export async function markRuntimeSubjectCold( } : {}; + const results = await runAppDatabaseBatch(database, (appDb) => { + const ownedOperation = and( + eq(sandboxesTable.id, input.runtimeSubjectId), + eq(sandboxesTable.status, input.expectedStatus), + eq(sandboxesTable.claimOwner, input.lease.claimOwner), + eq(sandboxesTable.incarnation, input.lease.incarnation), + eq(sandboxesTable.operationKind, input.lease.kind), + eq(sandboxesTable.statusOperationId, input.lease.operationId), + ); + const stillOwned = exists( + appDb.select({ id: sandboxesTable.id }).from(sandboxesTable).where(ownedOperation), + ); + + return [ + appDb.delete(nativeResumeRefsTable).where( + and( + input.clearNativeResumeRefs ? sql`TRUE` : sql`FALSE`, + stillOwned, + exists( + appDb + .select({ sessionId: sandboxSessionsTable.sessionId }) + .from(sandboxSessionsTable) + .where( + and( + eq(sandboxSessionsTable.sessionId, nativeResumeRefsTable.sessionId), + eq(sandboxSessionsTable.sandboxId, input.runtimeSubjectId), + eq(sandboxSessionsTable.sandboxIncarnation, input.lease.incarnation), + ), + ), + ), + ), + ), + appDb + .update(sandboxSessionsTable) + .set({ cleanupOperationId: null, status: "closed", updatedAt: now }) + .where( + and( + eq(sandboxSessionsTable.sandboxId, input.runtimeSubjectId), + eq(sandboxSessionsTable.sandboxIncarnation, input.lease.incarnation), + inArray(sandboxSessionsTable.status, ["active", "cleanup_pending", "error"]), + stillOwned, + ), + ), + appDb + .update(sandboxesTable) + .set({ + ...backupFields, + claimExpiresAt: null, + claimOwner: null, + inactiveDeadlineAt: null, + lastError: input.errorMessage ?? null, + lastErrorCode: input.errorCode ?? null, + ...runtimeSubjectStatusPatch({ + now, + operationId: null, + operationKind: null, + source: input.source ?? "api", + status: "cold", + }), + }) + .where(ownedOperation), + ]; + }); + + return getD1ChangeCount(results[2]) > 0; +} + +export async function markRuntimeSubjectOperationRepairNeeded( + database: D1Database, + input: { + readonly errorMessage: string; + readonly errorCode: RuntimeSubjectErrorCode; + readonly expectedStatus: RuntimeSubjectOperationLease["status"]; + readonly lease: RuntimeSubjectOperationLease; + readonly runtimeSubjectId: SandboxId; + readonly source?: "api" | "maintenance" | "runtime"; + }, +): Promise { + const now = currentTimestampMs(); const result = await getAppDatabase(database) .update(sandboxesTable) .set({ - ...backupFields, - claimExpiresAt: null, - claimOwner: null, - inactiveDeadlineAt: null, - lastError: null, - lastErrorCode: null, - ...runtimeSubjectStatusPatch({ - now, - operationId: input.operationId ?? null, - source: input.source ?? "api", - status: "cold", - }), + claimExpiresAt: now, + lastError: input.errorMessage, + lastErrorCode: input.errorCode, + updatedAt: now, }) .where( and( eq(sandboxesTable.id, input.runtimeSubjectId), eq(sandboxesTable.status, input.expectedStatus), - ...runtimeSubjectStatusOperationCondition(input.operationId), + eq(sandboxesTable.claimOwner, input.lease.claimOwner), + eq(sandboxesTable.incarnation, input.lease.incarnation), + eq(sandboxesTable.operationKind, input.lease.kind), + eq(sandboxesTable.statusOperationId, input.lease.operationId), ), ) .run(); @@ -669,37 +981,28 @@ export async function markRuntimeSubjectCold( return getD1ChangeCount(result) > 0; } -export async function markRuntimeSubjectOperationRepairNeeded( +export async function renewRuntimeSubjectOperationLease( database: D1Database, input: { - readonly errorMessage: string; - readonly errorCode: RuntimeSubjectErrorCode; - readonly expectedStatus: RuntimeSubjectOperationStatus; - readonly operationId: RuntimeOperationId; + readonly claimExpiresAt: number; + readonly lease: RuntimeSubjectOperationLease; readonly runtimeSubjectId: SandboxId; - readonly source?: "api" | "maintenance" | "runtime"; }, ): Promise { const now = currentTimestampMs(); const result = await getAppDatabase(database) .update(sandboxesTable) .set({ - claimExpiresAt: null, - claimOwner: null, - lastError: input.errorMessage, - lastErrorCode: input.errorCode, - ...runtimeSubjectStatusPatch({ - now, - operationId: input.operationId, - source: input.source ?? "maintenance", - status: input.expectedStatus, - }), + claimExpiresAt: input.claimExpiresAt, + updatedAt: now, }) .where( and( eq(sandboxesTable.id, input.runtimeSubjectId), - eq(sandboxesTable.status, input.expectedStatus), - eq(sandboxesTable.statusOperationId, input.operationId), + eq(sandboxesTable.claimOwner, input.lease.claimOwner), + eq(sandboxesTable.incarnation, input.lease.incarnation), + eq(sandboxesTable.operationKind, input.lease.kind), + eq(sandboxesTable.statusOperationId, input.lease.operationId), ), ) .run(); @@ -707,29 +1010,16 @@ export async function markRuntimeSubjectOperationRepairNeeded( return getD1ChangeCount(result) > 0; } -export async function markRuntimeSubjectFailed( +export async function releaseRuntimeSubjectActivationClaim( database: D1Database, input: { - readonly errorMessage: string; + readonly claimOwner: string; readonly errorCode: RuntimeSubjectErrorCode; - readonly expectedStatus?: RuntimeSubjectStatus; - readonly operationId?: RuntimeOperationId | null; + readonly errorMessage: string; + readonly incarnation: number; readonly runtimeSubjectId: SandboxId; - readonly source?: "api" | "maintenance" | "runtime"; - readonly status: RuntimeSubjectStatus; }, ): Promise { - const now = currentTimestampMs(); - const conditions: SQL[] = [eq(sandboxesTable.id, input.runtimeSubjectId)]; - - if (input.expectedStatus !== undefined) { - conditions.push(eq(sandboxesTable.status, input.expectedStatus)); - } - - if (input.operationId !== undefined) { - conditions.push(...runtimeSubjectStatusOperationCondition(input.operationId)); - } - const result = await getAppDatabase(database) .update(sandboxesTable) .set({ @@ -737,14 +1027,18 @@ export async function markRuntimeSubjectFailed( claimOwner: null, lastError: input.errorMessage, lastErrorCode: input.errorCode, - ...runtimeSubjectStatusPatch({ - now, - operationId: input.operationId ?? null, - source: input.source ?? "api", - status: input.status, - }), + updatedAt: currentTimestampMs(), }) - .where(and(...conditions)) + .where( + and( + eq(sandboxesTable.id, input.runtimeSubjectId), + eq(sandboxesTable.claimOwner, input.claimOwner), + eq(sandboxesTable.incarnation, input.incarnation), + inArray(sandboxesTable.status, ["active", "cold"]), + isNull(sandboxesTable.operationKind), + isNull(sandboxesTable.statusOperationId), + ), + ) .run(); return getD1ChangeCount(result) > 0; diff --git a/apps/api/src/modules/runtime/infrastructure/runtime-subject-lifecycle/runtime-subject-recycle.service.ts b/apps/api/src/modules/runtime/infrastructure/runtime-subject-lifecycle/runtime-subject-recycle.service.ts index 897028dd..8c72a134 100644 --- a/apps/api/src/modules/runtime/infrastructure/runtime-subject-lifecycle/runtime-subject-recycle.service.ts +++ b/apps/api/src/modules/runtime/infrastructure/runtime-subject-lifecycle/runtime-subject-recycle.service.ts @@ -3,91 +3,16 @@ import { createPlatformId } from "@mosoo/id"; import type { RuntimeOperationId, SandboxId } from "@mosoo/id"; import type { ApiBindings } from "../../../../platform/cloudflare/worker-types"; -import { getRuntimeKindPolicy } from "../../domain/runtime-kind-policy"; -import type { RuntimeSubjectOperationStatus } from "../../domain/runtime-subject-lifecycle.machine"; -import { createSandboxCheckpoints } from "../sandbox-backup.service"; -import { stopRuntimeSubjectDrivers } from "./runtime-subject-driver-stop"; -import { getRuntimeSubjectOperationErrorCode } from "./runtime-subject-errors"; -import { destroyRuntimeSubjectContainer } from "./runtime-subject-platform"; +import { runRuntimeSubjectOperation } from "./runtime-subject-operations.service"; import { - advanceRuntimeSubjectOperationStatus, claimInactiveRuntimeSubject, - closeRuntimeSubjectSessionsForRecycle, - markRuntimeSubjectCold, markRuntimeSubjectOperationStarted, - markRuntimeSubjectOperationRepairNeeded, releaseInactiveRuntimeSubjectClaim, } from "./runtime-subject-store"; +import type { RuntimeSubjectOperationLease } from "./runtime-subject-store"; const RECYCLE_CLAIM_TTL_MS = 10 * 60_000; -function getRuntimeSubjectRecycleErrorMessage(error: unknown): string { - return error instanceof Error ? error.message : "Runtime subject recycle failed."; -} - -async function runRuntimeSubjectRecycleOperation( - bindings: ApiBindings, - input: { - readonly kind: AgentKind; - readonly operationId: RuntimeOperationId; - readonly reason: string; - readonly runtimeSubjectId: SandboxId; - readonly startStatus: RuntimeSubjectOperationStatus; - }, -): Promise { - let destroyStarted = input.startStatus === "destroying"; - - try { - const policy = getRuntimeKindPolicy(input.kind); - - if (input.startStatus === "backing_up") { - await stopRuntimeSubjectDrivers(bindings, { - operationId: input.operationId, - reason: input.reason, - runtimeSubjectId: input.runtimeSubjectId, - }); - await createSandboxCheckpoints(bindings, { - operationId: input.operationId, - rules: policy.checkpoint.createOnHibernate, - sandboxId: input.runtimeSubjectId, - }); - destroyStarted = await advanceRuntimeSubjectOperationStatus(bindings.DB, { - expectedStatus: "backing_up", - operationId: input.operationId, - runtimeSubjectId: input.runtimeSubjectId, - source: "maintenance", - status: "destroying", - }); - if (!destroyStarted) { - throw new Error("Runtime subject changed before recycle destroy."); - } - } - - await destroyRuntimeSubjectContainer(bindings, input.runtimeSubjectId); - await closeRuntimeSubjectSessionsForRecycle(bindings.DB, input.runtimeSubjectId); - const completed = await markRuntimeSubjectCold(bindings.DB, { - clearBackups: false, - expectedStatus: "destroying", - operationId: input.operationId, - runtimeSubjectId: input.runtimeSubjectId, - source: "maintenance", - }); - if (!completed) { - throw new Error("Runtime subject changed before recycle completion."); - } - } catch (error) { - await markRuntimeSubjectOperationRepairNeeded(bindings.DB, { - errorCode: getRuntimeSubjectOperationErrorCode(error), - errorMessage: getRuntimeSubjectRecycleErrorMessage(error), - expectedStatus: destroyStarted ? "destroying" : "backing_up", - operationId: input.operationId, - runtimeSubjectId: input.runtimeSubjectId, - source: "maintenance", - }); - throw error; - } -} - export async function recycleRuntimeSubject( bindings: ApiBindings, input: { @@ -99,16 +24,17 @@ export async function recycleRuntimeSubject( }, ): Promise { const operationId = createPlatformId(); - const started = await markRuntimeSubjectOperationStarted(bindings.DB, { + const lease = await markRuntimeSubjectOperationStarted(bindings.DB, { + claimExpiresAt: input.now + RECYCLE_CLAIM_TTL_MS, claimOwner: input.claimOwner, now: input.now, operationId, + operationKind: "hibernate", runtimeSubjectId: input.runtimeSubjectId, source: "maintenance", - status: "backing_up", }); - if (!started) { + if (lease === null) { await releaseInactiveRuntimeSubjectClaim(bindings.DB, { claimOwner: input.claimOwner, runtimeSubjectId: input.runtimeSubjectId, @@ -116,14 +42,12 @@ export async function recycleRuntimeSubject( return false; } - await runRuntimeSubjectRecycleOperation(bindings, { + await runRuntimeSubjectOperation(bindings, { kind: input.kind, - operationId, + lease, reason: input.reason, runtimeSubjectId: input.runtimeSubjectId, - startStatus: "backing_up", }); - return true; } @@ -131,20 +55,12 @@ export async function resumeRuntimeSubjectRecycleOperation( bindings: ApiBindings, input: { readonly kind: AgentKind; - readonly operationId: RuntimeOperationId; + readonly lease: RuntimeSubjectOperationLease; readonly reason: string; readonly runtimeSubjectId: SandboxId; - readonly status: RuntimeSubjectOperationStatus; }, ): Promise { - await runRuntimeSubjectRecycleOperation(bindings, { - kind: input.kind, - operationId: input.operationId, - reason: input.reason, - runtimeSubjectId: input.runtimeSubjectId, - startStatus: input.status, - }); - + await runRuntimeSubjectOperation(bindings, input); return true; } @@ -166,15 +82,13 @@ export async function recycleInactiveRuntimeSubjectNow( runtimeSubjectId: input.runtimeSubjectId, }); - if (!claimed) { - return false; - } - - return recycleRuntimeSubject(bindings, { - claimOwner, - kind: input.kind, - now, - reason: input.reason, - runtimeSubjectId: input.runtimeSubjectId, - }); + return claimed + ? recycleRuntimeSubject(bindings, { + claimOwner, + kind: input.kind, + now, + reason: input.reason, + runtimeSubjectId: input.runtimeSubjectId, + }) + : false; } diff --git a/apps/api/src/modules/runtime/infrastructure/runtime-subject-lifecycle/runtime-subject-store-queries.ts b/apps/api/src/modules/runtime/infrastructure/runtime-subject-lifecycle/runtime-subject-store-queries.ts index b9dda0e0..14b45922 100644 --- a/apps/api/src/modules/runtime/infrastructure/runtime-subject-lifecycle/runtime-subject-store-queries.ts +++ b/apps/api/src/modules/runtime/infrastructure/runtime-subject-lifecycle/runtime-subject-store-queries.ts @@ -4,6 +4,7 @@ import { sandboxesTable, sandboxSessionsTable, sessionRunsTable, + sessionsTable, } from "@mosoo/db"; import type { SandboxBackupId, SandboxId } from "@mosoo/id"; import { and, eq, inArray, or, sql } from "drizzle-orm"; @@ -31,6 +32,7 @@ const activeRuntimeSubjectRunsTable = alias(sessionRunsTable, "active_runtime_su const liveSubjectDriversTable = alias(driverInstancesTable, "live_runtime_subject_driver"); const runLeaseDriversTable = alias(driverInstancesTable, "runtime_run_lease_driver"); const runLeaseRunsTable = alias(sessionRunsTable, "runtime_run_lease_run"); +const provisioningSessionsTable = alias(sessionsTable, "runtime_provisioning_session"); export const readyConversationBackupTable = alias(sandboxBackupsTable, "ready_conversation_backup"); export const lastBackupTable = alias(sandboxBackupsTable, "last_backup"); export const readyLastBackupTable = alias(sandboxBackupsTable, "ready_last_backup"); @@ -72,7 +74,7 @@ export function activeConversationSessionQuery(appDb: AppDatabase, runtimeSubjec .where( and( eq(activeConversationSessionsTable.sandboxId, runtimeSubjectId), - eq(activeConversationSessionsTable.status, "active"), + inArray(activeConversationSessionsTable.status, ["active", "cleanup_pending"]), ), ); } @@ -84,7 +86,7 @@ export function activeConversationSessionQueryForListedSubject(appDb: AppDatabas .where( and( eq(activeConversationSessionsTable.sandboxId, sandboxesTable.id), - eq(activeConversationSessionsTable.status, "active"), + inArray(activeConversationSessionsTable.status, ["active", "cleanup_pending"]), ), ); } @@ -154,6 +156,20 @@ export function runLeaseQueryForListedSubject(appDb: AppDatabase) { ); } +export function runtimeProvisioningQuery(appDb: AppDatabase, runtimeSubjectId: SandboxId) { + return appDb + .select({ id: provisioningSessionsTable.id }) + .from(provisioningSessionsTable) + .where(eq(provisioningSessionsTable.runtimeProvisioningSandboxId, runtimeSubjectId)); +} + +export function runtimeProvisioningQueryForListedSubject(appDb: AppDatabase) { + return appDb + .select({ id: provisioningSessionsTable.id }) + .from(provisioningSessionsTable) + .where(eq(provisioningSessionsTable.runtimeProvisioningSandboxId, sandboxesTable.id)); +} + export function getRuntimeSubjectInactiveDeadlineSql(now: number) { return sql` CASE ${sandboxesTable.kind} diff --git a/apps/api/src/modules/runtime/infrastructure/runtime-subject-lifecycle/runtime-subject-store.ts b/apps/api/src/modules/runtime/infrastructure/runtime-subject-lifecycle/runtime-subject-store.ts index fc99c5ea..a98f3492 100644 --- a/apps/api/src/modules/runtime/infrastructure/runtime-subject-lifecycle/runtime-subject-store.ts +++ b/apps/api/src/modules/runtime/infrastructure/runtime-subject-lifecycle/runtime-subject-store.ts @@ -1,15 +1,18 @@ export { claimIdleSessionScopedConversationForClose, + claimRuntimeConversationSessionCleanup, ensureRuntimeConversationSessionRecord, getRuntimeConversationSession, getRuntimeConversationSessionState, + listPendingRuntimeConversationSessionCleanups, recordRuntimeConversationSessionActive, recordRuntimeConversationSessionClosed, recordRuntimeConversationSessionError, + retireRuntimeConversationSessionsForIncarnation, } from "./runtime-conversation-session-store"; export { claimInactiveRuntimeSubject, - closeRuntimeSubjectSessionsForRecycle, + claimRuntimeSubjectOperationForRepair, listInactiveRuntimeSubjects, listRuntimeSubjectDriverIds, listRuntimeSubjectSessionStateTargets, @@ -23,13 +26,15 @@ export { getRuntimeSubject, getRuntimeSubjectActivationRecord, getRuntimeSubjectIdByTuple, + markRuntimeSubjectActiveDestroying, markRuntimeSubjectActivationDestroying, markRuntimeSubjectActivationFailed, markRuntimeSubjectActive, markRuntimeSubjectCold, - markRuntimeSubjectFailed, markRuntimeSubjectOperationStarted, markRuntimeSubjectOperationRepairNeeded, + releaseRuntimeSubjectActivationClaim, + renewRuntimeSubjectOperationLease, preemptRuntimeSubjectActivationClaim, markRuntimeSubjectRestoreApplied, markRuntimeSubjectRestoring, @@ -41,6 +46,5 @@ export type { RuntimeSubjectActivationRecord, RuntimeSubjectMaintenanceCandidate, RuntimeSubjectOperationRepairCandidate, - RuntimeSubjectRecord, - RuntimeSubjectStatus, + RuntimeSubjectOperationLease, } from "./runtime-subject-store.types"; diff --git a/apps/api/src/modules/runtime/infrastructure/runtime-subject-lifecycle/runtime-subject-store.types.ts b/apps/api/src/modules/runtime/infrastructure/runtime-subject-lifecycle/runtime-subject-store.types.ts index 6161009d..9325a7b6 100644 --- a/apps/api/src/modules/runtime/infrastructure/runtime-subject-lifecycle/runtime-subject-store.types.ts +++ b/apps/api/src/modules/runtime/infrastructure/runtime-subject-lifecycle/runtime-subject-store.types.ts @@ -2,13 +2,17 @@ import type { AgentKind } from "@mosoo/contracts/agent"; import type { RuntimeSubjectErrorCode, SandboxBackupStatus, + SandboxOperationKind, SandboxSessionStatus, SandboxStatus, SandboxSubjectKind, } from "@mosoo/contracts/sandbox"; import type { + AccountId, AgentId, + AppId, DriverInstanceId, + PlatformId, RuntimeOperationId, SandboxBackupId, SandboxId, @@ -17,27 +21,51 @@ import type { SessionRunId, } from "@mosoo/id"; -import type { RuntimeSubjectOperationStatus } from "../../domain/runtime-subject-lifecycle.machine"; +import type { RuntimeSubjectRecoverableOperationStatus } from "../../domain/runtime-subject-lifecycle.machine"; export type RuntimeSubjectStatus = SandboxStatus; export interface RuntimeSubjectRecord { + readonly agentId: AgentId | null; + readonly appId: AppId | null; readonly id: SandboxId; + readonly incarnation: number; readonly kind: AgentKind; + readonly networkConstraintsHash: string | null; + readonly ownerAccountId: AccountId | null; readonly status: RuntimeSubjectStatus; + readonly subjectId: PlatformId; readonly subjectKind: SandboxSubjectKind; } export interface RuntimeSubjectActivationRecord { + readonly agentId: AgentId | null; + readonly appId: AppId | null; readonly claimExpiresAt: number | null; readonly claimOwner: string | null; readonly id: SandboxId; + readonly incarnation: number; readonly kind: AgentKind; readonly lastError: string | null; readonly lastErrorCode: RuntimeSubjectErrorCode | null; readonly lastBackup: RuntimeSubjectBackupRecord | null; readonly lastReadyBackup: ReadyRuntimeSubjectBackupRecord | null; + readonly networkConstraintsHash: string | null; + readonly ownerAccountId: AccountId | null; + readonly operationId: RuntimeOperationId | null; + readonly operationKind: SandboxOperationKind | null; readonly status: RuntimeSubjectStatus; + readonly subjectId: PlatformId; + readonly subjectKind: SandboxSubjectKind; +} + +export interface RuntimeSubjectOperationLease { + readonly claimExpiresAt: number; + readonly claimOwner: string; + readonly incarnation: number; + readonly kind: SandboxOperationKind; + readonly operationId: RuntimeOperationId; + readonly status: RuntimeSubjectRecoverableOperationStatus; } export interface RuntimeSubjectBackupRecord { @@ -52,6 +80,7 @@ export interface ReadyRuntimeSubjectBackupRecord { } export interface RuntimeConversationSessionRecord { + readonly sandboxIncarnation: number; readonly sandboxSessionId: SandboxSessionId; readonly cwd: string; readonly latestReadyBackup: ReadyRuntimeSubjectBackupRecord | null; @@ -63,26 +92,41 @@ export interface RuntimeConversationSessionRecord { export interface RuntimeConversationSessionState { readonly agentId: AgentId | null; + readonly cleanupOperationId: RuntimeOperationId | null; readonly sandboxSessionId: SandboxSessionId; + readonly sandboxIncarnation: number; readonly kind: AgentKind; readonly status: RuntimeConversationSessionRecord["status"]; } +export interface PendingRuntimeConversationSessionCleanup extends RuntimeConversationSessionState { + readonly cleanupOperationId: RuntimeOperationId; + readonly sandboxId: SandboxId; + readonly sessionId: SessionId; + readonly status: "cleanup_pending"; +} + export interface RuntimeSubjectMaintenanceCandidate { readonly id: SandboxId; readonly kind: AgentKind; } export interface RuntimeSubjectOperationRepairCandidate { + readonly claimExpiresAt: number | null; + readonly claimOwner: string | null; readonly id: SandboxId; + readonly incarnation: number; readonly kind: AgentKind; + readonly operationKind: SandboxOperationKind; readonly operationId: RuntimeOperationId; - readonly status: RuntimeSubjectOperationStatus; + readonly status: RuntimeSubjectRecoverableOperationStatus; } export interface RuntimeRunLeaseInput { + readonly driverGeneration: number; readonly driverInstanceId: DriverInstanceId; readonly runtimeSubjectId: SandboxId; + readonly runtimeSubjectIncarnation: number; readonly sessionId: SessionId; readonly sessionRunId: SessionRunId; } diff --git a/apps/api/src/modules/runtime/infrastructure/sandbox-backup-platform.ts b/apps/api/src/modules/runtime/infrastructure/sandbox-backup-platform.ts index 660df1d7..4e02adc3 100644 --- a/apps/api/src/modules/runtime/infrastructure/sandbox-backup-platform.ts +++ b/apps/api/src/modules/runtime/infrastructure/sandbox-backup-platform.ts @@ -1,8 +1,5 @@ -import { - getSessionResourceRootPath, - getSessionRuntimeStatePath, - getSessionStateRootPath, -} from "@mosoo/agent-driver/paths"; +import { getSessionRuntimeStatePath } from "@mosoo/agent-driver/paths"; +import type { SandboxBackupId } from "@mosoo/id"; import { withDisposedRpcResource, @@ -13,100 +10,124 @@ import { decodeSandboxBackupIdForPlatform, encodeSandboxBackupIdForStorage, } from "./sandbox-backup-id"; -import type { SandboxHandle } from "./sandbox-handles"; +import { + beginSandboxBackupDeletionAttempt, + completeSandboxBackupDeletion, + isSandboxBackupDeletionAuthorized, +} from "./sandbox-backup-store"; +import { toRuntimeSubjectIncarnationHandle } from "./sandbox-handles"; -interface SandboxBackupObject { +const RUNTIME_SANDBOX_BACKUP_NAME_PREFIX = "mosoo:runtime-backup:v1:"; + +export interface SandboxBackupObject { readonly dir: string; - readonly id: string; + readonly id: SandboxBackupId; } -function isMissingRuntimeBucketMountError(error: unknown): boolean { - return error instanceof Error && error.message.includes("No active mount found at path:"); +export interface SandboxBackupMetadata { + readonly dir: string; + readonly id: string; + readonly name: string | null; } -function quoteShellArg(value: string): string { - return `'${value.replaceAll("'", `'"'"'`)}'`; +export function createRuntimeSandboxBackupName(stagingId: SandboxBackupId): string { + return `${RUNTIME_SANDBOX_BACKUP_NAME_PREFIX}${stagingId}`; } -async function prepareRuntimeSessionWorkspaceCheckpoint( - sandbox: SandboxHandle, - input: { - readonly cwd: string; - readonly sessionId: string; - }, -): Promise { - const resourceRoot = getSessionResourceRootPath(input.sessionId); - const stateRoot = getSessionStateRootPath(input.sessionId); - const openAiAuthPath = `${getSessionRuntimeStatePath(input.sessionId, "openai-runtime")}/auth.json`; - - if (!resourceRoot.startsWith(`${input.cwd}/`) || !stateRoot.startsWith(`${input.cwd}/`)) { - throw new Error("Session checkpoint exclusions must stay inside the session workspace."); +export function parseRuntimeSandboxBackupName(value: string | null): SandboxBackupId | null { + if (value === null || !value.startsWith(RUNTIME_SANDBOX_BACKUP_NAME_PREFIX)) { + return null; } - + const stagingId = value.slice(RUNTIME_SANDBOX_BACKUP_NAME_PREFIX.length); try { - await sandbox.unmountBucket(resourceRoot); - } catch (error) { - if (!isMissingRuntimeBucketMountError(error)) { - throw error; - } + return encodeSandboxBackupIdForStorage(stagingId); + } catch { + return null; } +} - const command = [ - "set -eu", - `cwd=${quoteShellArg(input.cwd)}`, - 'test -d "$cwd"', - `resource_root=${quoteShellArg(resourceRoot)}`, - 'if [ -L "$resource_root" ]; then unlink "$resource_root"; elif mountpoint -q "$resource_root"; then fusermount -u "$resource_root"; fi', - 'if [ -e "$resource_root" ]; then rm -rf "$resource_root"; fi', - `state_root=${quoteShellArg(stateRoot)}`, - 'if [ -d "$state_root" ]; then find "$state_root" -type f -name "driver-boot-payload-*.json" -delete; fi', - `rm -f ${quoteShellArg(openAiAuthPath)}`, - "sync", - ].join("; "); +export function parseSandboxBackupMetadata(value: unknown): SandboxBackupMetadata | null { + if (typeof value !== "object" || value === null) { + return null; + } + const dir = Reflect.get(value, "dir"); + const id = Reflect.get(value, "id"); + const name = Reflect.get(value, "name"); + return typeof dir === "string" && + dir.length > 0 && + typeof id === "string" && + (name === null || typeof name === "string") + ? { dir, id, name } + : null; +} - await withDisposedRpcResult(sandbox.exec(`sh -lc ${quoteShellArg(command)}`), (result) => { - if (!result.success || result.exitCode !== 0) { - const detail = result.stderr.trim(); - throw new Error( - `Session workspace could not be prepared for a credential-safe checkpoint${detail ? `: ${detail}` : "."}`, - ); - } - }); +export function getSandboxBackupObjectKeys(backupId: string): readonly [string, string] { + const platformId = decodeSandboxBackupIdForPlatform(backupId); + return [`backups/${platformId}/data.sqsh`, `backups/${platformId}/meta.json`]; } -function getSandboxBackupObjectKeys(backupId: string): string[] { - const platformBackupId = decodeSandboxBackupIdForPlatform(backupId); +async function readSandboxBackupMetadata( + bindings: Pick, + backupId: SandboxBackupId, +): Promise { + const [, metadataKey] = getSandboxBackupObjectKeys(backupId); + const stored = await bindings.SANDBOX_STATE_BUCKET.get(metadataKey); + if (stored === null) { + return null; + } + try { + return parseSandboxBackupMetadata(JSON.parse(await stored.text())); + } catch { + return null; + } +} - return [`backups/${platformBackupId}/data.sqsh`, `backups/${platformBackupId}/meta.json`]; +export async function isRuntimeSandboxBackupObjectReady( + bindings: Pick, + input: { + readonly backupId: SandboxBackupId; + readonly dir: string; + readonly stagingId: SandboxBackupId; + }, +): Promise { + const [dataKey] = getSandboxBackupObjectKeys(input.backupId); + const [data, metadata] = await Promise.all([ + bindings.SANDBOX_STATE_BUCKET.head(dataKey), + readSandboxBackupMetadata(bindings, input.backupId), + ]); + return ( + data !== null && + metadata?.id === decodeSandboxBackupIdForPlatform(input.backupId) && + metadata.dir === input.dir && + parseRuntimeSandboxBackupName(metadata.name) === input.stagingId + ); } export async function createRuntimeSandboxBackup( bindings: ApiBindings, input: { readonly dir: string; + readonly incarnation: number; readonly sandboxId: string; readonly sessionId: string | null; + readonly stagingId: SandboxBackupId; readonly ttlSeconds: number; }, ): Promise { const { getRuntimeSubjectKeepAliveHandle } = await import("./runtime-subject-lifecycle/runtime-subject-lifecycle.service"); - return withDisposedRpcResource( - await getRuntimeSubjectKeepAliveHandle(bindings, input.sandboxId), + await getRuntimeSubjectKeepAliveHandle(bindings, input.sandboxId, input.incarnation), async (sandbox) => { - if (input.sessionId !== null) { - await prepareRuntimeSessionWorkspaceCheckpoint(sandbox, { - cwd: input.dir, - sessionId: input.sessionId, - }); - } else { - await sandbox.mkdir(input.dir, { recursive: true }); - } - + const forbiddenPaths = + input.sessionId === null + ? undefined + : [`${getSessionRuntimeStatePath(input.sessionId, "openai-runtime")}/auth.json`]; return withDisposedRpcResult( - sandbox.createBackup({ + toRuntimeSubjectIncarnationHandle(sandbox).createRuntimeSubjectBackup(input.incarnation, { dir: input.dir, + ...(forbiddenPaths === undefined ? {} : { forbiddenPaths }), + name: createRuntimeSandboxBackupName(input.stagingId), ttl: input.ttlSeconds, }), (result) => ({ @@ -118,15 +139,16 @@ export async function createRuntimeSandboxBackup( ); } -export async function deleteSandboxBackupObjects( - bindings: ApiBindings, - backupIds: readonly string[], +export async function deleteAuthorizedSandboxBackupObjects( + bindings: Pick, + backupIds: readonly SandboxBackupId[], ): Promise { - if (backupIds.length === 0) { - return; + for (const backupId of new Set(backupIds)) { + if (!(await isSandboxBackupDeletionAuthorized(bindings.DB, backupId))) { + throw new Error("Sandbox backup object deletion lacks a durable D1 intent."); + } + await beginSandboxBackupDeletionAttempt(bindings.DB, backupId); + await bindings.SANDBOX_STATE_BUCKET.delete([...getSandboxBackupObjectKeys(backupId)]); + await completeSandboxBackupDeletion(bindings.DB, backupId); } - - const objectKeys = backupIds.flatMap((backupId) => getSandboxBackupObjectKeys(backupId)); - - await bindings.SANDBOX_STATE_BUCKET.delete(objectKeys); } diff --git a/apps/api/src/modules/runtime/infrastructure/sandbox-backup-pruning.ts b/apps/api/src/modules/runtime/infrastructure/sandbox-backup-pruning.ts index de37061a..dca2ec2f 100644 --- a/apps/api/src/modules/runtime/infrastructure/sandbox-backup-pruning.ts +++ b/apps/api/src/modules/runtime/infrastructure/sandbox-backup-pruning.ts @@ -1,13 +1,15 @@ +import type { SandboxBackupId } from "@mosoo/id"; + import type { ReadySandboxBackupForPruning } from "./sandbox-backup-store"; export function selectSandboxBackupPruneIds( backups: readonly ReadySandboxBackupForPruning[], -): string[] { +): SandboxBackupId[] { const keepIds = new Set(); const readyCountsByDir = new Map(); for (const backup of backups) { - if (backup.keep) { + if (backup.keep || backup.protected) { keepIds.add(backup.id); continue; } diff --git a/apps/api/src/modules/runtime/infrastructure/sandbox-backup-reconciliation.service.ts b/apps/api/src/modules/runtime/infrastructure/sandbox-backup-reconciliation.service.ts new file mode 100644 index 00000000..b4cb5b39 --- /dev/null +++ b/apps/api/src/modules/runtime/infrastructure/sandbox-backup-reconciliation.service.ts @@ -0,0 +1,520 @@ +import type { ApiCommandId } from "@mosoo/db"; +import type { SandboxBackupId } from "@mosoo/id"; +import { parsePlatformId } from "@mosoo/id"; + +import { createErrorLogContext, logWarn } from "../../../platform/cloudflare/logger"; +import type { ApiBindings } from "../../../platform/cloudflare/worker-types"; +import { + environmentPackageArtifactMetadataBackupId, + publishEnvironmentPackageArtifactBackup, + resolveEnvironmentPackageArtifactBackup, +} from "../../environments/application/environment-package-artifact-backup"; +import { + claimEnvironmentPackageArtifactBackupActual, + clearMissingEnvironmentPackageArtifactBackupActual, + completeEnvironmentPackageArtifactBackupStage, + getEnvironmentPackageArtifactBackupStage, + getEnvironmentPackageArtifactCommandIntent, + retireExpiredEnvironmentPackageArtifactBackups, + revokeTerminalEnvironmentPackageArtifactBackupStage, + revokeTerminalEnvironmentPackageArtifactBackupStages, +} from "../../environments/application/environment-package-artifact-backup-store"; +import { + environmentPackageArtifactDir, + parseEnvironmentPackageArtifactBackupName, +} from "../../environments/domain/environment-package-artifact"; +import type { + EnvironmentPackageArtifactBackupAuthorityRef, + EnvironmentPackageArtifactKey, + EnvironmentPackageArtifactPaths, +} from "../../environments/domain/environment-package-artifact"; +import { + decodeSandboxBackupIdForPlatform, + encodeSandboxBackupIdForStorage, +} from "./sandbox-backup-id"; +import { + deleteAuthorizedSandboxBackupObjects, + getSandboxBackupObjectKeys, + parseRuntimeSandboxBackupName, + parseSandboxBackupMetadata, +} from "./sandbox-backup-platform"; +import type { SandboxBackupMetadata } from "./sandbox-backup-platform"; +import { + authorizeSandboxBackupDeletion, + claimSandboxBackupStageActual, + finalizeSandboxBackupStage, + getSandboxBackupRecord, + getSandboxBackupRecordByStagingId, + getSandboxBackupStage, + listPendingSandboxBackupDeletions, +} from "./sandbox-backup-store"; +import type { SandboxBackupDeletionAuthority } from "./sandbox-backup-store"; + +const RECONCILIATION_PAGE_SIZE = 64; +const ORPHAN_GRACE_MS = 24 * 60 * 60_000; +const BACKUP_OBJECT_KEY = /^backups\/([^/]+)\/(data\.sqsh|meta\.json)$/u; + +export interface SandboxBackupReconciliationPageResult { + readonly hasMore: boolean; + readonly nextCursor: string | null; + readonly processed: number; +} + +function parseBackupObjectKey(key: string): { + readonly backupId: SandboxBackupId; + readonly kind: "data" | "meta"; +} | null { + const match = BACKUP_OBJECT_KEY.exec(key); + if (match === null) { + return null; + } + try { + return { + backupId: encodeSandboxBackupIdForStorage(match[1] ?? ""), + kind: match[2] === "meta.json" ? "meta" : "data", + }; + } catch { + return null; + } +} + +function isPastGrace(uploaded: Date, nowMs: number): boolean { + return uploaded.getTime() <= nowMs - ORPHAN_GRACE_MS; +} + +async function deleteSandboxBackup( + bindings: ApiBindings, + backupId: SandboxBackupId, + authority: SandboxBackupDeletionAuthority, +): Promise { + if (!(await authorizeSandboxBackupDeletion(bindings.DB, { authority, backupId }))) { + return false; + } + await deleteAuthorizedSandboxBackupObjects(bindings, [backupId]); + return true; +} + +function environmentArtifactPathsEqual( + left: EnvironmentPackageArtifactPaths, + right: EnvironmentPackageArtifactPaths, +): boolean { + return JSON.stringify(left) === JSON.stringify(right); +} + +async function readEnvironmentArtifactBackup( + bindings: ApiBindings, + key: EnvironmentPackageArtifactKey, +): Promise<{ + readonly backupId: SandboxBackupId; + readonly paths: EnvironmentPackageArtifactPaths; +} | null> { + const metadata = await resolveEnvironmentPackageArtifactBackup(bindings, key); + if (metadata === null) { + return null; + } + const backupId = environmentPackageArtifactMetadataBackupId(metadata); + if (backupId === null) { + throw new Error("Environment package artifact metadata has an invalid backup ID."); + } + return { backupId, paths: metadata.paths }; +} + +async function deleteEnvironmentArtifactBackupIfUnreferenced( + bindings: ApiBindings, + key: EnvironmentPackageArtifactKey, + backupId: SandboxBackupId, + authority: { + readonly attemptCount: number; + readonly commandId: ApiCommandId; + readonly deliveryGeneration: number; + readonly invalid?: boolean; + }, +): Promise { + if ((await readEnvironmentArtifactBackup(bindings, key))?.backupId !== backupId) { + await deleteSandboxBackup(bindings, backupId, { + attemptCount: authority.attemptCount, + commandId: authority.commandId, + deliveryGeneration: authority.deliveryGeneration, + kind: authority.invalid === true ? "environment_invalid" : "environment_candidate", + }); + } +} + +function parseEnvironmentArtifactCommandId( + authority: EnvironmentPackageArtifactBackupAuthorityRef, +): ApiCommandId | null { + try { + return parsePlatformId(authority.commandId, "environment artifact command ID"); + } catch { + return null; + } +} + +async function reconcileEnvironmentArtifactMetadataObject( + bindings: ApiBindings, + input: { + readonly authority: EnvironmentPackageArtifactBackupAuthorityRef; + readonly backupId: SandboxBackupId; + readonly dataExists: boolean; + readonly metadata: SandboxBackupMetadata; + readonly nowMs: number; + readonly uploaded: Date; + }, +): Promise { + const commandId = parseEnvironmentArtifactCommandId(input.authority); + if (commandId === null) { + return; + } + const [stage, intent] = await Promise.all([ + getEnvironmentPackageArtifactBackupStage(bindings.DB, commandId), + getEnvironmentPackageArtifactCommandIntent(bindings.DB, commandId), + ]); + const keySource = stage ?? intent; + if (keySource === null) { + return; + } + const key: EnvironmentPackageArtifactKey = { + appId: keySource.appId, + inputDigest: keySource.inputDigest, + }; + const current = await readEnvironmentArtifactBackup(bindings, key); + if (current !== null) { + if (stage !== null) { + if (!environmentArtifactPathsEqual(stage.paths, current.paths)) { + logWarn("runtime.environment_artifact.manifest_paths_mismatch", { + backupId: input.backupId, + commandId, + }); + return; + } + await completeEnvironmentPackageArtifactBackupStage(bindings.DB, { + actualBackupId: stage.actualBackupId, + attemptCount: stage.attemptCount, + claimOwner: stage.claimOwner, + commandId: stage.commandId, + deliveryGeneration: stage.deliveryGeneration, + }); + } + if (current.backupId !== input.backupId && isPastGrace(input.uploaded, input.nowMs)) { + await deleteEnvironmentArtifactBackupIfUnreferenced(bindings, key, input.backupId, { + attemptCount: input.authority.attemptCount, + commandId, + deliveryGeneration: input.authority.deliveryGeneration, + }); + } + return; + } + + const stageMatchesObjectAuthority = + stage !== null && + stage.attemptCount === input.authority.attemptCount && + stage.commandId === commandId && + stage.deliveryGeneration === input.authority.deliveryGeneration; + const objectMatchesStage = + stageMatchesObjectAuthority && + input.dataExists && + input.metadata.id === decodeSandboxBackupIdForPlatform(input.backupId) && + input.metadata.dir === stage.dir && + stage.dir === environmentPackageArtifactDir(key); + + if (!objectMatchesStage) { + if (stageMatchesObjectAuthority && stage.actualBackupId === input.backupId) { + await clearMissingEnvironmentPackageArtifactBackupActual(bindings.DB, { + actualBackupId: input.backupId, + attemptCount: stage.attemptCount, + claimOwner: stage.claimOwner, + commandId: stage.commandId, + deliveryGeneration: stage.deliveryGeneration, + }); + } + if (stageMatchesObjectAuthority) { + await revokeTerminalEnvironmentPackageArtifactBackupStage(bindings.DB, commandId); + } + if (isPastGrace(input.uploaded, input.nowMs)) { + await deleteEnvironmentArtifactBackupIfUnreferenced(bindings, key, input.backupId, { + attemptCount: input.authority.attemptCount, + commandId, + deliveryGeneration: input.authority.deliveryGeneration, + invalid: true, + }); + } + return; + } + + const claimed = await claimEnvironmentPackageArtifactBackupActual(bindings.DB, { + actualBackupId: input.backupId, + authority: { + attemptCount: stage.attemptCount, + claimOwner: stage.claimOwner, + deliveryGeneration: stage.deliveryGeneration, + }, + commandId, + dir: stage.dir, + }); + if (claimed?.actualBackupId === input.backupId) { + await publishEnvironmentPackageArtifactBackup(bindings, { + attemptCount: stage.attemptCount, + backupId: input.backupId, + claimOwner: stage.claimOwner, + commandId, + deliveryGeneration: stage.deliveryGeneration, + key, + paths: stage.paths, + }); + const committed = await readEnvironmentArtifactBackup(bindings, key); + if (committed !== null && environmentArtifactPathsEqual(stage.paths, committed.paths)) { + await completeEnvironmentPackageArtifactBackupStage(bindings.DB, { + actualBackupId: stage.actualBackupId ?? input.backupId, + attemptCount: stage.attemptCount, + claimOwner: stage.claimOwner, + commandId: stage.commandId, + deliveryGeneration: stage.deliveryGeneration, + }); + if (committed.backupId === input.backupId) { + return; + } + } + } else if (claimed === null) { + await revokeTerminalEnvironmentPackageArtifactBackupStage(bindings.DB, commandId); + } + if (isPastGrace(input.uploaded, input.nowMs)) { + await deleteEnvironmentArtifactBackupIfUnreferenced(bindings, key, input.backupId, { + attemptCount: input.authority.attemptCount, + commandId, + deliveryGeneration: input.authority.deliveryGeneration, + }); + } +} + +async function reconcileMetadataObject( + bindings: ApiBindings, + input: { readonly backupId: SandboxBackupId; readonly nowMs: number; readonly uploaded: Date }, +): Promise { + const publicRecord = await getSandboxBackupRecord(bindings.DB, input.backupId); + if (publicRecord?.status === "pruned") { + await deleteSandboxBackup(bindings, input.backupId, { kind: "pruned" }); + return; + } + + const [, metadataKey] = getSandboxBackupObjectKeys(input.backupId); + const stored = await bindings.SANDBOX_STATE_BUCKET.get(metadataKey); + if (stored === null) { + return; + } + let metadata = null; + try { + metadata = parseSandboxBackupMetadata(JSON.parse(await stored.text())); + } catch { + // Unknown or legacy metadata is never owned by this reconciler. + } + if (metadata === null) { + if (publicRecord?.status === "ready") { + logWarn("runtime.sandbox_backup.ready_metadata_invalid", { backupId: input.backupId }); + } + return; + } + const [dataKey] = getSandboxBackupObjectKeys(input.backupId); + const data = await bindings.SANDBOX_STATE_BUCKET.head(dataKey); + if (publicRecord?.status === "ready") { + if (data === null) { + logWarn("runtime.sandbox_backup.ready_data_missing", { backupId: input.backupId }); + } + return; + } + + const environmentAuthority = parseEnvironmentPackageArtifactBackupName(metadata.name); + if (environmentAuthority !== null) { + await reconcileEnvironmentArtifactMetadataObject(bindings, { + authority: environmentAuthority, + backupId: input.backupId, + dataExists: data !== null, + metadata, + nowMs: input.nowMs, + uploaded: input.uploaded, + }); + return; + } + + const stagingId = parseRuntimeSandboxBackupName(metadata.name); + if (stagingId === null) { + return; + } + const finalized = await getSandboxBackupRecordByStagingId(bindings.DB, stagingId); + if (finalized !== null) { + if (finalized.id !== input.backupId) { + await deleteSandboxBackup(bindings, input.backupId, { + kind: "runtime_candidate", + stagingId, + }); + } else if (finalized.status === "pruned") { + await deleteSandboxBackup(bindings, input.backupId, { kind: "pruned" }); + } + return; + } + + const stage = await getSandboxBackupStage(bindings.DB, stagingId); + if (data === null) { + if (isPastGrace(input.uploaded, input.nowMs)) { + await deleteSandboxBackup(bindings, input.backupId, { + kind: "runtime_invalid", + stagingId, + }); + } + return; + } + if (metadata.id !== decodeSandboxBackupIdForPlatform(input.backupId)) { + if (isPastGrace(input.uploaded, input.nowMs)) { + await deleteSandboxBackup(bindings, input.backupId, { + kind: "runtime_invalid", + stagingId, + }); + } + return; + } + if (stage === null) { + if (isPastGrace(input.uploaded, input.nowMs)) { + await deleteSandboxBackup(bindings, input.backupId, { + kind: "runtime_candidate", + stagingId, + }); + } + return; + } + if (stage.dir !== metadata.dir) { + if (isPastGrace(input.uploaded, input.nowMs)) { + await deleteSandboxBackup(bindings, input.backupId, { + kind: "runtime_invalid", + stagingId, + }); + } + return; + } + + const claimed = await claimSandboxBackupStageActual(bindings.DB, { + actualBackupId: input.backupId, + dir: metadata.dir, + sandboxIncarnation: stage.sandboxIncarnation, + stagingId, + }); + if (claimed === null) { + await deleteSandboxBackup(bindings, input.backupId, { + kind: "runtime_candidate", + stagingId, + }); + return; + } + if (claimed.actualBackupId !== input.backupId) { + await deleteSandboxBackup(bindings, input.backupId, { + kind: "runtime_candidate", + stagingId, + }); + return; + } + const result = await finalizeSandboxBackupStage(bindings.DB, { + actualBackupId: input.backupId, + stagingId, + }); + if (result === null || !result.candidateAccepted) { + await deleteSandboxBackup(bindings, input.backupId, { + kind: "runtime_candidate", + stagingId, + }); + } +} + +async function reconcileDataObject( + bindings: ApiBindings, + input: { readonly backupId: SandboxBackupId; readonly nowMs: number; readonly uploaded: Date }, +): Promise { + const publicRecord = await getSandboxBackupRecord(bindings.DB, input.backupId); + if (publicRecord?.status === "pruned") { + await deleteSandboxBackup(bindings, input.backupId, { kind: "pruned" }); + return; + } + const [, metadataKey] = getSandboxBackupObjectKeys(input.backupId); + if ((await bindings.SANDBOX_STATE_BUCKET.head(metadataKey)) !== null) { + return; + } + if (publicRecord?.status === "ready") { + logWarn("runtime.sandbox_backup.ready_metadata_missing", { backupId: input.backupId }); + return; + } + if (!isPastGrace(input.uploaded, input.nowMs)) { + return; + } + await deleteSandboxBackup(bindings, input.backupId, { kind: "unattributed" }); +} + +export async function reconcileSandboxBackupPage( + bindings: ApiBindings, + input: { readonly cursor: string | null }, +): Promise { + const clock = await bindings.DB.prepare( + "SELECT CAST(unixepoch('subsec') * 1000 AS INTEGER) AS now_ms", + ).first<{ now_ms: number }>(); + if (clock === null) { + throw new Error("Sandbox backup reconciliation could not read the D1 clock."); + } + const revokedStages = await revokeTerminalEnvironmentPackageArtifactBackupStages(bindings.DB); + const retired = await retireExpiredEnvironmentPackageArtifactBackups( + bindings.DB, + RECONCILIATION_PAGE_SIZE, + ); + const pending = await listPendingSandboxBackupDeletions(bindings.DB, RECONCILIATION_PAGE_SIZE); + for (const backupId of pending) { + try { + await deleteAuthorizedSandboxBackupObjects(bindings, [backupId]); + } catch (error) { + logWarn("runtime.sandbox_backup.pending_deletion_failed", { + ...createErrorLogContext(error), + backupId, + }); + } + } + const pendingAfter = + pending.length === RECONCILIATION_PAGE_SIZE + ? await listPendingSandboxBackupDeletions(bindings.DB, RECONCILIATION_PAGE_SIZE) + : []; + const databaseHasMore = + revokedStages === RECONCILIATION_PAGE_SIZE || + retired.length === RECONCILIATION_PAGE_SIZE || + (pendingAfter.length > 0 && + (pendingAfter.length !== pending.length || + pendingAfter.some((backupId, index) => backupId !== pending[index]))); + const page = await bindings.SANDBOX_STATE_BUCKET.list({ + ...(input.cursor === null ? {} : { cursor: input.cursor }), + limit: RECONCILIATION_PAGE_SIZE, + prefix: "backups/", + }); + for (const object of page.objects) { + const parsed = parseBackupObjectKey(object.key); + if (parsed === null) { + continue; + } + try { + if (parsed.kind === "meta") { + await reconcileMetadataObject(bindings, { + backupId: parsed.backupId, + nowMs: clock.now_ms, + uploaded: object.uploaded, + }); + } else { + await reconcileDataObject(bindings, { + backupId: parsed.backupId, + nowMs: clock.now_ms, + uploaded: object.uploaded, + }); + } + } catch (error) { + logWarn("runtime.sandbox_backup.reconciliation_object_failed", { + ...createErrorLogContext(error), + key: object.key, + }); + } + } + return { + hasMore: page.truncated || databaseHasMore, + nextCursor: page.truncated ? page.cursor : null, + processed: page.objects.length, + }; +} diff --git a/apps/api/src/modules/runtime/infrastructure/sandbox-backup-store.ts b/apps/api/src/modules/runtime/infrastructure/sandbox-backup-store.ts index cbce1d7c..739271fc 100644 --- a/apps/api/src/modules/runtime/infrastructure/sandbox-backup-store.ts +++ b/apps/api/src/modules/runtime/infrastructure/sandbox-backup-store.ts @@ -1,311 +1,1263 @@ import type { SessionStatus } from "@mosoo/contracts/session"; +import type { ApiCommandId } from "@mosoo/db"; import { sandboxBackupsTable, + sandboxBackupStagingTable, sandboxSessionsTable, - sandboxesTable, - nativeResumeRefsTable, sessionsTable, } from "@mosoo/db"; -import { parsePlatformId } from "@mosoo/id"; +import { createPlatformId, parsePlatformId } from "@mosoo/id"; import type { + DriverInstanceId, RuntimeOperationId, SandboxBackupId, SandboxId, SessionId, SessionRunId, } from "@mosoo/id"; -import { and, asc, desc, eq, inArray, isNull, sql } from "drizzle-orm"; +import { and, asc, eq, inArray, isNull } from "drizzle-orm"; -import type { AppDatabase } from "../../../platform/db/drizzle"; -import { - getAppDatabase, - getD1ChangeCount, - runAppDatabaseBatch, -} from "../../../platform/db/drizzle"; -import { currentTimestampMs } from "../../../time"; +import { getAppDatabase } from "../../../platform/db/drizzle"; +import type { RuntimeSubjectOperationLease } from "./runtime-subject-lifecycle/runtime-subject-store"; -// Each row binds ten values; nine rows stay below D1's per-statement variable limit. -const SANDBOX_BACKUP_INSERT_BATCH_SIZE = 9; -type AppDatabaseBatchItem = Parameters[0][number]; +const TERMINAL_BACKUP_DRIVER_STATUSES_SQL = + "('provisioning', 'connecting', 'ready', 'stopping', 'stopped', 'failed')"; +const DATABASE_NOW_MS_SQL = "CAST(unixepoch('subsec') * 1000 AS INTEGER)"; -export interface CreatedSandboxBackupRecord { +export interface SandboxBackupRecord { + readonly createdAt: number; readonly dir: string; - readonly id: string; + readonly id: SandboxBackupId; + readonly keep: boolean; + readonly operationId: RuntimeOperationId | null; + readonly sandboxId: SandboxId; + readonly sandboxIncarnation: number; + readonly sessionRunId: SessionRunId | null; + readonly stagingId: SandboxBackupId; + readonly status: "pruned" | "ready"; + readonly ttlSeconds: number; + readonly updatedAt: number; + readonly workspaceSessionId: SessionId | null; +} + +export interface SandboxBackupStage { + readonly actualBackupId: SandboxBackupId | null; + readonly claimOwner: string | null; + readonly createdAt: number; + readonly dir: string; + readonly driverGeneration: number | null; + readonly driverInstanceId: DriverInstanceId | null; + readonly id: SandboxBackupId; + readonly operationId: RuntimeOperationId | null; + readonly sandboxId: SandboxId; + readonly sandboxIncarnation: number; + readonly sessionRunId: SessionRunId | null; + readonly ttlSeconds: number; + readonly updatedAt: number; + readonly updatesSubjectBackup: boolean; + readonly workspaceSessionId: SessionId | null; } -export interface CreatedSandboxBackupWrite { - readonly backup: CreatedSandboxBackupRecord; +export interface SandboxBackupTarget { + readonly dir: string; readonly updateSandboxLastBackup: boolean; + readonly workspaceSessionId: string | null; } -export interface ReadySandboxBackupForPruning { +export type SandboxBackupAdmission = + | { readonly kind: "operation"; readonly lease: RuntimeSubjectOperationLease } + | { + readonly driverGeneration: number; + readonly driverInstanceId: DriverInstanceId; + readonly incarnation: number; + readonly kind: "terminal"; + readonly sessionId: SessionId; + readonly sessionRunId: SessionRunId; + }; + +export type SandboxBackupWrite = + | { readonly backup: SandboxBackupRecord; readonly kind: "finalized" } + | { readonly isNew: boolean; readonly kind: "staged"; readonly stage: SandboxBackupStage }; + +interface SandboxBackupScope { readonly dir: string; - readonly id: SandboxBackupId; - readonly keep: boolean; + readonly operationId: RuntimeOperationId | null; + readonly sandboxId: SandboxId; + readonly sandboxIncarnation: number; + readonly sessionRunId: SessionRunId | null; } -export async function listReadySandboxBackupsForSessionRun( +function assertPositiveSafeInteger(value: number, label: string): void { + if (!Number.isSafeInteger(value) || value <= 0) { + throw new TypeError(`${label} must be a positive safe integer.`); + } +} + +function scopeForTarget( + sandboxId: SandboxId, + admission: SandboxBackupAdmission, + dir: string, +): SandboxBackupScope { + return { + dir, + operationId: admission.kind === "operation" ? admission.lease.operationId : null, + sandboxId, + sandboxIncarnation: + admission.kind === "operation" ? admission.lease.incarnation : admission.incarnation, + sessionRunId: admission.kind === "terminal" ? admission.sessionRunId : null, + }; +} + +function mapBackup(row: typeof sandboxBackupsTable.$inferSelect): SandboxBackupRecord { + if (row.status !== "ready" && row.status !== "pruned") { + throw new Error("Sandbox backup storage returned an invalid public status."); + } + return { ...row, status: row.status }; +} + +export async function getSandboxBackupStage( + database: D1Database, + stagingId: SandboxBackupId, +): Promise { + return ( + (await getAppDatabase(database) + .select() + .from(sandboxBackupStagingTable) + .where(eq(sandboxBackupStagingTable.id, stagingId)) + .limit(1) + .get()) ?? null + ); +} + +export async function getSandboxBackupRecord( + database: D1Database, + backupId: SandboxBackupId, +): Promise { + const row = await getAppDatabase(database) + .select() + .from(sandboxBackupsTable) + .where(eq(sandboxBackupsTable.id, backupId)) + .limit(1) + .get(); + return row === undefined ? null : mapBackup(row); +} + +export async function getSandboxBackupRecordByStagingId( + database: D1Database, + stagingId: SandboxBackupId, +): Promise { + const row = await getAppDatabase(database) + .select() + .from(sandboxBackupsTable) + .where(eq(sandboxBackupsTable.stagingId, stagingId)) + .limit(1) + .get(); + return row === undefined ? null : mapBackup(row); +} + +export type SandboxBackupDeletionAuthority = + | { readonly kind: "pruned" } + | { readonly kind: "unattributed" } + | { + readonly kind: "runtime_candidate" | "runtime_invalid"; + readonly stagingId: SandboxBackupId; + } + | { + readonly attemptCount: number; + readonly commandId: ApiCommandId; + readonly deliveryGeneration: number; + readonly kind: "environment_candidate" | "environment_invalid"; + }; + +export async function authorizeSandboxBackupDeletion( database: D1Database, input: { - readonly sandboxId: string; - readonly sessionRunId: string; + readonly authority: SandboxBackupDeletionAuthority; + readonly backupId: SandboxBackupId; }, -): Promise { - const sandboxId = parsePlatformId(input.sandboxId, "sandbox id"); - const sessionRunId = parsePlatformId(input.sessionRunId, "session run id"); - const rows = await getAppDatabase(database) - .select({ - dir: sandboxBackupsTable.dir, - id: sandboxBackupsTable.id, - }) - .from(sandboxBackupsTable) - .where( - and( - eq(sandboxBackupsTable.sandboxId, sandboxId), - eq(sandboxBackupsTable.sessionRunId, sessionRunId), - eq(sandboxBackupsTable.status, "ready"), - ), +): Promise { + const { authority, backupId } = input; + const invalidRuntime = authority.kind === "runtime_invalid"; + const invalidEnvironment = authority.kind === "environment_invalid"; + const preparations: D1PreparedStatement[] = []; + if (invalidRuntime) { + preparations.push( + database + .prepare( + `UPDATE sandbox_backup_staging AS stage + SET actual_backup_id = NULL, updated_at = ${DATABASE_NOW_MS_SQL} + WHERE stage.id = ? AND stage.actual_backup_id = ? + AND NOT EXISTS ( + SELECT 1 FROM sandbox_backup AS backup + WHERE backup.id = ? AND backup.status = 'ready' + ) + AND NOT EXISTS ( + SELECT 1 FROM environment_package_artifact_backup AS artifact + WHERE artifact.backup_id = ? + )`, + ) + .bind(authority.stagingId, backupId, backupId, backupId), + ); + } else if (invalidEnvironment) { + preparations.push( + database + .prepare( + `UPDATE environment_package_artifact_backup_staging AS stage + SET actual_backup_id = NULL, updated_at = ${DATABASE_NOW_MS_SQL} + WHERE stage.command_id = ? AND stage.delivery_generation = ? + AND stage.attempt_count = ? AND stage.actual_backup_id = ? + AND NOT EXISTS ( + SELECT 1 FROM environment_package_artifact_backup AS artifact + WHERE artifact.backup_id = ? + )`, + ) + .bind( + authority.commandId, + authority.deliveryGeneration, + authority.attemptCount, + backupId, + backupId, + ), + ); + } + + const requiresPruned = + authority.kind === "pruned" + ? `AND EXISTS ( + SELECT 1 FROM sandbox_backup AS pruned + WHERE pruned.id = ? AND pruned.status = 'pruned' + )` + : ""; + const blocksUnclaimedRuntime = + authority.kind === "runtime_candidate" + ? `AND NOT EXISTS ( + SELECT 1 FROM sandbox_backup_staging AS candidate_stage + WHERE candidate_stage.id = ? AND candidate_stage.actual_backup_id IS NULL + )` + : ""; + const blocksUnclaimedEnvironment = + authority.kind === "environment_candidate" + ? `AND NOT EXISTS ( + SELECT 1 FROM environment_package_artifact_backup_staging AS candidate_stage + WHERE candidate_stage.command_id = ? AND candidate_stage.delivery_generation = ? + AND candidate_stage.attempt_count = ? + AND candidate_stage.actual_backup_id IS NULL + )` + : ""; + const authorityBindings = + authority.kind === "pruned" + ? [backupId] + : authority.kind === "runtime_candidate" + ? [authority.stagingId] + : authority.kind === "environment_candidate" + ? [authority.commandId, authority.deliveryGeneration, authority.attemptCount] + : []; + const authorize = database + .prepare( + `INSERT INTO sandbox_backup_delete_intent (backup_id, created_at, delete_after, deleted_at) + SELECT ?, ${DATABASE_NOW_MS_SQL}, ${DATABASE_NOW_MS_SQL}, NULL + WHERE NOT EXISTS ( + SELECT 1 FROM sandbox_backup_delete_intent AS deletion + WHERE deletion.backup_id = ? + ) + AND NOT EXISTS ( + SELECT 1 FROM sandbox_backup AS backup + WHERE backup.id = ? AND backup.status = 'ready' + ) + AND NOT EXISTS ( + SELECT 1 FROM sandbox_backup_staging AS stage + WHERE stage.actual_backup_id = ? + ) + AND NOT EXISTS ( + SELECT 1 FROM environment_package_artifact_backup_staging AS stage + WHERE stage.actual_backup_id = ? + ) + AND NOT EXISTS ( + SELECT 1 FROM environment_package_artifact_backup AS artifact + WHERE artifact.backup_id = ? + ) + ${requiresPruned} + ${blocksUnclaimedRuntime} + ${blocksUnclaimedEnvironment} + `, ) - .all(); + .bind(backupId, backupId, backupId, backupId, backupId, backupId, ...authorityBindings); + if (preparations.length === 0) { + await authorize.run(); + } else { + await database.batch([...preparations, authorize]); + } + return ( + (await database + .prepare( + `SELECT 1 FROM sandbox_backup_delete_intent + WHERE backup_id = ? AND delete_after <= ${DATABASE_NOW_MS_SQL}`, + ) + .bind(backupId) + .first()) !== null + ); +} - return rows; +export async function listPendingSandboxBackupDeletions( + database: D1Database, + limit: number, +): Promise { + assertPositiveSafeInteger(limit, "Sandbox backup deletion limit"); + const rows = await database + .prepare( + `SELECT backup_id + FROM sandbox_backup_delete_intent + WHERE deleted_at IS NULL AND delete_after <= ${DATABASE_NOW_MS_SQL} + ORDER BY coalesce(attempted_at, delete_after), attempted_at IS NOT NULL, + created_at, backup_id + LIMIT ?`, + ) + .bind(limit) + .all<{ backup_id: SandboxBackupId }>(); + return rows.results.map((row) => row.backup_id); } -export interface SandboxSessionBackupCandidate { - readonly cwd: string; - readonly lastMessageAt: number | null; - readonly sessionId: SessionId; - readonly sessionStatus: SessionStatus; +export async function beginSandboxBackupDeletionAttempt( + database: D1Database, + backupId: SandboxBackupId, +): Promise { + const result = await database + .prepare( + `UPDATE sandbox_backup_delete_intent + SET attempted_at = ${DATABASE_NOW_MS_SQL} + WHERE backup_id = ? AND deleted_at IS NULL AND delete_after <= ${DATABASE_NOW_MS_SQL}`, + ) + .bind(backupId) + .run(); + return (result.meta.changes ?? 0) === 1; } -function parseSandboxBackupIds(values: readonly string[], label: string): SandboxBackupId[] { - return values.map((value, index) => - parsePlatformId(value, `${label}[${index}]`), +export async function isSandboxBackupDeletionAuthorized( + database: D1Database, + backupId: SandboxBackupId, +): Promise { + return ( + (await database + .prepare( + `SELECT 1 FROM sandbox_backup_delete_intent + WHERE backup_id = ? AND delete_after <= ${DATABASE_NOW_MS_SQL}`, + ) + .bind(backupId) + .first()) !== null ); } -function sandboxStatusOperationCondition(operationId: RuntimeOperationId | null | undefined) { - if (operationId === undefined) { - return []; - } - - return operationId === null - ? [isNull(sandboxesTable.statusOperationId)] - : [eq(sandboxesTable.statusOperationId, operationId)]; +export async function completeSandboxBackupDeletion( + database: D1Database, + backupId: SandboxBackupId, +): Promise { + const result = await database + .prepare( + `UPDATE sandbox_backup_delete_intent + SET deleted_at = coalesce(deleted_at, ${DATABASE_NOW_MS_SQL}) + WHERE backup_id = ? AND attempted_at IS NOT NULL`, + ) + .bind(backupId) + .run(); + return (result.meta.changes ?? 0) === 1; } -export async function listReadySandboxBackupsForPruning( +async function getStageByScope( database: D1Database, - sandboxId: string, -): Promise { - const parsedSandboxId = parsePlatformId(sandboxId, "sandbox id"); + scope: SandboxBackupScope, +): Promise { + return ( + (await getAppDatabase(database) + .select() + .from(sandboxBackupStagingTable) + .where( + and( + eq(sandboxBackupStagingTable.sandboxId, scope.sandboxId), + eq(sandboxBackupStagingTable.sandboxIncarnation, scope.sandboxIncarnation), + eq(sandboxBackupStagingTable.dir, scope.dir), + scope.operationId === null + ? isNull(sandboxBackupStagingTable.operationId) + : eq(sandboxBackupStagingTable.operationId, scope.operationId), + scope.sessionRunId === null + ? isNull(sandboxBackupStagingTable.sessionRunId) + : eq(sandboxBackupStagingTable.sessionRunId, scope.sessionRunId), + ), + ) + .limit(1) + .get()) ?? null + ); +} - return getAppDatabase(database) - .select({ - dir: sandboxBackupsTable.dir, - id: sandboxBackupsTable.id, - keep: sandboxBackupsTable.keep, - }) +async function getFinalizedSandboxBackupByScope( + database: D1Database, + scope: SandboxBackupScope, +): Promise { + const row = await getAppDatabase(database) + .select() .from(sandboxBackupsTable) .where( and( - eq(sandboxBackupsTable.sandboxId, parsedSandboxId), - eq(sandboxBackupsTable.status, "ready"), + eq(sandboxBackupsTable.sandboxId, scope.sandboxId), + eq(sandboxBackupsTable.sandboxIncarnation, scope.sandboxIncarnation), + eq(sandboxBackupsTable.dir, scope.dir), + scope.operationId === null + ? isNull(sandboxBackupsTable.operationId) + : eq(sandboxBackupsTable.operationId, scope.operationId), + scope.sessionRunId === null + ? isNull(sandboxBackupsTable.sessionRunId) + : eq(sandboxBackupsTable.sessionRunId, scope.sessionRunId), ), ) - .orderBy(asc(sandboxBackupsTable.dir), desc(sandboxBackupsTable.createdAt)) - .all(); + .limit(1) + .get(); + return row === undefined ? null : mapBackup(row); } -export async function markSandboxBackupsPruned( - database: D1Database, - backupIds: readonly string[], -): Promise { - if (backupIds.length === 0) { - return; +function assertTargetMatches( + value: Pick, + target: SandboxBackupTarget, +): void { + if (value.workspaceSessionId !== target.workspaceSessionId) { + throw new Error("A sandbox backup retry changed its immutable workspace target."); } + if ( + "updatesSubjectBackup" in value && + value.updatesSubjectBackup !== target.updateSandboxLastBackup + ) { + throw new Error("A sandbox backup retry changed its subject checkpoint target."); + } +} - const parsedBackupIds = parseSandboxBackupIds(backupIds, "sandbox backup id"); +function workspaceAdmission( + target: SandboxBackupTarget, + scope: SandboxBackupScope, +): { + readonly bindings: readonly unknown[]; + readonly predicate: string; +} { + if (target.workspaceSessionId === null) { + return { bindings: [], predicate: "1 = 1" }; + } + return { + bindings: [target.workspaceSessionId, scope.sandboxId, scope.sandboxIncarnation, scope.dir], + predicate: `EXISTS ( + SELECT 1 + FROM sandbox_session AS workspace + JOIN session AS logical_session ON logical_session.id = workspace.session_id + WHERE workspace.session_id = ? + AND workspace.sandbox_id = ? + AND workspace.sandbox_incarnation = ? + AND workspace.cwd = ? + AND workspace.status IN ('active', 'closed') + AND workspace.cleanup_operation_id IS NULL + AND logical_session.archived_at IS NULL + AND logical_session.cleanup_operation_kind IS NULL + )`, + }; +} - await getAppDatabase(database) - .update(sandboxBackupsTable) - .set({ - status: "pruned", - updatedAt: currentTimestampMs(), - }) - .where(inArray(sandboxBackupsTable.id, [...new Set(parsedBackupIds)])) +async function insertStage( + database: D1Database, + input: { + readonly admission: SandboxBackupAdmission; + readonly sandboxId: SandboxId; + readonly target: SandboxBackupTarget; + readonly ttlSeconds: number; + }, +): Promise { + const id = createPlatformId(); + const scope = scopeForTarget(input.sandboxId, input.admission, input.target.dir); + const workspace = workspaceAdmission(input.target, scope); + const authority = + input.admission.kind === "operation" + ? { + bindings: [ + input.sandboxId, + input.admission.lease.incarnation, + input.admission.lease.operationId, + input.admission.lease.claimOwner, + ], + predicate: `EXISTS ( + SELECT 1 FROM sandbox + WHERE id = ? AND incarnation = ? AND status = 'backing_up' + AND status_operation_id = ? AND claim_owner = ? + AND claim_expires_at > ${DATABASE_NOW_MS_SQL} + )`, + } + : { + bindings: [ + input.admission.sessionRunId, + input.admission.sessionId, + input.admission.driverInstanceId, + input.admission.driverInstanceId, + input.admission.driverGeneration, + input.sandboxId, + input.admission.incarnation, + input.admission.sessionId, + input.admission.sessionRunId, + ], + predicate: `EXISTS ( + SELECT 1 + FROM session_run AS terminal_run + JOIN driver_instance AS terminal_driver + ON terminal_driver.id = terminal_run.driver_instance_id + WHERE terminal_run.id = ? AND terminal_run.session_id = ? + AND terminal_run.driver_instance_id = ? AND terminal_run.status = 'completed' + AND terminal_driver.id = ? AND terminal_driver.generation = ? + AND terminal_driver.sandbox_id = ? AND terminal_driver.sandbox_incarnation = ? + AND terminal_driver.sandbox_session_id = ? + AND terminal_driver.status IN ${TERMINAL_BACKUP_DRIVER_STATUSES_SQL} + AND terminal_driver.status_operation_id = ? + AND NOT EXISTS ( + SELECT 1 FROM session_run AS successor + WHERE successor.session_id = terminal_run.session_id + AND successor.id <> terminal_run.id + AND successor.status IN ('queued', 'booting', 'running', 'waiting_input') + ) + )`, + }; + const result = await database + .prepare( + `INSERT INTO sandbox_backup_staging ( + actual_backup_id, claim_owner, created_at, dir, driver_generation, driver_instance_id, + id, operation_id, sandbox_id, sandbox_incarnation, session_run_id, ttl_seconds, updated_at, + updates_subject_backup, workspace_session_id + ) + SELECT NULL, ?, ${DATABASE_NOW_MS_SQL}, ?, ?, ?, ?, ?, ?, ?, ?, ?, + ${DATABASE_NOW_MS_SQL}, ?, ? + WHERE ${authority.predicate} AND ${workspace.predicate} + AND NOT EXISTS ( + SELECT 1 FROM sandbox_backup AS finalized + WHERE finalized.staging_id = ? + OR (finalized.sandbox_id = ? AND finalized.sandbox_incarnation = ? + AND finalized.dir = ? AND finalized.operation_id IS ? + AND finalized.session_run_id IS ?) + ) + AND NOT EXISTS ( + SELECT 1 FROM sandbox_backup_staging AS existing + WHERE existing.id = ? + OR (existing.sandbox_id = ? AND existing.sandbox_incarnation = ? + AND existing.dir = ? AND existing.operation_id IS ? + AND existing.session_run_id IS ?) + ) + ON CONFLICT DO NOTHING`, + ) + .bind( + input.admission.kind === "operation" ? input.admission.lease.claimOwner : null, + scope.dir, + input.admission.kind === "terminal" ? input.admission.driverGeneration : null, + input.admission.kind === "terminal" ? input.admission.driverInstanceId : null, + id, + scope.operationId, + scope.sandboxId, + scope.sandboxIncarnation, + scope.sessionRunId, + input.ttlSeconds, + input.target.updateSandboxLastBackup ? 1 : 0, + input.target.workspaceSessionId, + ...authority.bindings, + ...workspace.bindings, + id, + scope.sandboxId, + scope.sandboxIncarnation, + scope.dir, + scope.operationId, + scope.sessionRunId, + id, + scope.sandboxId, + scope.sandboxIncarnation, + scope.dir, + scope.operationId, + scope.sessionRunId, + ) .run(); + return (result.meta.changes ?? 0) === 1; } -export async function recordCreatedSandboxBackups( +export async function stageSandboxBackupWrites( database: D1Database, input: { - readonly backups: readonly CreatedSandboxBackupWrite[]; - readonly checkpointSessionId?: string; - readonly operationId?: string | null; + readonly admission: SandboxBackupAdmission; readonly sandboxId: string; - readonly sessionRunId?: string; + readonly targets: readonly SandboxBackupTarget[]; readonly ttlSeconds: number; }, -): Promise { - if (input.backups.length === 0) { - return; +): Promise { + assertPositiveSafeInteger(input.ttlSeconds, "Sandbox backup TTL"); + assertPositiveSafeInteger( + input.admission.kind === "operation" + ? input.admission.lease.incarnation + : input.admission.incarnation, + "Sandbox backup incarnation", + ); + if ( + input.admission.kind === "terminal" && + (!Number.isSafeInteger(input.admission.driverGeneration) || + input.admission.driverGeneration < 0) + ) { + throw new TypeError("Terminal sandbox backup identity is invalid."); } - const sandboxId = parsePlatformId(input.sandboxId, "sandbox id"); - const checkpointSessionId = - input.checkpointSessionId === undefined - ? null - : parsePlatformId(input.checkpointSessionId, "checkpoint session id"); - const operationId = - input.operationId === undefined || input.operationId === null - ? input.operationId - : parsePlatformId(input.operationId, "runtime operation id"); - const sessionRunId = - input.sessionRunId === undefined - ? null - : parsePlatformId(input.sessionRunId, "session run id"); - const now = currentTimestampMs(); - const backupRows = input.backups.map((entry, index) => ({ - createdAt: now, - dir: entry.backup.dir, - errorMessage: null, - id: parsePlatformId(entry.backup.id, `sandbox backup id ${index}`), - keep: false, - sandboxId, - sessionRunId, - status: "ready" as const, - ttlSeconds: input.ttlSeconds, - updatedAt: now, - })); - let subjectCheckpointBackup: CreatedSandboxBackupRecord | null = null; + const writes: SandboxBackupWrite[] = []; - for (const entry of input.backups) { - if (entry.updateSandboxLastBackup) { - subjectCheckpointBackup = entry.backup; + for (const rawTarget of input.targets) { + const target: SandboxBackupTarget = { + ...rawTarget, + workspaceSessionId: + rawTarget.workspaceSessionId === null + ? null + : parsePlatformId(rawTarget.workspaceSessionId, "workspace session id"), + }; + if (input.admission.kind === "terminal" && target.workspaceSessionId === null) { + throw new Error("Terminal sandbox backups require an exact workspace session."); } - } - - const results = await runAppDatabaseBatch(database, (appDb) => { - const queries: [AppDatabaseBatchItem, ...AppDatabaseBatchItem[]] = [ - appDb - .insert(sandboxBackupsTable) - .values(backupRows.slice(0, SANDBOX_BACKUP_INSERT_BATCH_SIZE)), - ]; - - for ( - let index = SANDBOX_BACKUP_INSERT_BATCH_SIZE; - index < backupRows.length; - index += SANDBOX_BACKUP_INSERT_BATCH_SIZE + const scope = scopeForTarget(sandboxId, input.admission, target.dir); + const finalized = await getFinalizedSandboxBackupByScope(database, scope); + if (finalized !== null) { + assertTargetMatches(finalized, target); + writes.push({ backup: finalized, kind: "finalized" }); + continue; + } + let stage = await getStageByScope(database, scope); + let isNew = false; + if ( + stage !== null && + input.admission.kind === "operation" && + stage.claimOwner !== input.admission.lease.claimOwner ) { - queries.push( - appDb - .insert(sandboxBackupsTable) - .values(backupRows.slice(index, index + SANDBOX_BACKUP_INSERT_BATCH_SIZE)), - ); + if (await isSandboxBackupStageCurrent(database, stage.id)) { + throw new Error("Sandbox backup stage belongs to another live lease owner."); + } + const revoked = await revokeSandboxBackupStage(database, { + onlyIfStale: true, + stagingId: stage.id, + }); + if (revoked?.actualBackupId !== null && revoked?.actualBackupId !== undefined) { + await authorizeSandboxBackupDeletion(database, { + authority: { kind: "runtime_candidate", stagingId: stage.id }, + backupId: revoked.actualBackupId, + }); + } + stage = null; } - - if (checkpointSessionId !== null && sessionRunId !== null) { - queries.push( - appDb - .update(nativeResumeRefsTable) - .set({ - committedSessionRunId: sessionRunId, - committedValue: sql`${nativeResumeRefsTable.value}`, - }) - .where( - and( - eq(nativeResumeRefsTable.sessionId, checkpointSessionId), - eq(nativeResumeRefsTable.observedSessionRunId, sessionRunId), - ), - ), - ); + if (stage === null) { + isNew = await insertStage(database, { + admission: input.admission, + sandboxId, + target, + ttlSeconds: input.ttlSeconds, + }); + const racedFinalized = await getFinalizedSandboxBackupByScope(database, scope); + if (racedFinalized !== null) { + assertTargetMatches(racedFinalized, target); + writes.push({ backup: racedFinalized, kind: "finalized" }); + continue; + } + stage = await getStageByScope(database, scope); } - - if (subjectCheckpointBackup) { - queries.push( - appDb - .update(sandboxesTable) - .set({ - lastBackupId: parsePlatformId( - subjectCheckpointBackup.id, - "checkpoint sandbox backup id", - ), - updatedAt: now, - }) - .where( - and( - eq(sandboxesTable.id, sandboxId), - inArray(sandboxesTable.status, ["backing_up", "destroying"]), - ...sandboxStatusOperationCondition(operationId), - ), - ), - ); + if (stage === null) { + throw new Error("Sandbox backup admission lost its exact lifecycle authority."); } + if ( + input.admission.kind === "operation" + ? stage.claimOwner !== input.admission.lease.claimOwner + : stage.claimOwner !== null || + stage.driverInstanceId !== input.admission.driverInstanceId || + stage.driverGeneration !== input.admission.driverGeneration + ) { + throw new Error("Sandbox backup stage belongs to another admission authority."); + } + if (!(await isSandboxBackupStageCurrent(database, stage.id))) { + throw new Error("Sandbox backup stage lost its lifecycle authority before use."); + } + assertTargetMatches(stage, target); + writes.push({ isNew, kind: "staged", stage }); + } + return writes; +} + +function currentAuthority(alias: string): string { + return `( + (${alias}.operation_id IS NOT NULL AND EXISTS ( + SELECT 1 FROM sandbox AS subject + WHERE subject.id = ${alias}.sandbox_id + AND subject.incarnation = ${alias}.sandbox_incarnation + AND subject.status = 'backing_up' + AND subject.status_operation_id = ${alias}.operation_id + AND ${alias}.claim_owner IS NOT NULL + AND subject.claim_owner = ${alias}.claim_owner + AND subject.claim_expires_at > ${DATABASE_NOW_MS_SQL} + )) OR (${alias}.session_run_id IS NOT NULL AND EXISTS ( + SELECT 1 FROM sandbox AS subject + JOIN session_run AS terminal_run ON terminal_run.id = ${alias}.session_run_id + JOIN driver_instance AS terminal_driver + ON terminal_driver.id = ${alias}.driver_instance_id + WHERE subject.id = ${alias}.sandbox_id + AND subject.incarnation = ${alias}.sandbox_incarnation + AND terminal_run.session_id = ${alias}.workspace_session_id + AND terminal_run.status = 'completed' + AND terminal_run.driver_instance_id = ${alias}.driver_instance_id + AND terminal_driver.generation = ${alias}.driver_generation + AND terminal_driver.sandbox_id = ${alias}.sandbox_id + AND terminal_driver.sandbox_incarnation = ${alias}.sandbox_incarnation + AND terminal_driver.sandbox_session_id = ${alias}.workspace_session_id + AND terminal_driver.status IN ${TERMINAL_BACKUP_DRIVER_STATUSES_SQL} + AND terminal_driver.status_operation_id = ${alias}.session_run_id + AND NOT EXISTS ( + SELECT 1 FROM session_run AS successor + WHERE successor.session_id = terminal_run.session_id + AND successor.id <> terminal_run.id + AND successor.status IN ('queued', 'booting', 'running', 'waiting_input') + ) + )) + ) AND (${alias}.workspace_session_id IS NULL OR EXISTS ( + SELECT 1 + FROM sandbox_session AS workspace + JOIN session AS logical_session ON logical_session.id = workspace.session_id + WHERE workspace.session_id = ${alias}.workspace_session_id + AND workspace.sandbox_id = ${alias}.sandbox_id + AND workspace.sandbox_incarnation = ${alias}.sandbox_incarnation + AND workspace.cwd = ${alias}.dir + AND workspace.status IN ('active', 'closed') + AND workspace.cleanup_operation_id IS NULL + AND logical_session.archived_at IS NULL + AND logical_session.cleanup_operation_kind IS NULL + ))`; +} - return queries; - }); +export async function isSandboxBackupStageCurrent( + database: D1Database, + stagingId: SandboxBackupId, +): Promise { + return ( + (await database + .prepare( + `SELECT id FROM sandbox_backup_staging AS stage + WHERE id = ? AND ${currentAuthority("stage")}`, + ) + .bind(stagingId) + .first()) !== null + ); +} - if (!subjectCheckpointBackup) { - return; +export async function claimSandboxBackupStageActual( + database: D1Database, + input: { + readonly actualBackupId: SandboxBackupId; + readonly dir: string; + readonly sandboxIncarnation: number; + readonly stagingId: SandboxBackupId; + }, +): Promise<{ readonly actualBackupId: SandboxBackupId } | null> { + const claimed = await database + .prepare( + `UPDATE sandbox_backup_staging AS stage + SET actual_backup_id = ?, updated_at = ${DATABASE_NOW_MS_SQL} + WHERE id = ? AND sandbox_incarnation = ? AND dir = ? + AND actual_backup_id IS NULL AND ${currentAuthority("stage")} + AND NOT EXISTS ( + SELECT 1 FROM sandbox_backup_delete_intent AS deletion + WHERE deletion.backup_id = ? + ) + AND NOT EXISTS ( + SELECT 1 FROM sandbox_backup + WHERE id = ? + ) + AND NOT EXISTS ( + SELECT 1 FROM environment_package_artifact_backup + WHERE backup_id = ? + UNION ALL + SELECT 1 FROM environment_package_artifact_backup_staging + WHERE actual_backup_id = ? + ) + RETURNING actual_backup_id`, + ) + .bind( + input.actualBackupId, + input.stagingId, + input.sandboxIncarnation, + input.dir, + input.actualBackupId, + input.actualBackupId, + input.actualBackupId, + input.actualBackupId, + ) + .first<{ actual_backup_id: SandboxBackupId }>(); + if (claimed !== null) { + return { actualBackupId: claimed.actual_backup_id }; } - const updated = results.at(-1); + const winner = await database + .prepare( + `SELECT actual_backup_id + FROM ( + SELECT actual_backup_id, 0 AS priority + FROM sandbox_backup_staging + WHERE id = ? AND actual_backup_id IS NOT NULL + AND NOT EXISTS ( + SELECT 1 FROM sandbox_backup_delete_intent AS deletion + WHERE deletion.backup_id = sandbox_backup_staging.actual_backup_id + ) + AND NOT EXISTS ( + SELECT 1 FROM sandbox_backup + WHERE id = sandbox_backup_staging.actual_backup_id + ) + AND NOT EXISTS ( + SELECT 1 FROM environment_package_artifact_backup + WHERE backup_id = sandbox_backup_staging.actual_backup_id + UNION ALL + SELECT 1 FROM environment_package_artifact_backup_staging + WHERE actual_backup_id = sandbox_backup_staging.actual_backup_id + ) + UNION ALL + SELECT id AS actual_backup_id, 1 AS priority + FROM sandbox_backup + WHERE staging_id = ? AND status = 'ready' + AND NOT EXISTS ( + SELECT 1 FROM sandbox_backup_delete_intent AS deletion + WHERE deletion.backup_id = sandbox_backup.id + ) + AND NOT EXISTS ( + SELECT 1 FROM environment_package_artifact_backup + WHERE backup_id = sandbox_backup.id + UNION ALL + SELECT 1 FROM environment_package_artifact_backup_staging + WHERE actual_backup_id = sandbox_backup.id + ) + ) + ORDER BY priority + LIMIT 1`, + ) + .bind(input.stagingId, input.stagingId) + .first<{ actual_backup_id: SandboxBackupId }>(); + return winner === null ? null : { actualBackupId: winner.actual_backup_id }; +} + +export async function clearMissingSandboxBackupStageActual( + database: D1Database, + input: { readonly actualBackupId: SandboxBackupId; readonly stagingId: SandboxBackupId }, +): Promise { + const result = await database + .prepare( + `UPDATE sandbox_backup_staging + SET actual_backup_id = NULL, updated_at = ${DATABASE_NOW_MS_SQL} + WHERE id = ? AND actual_backup_id = ? + AND NOT EXISTS ( + SELECT 1 FROM sandbox_backup + WHERE staging_id = sandbox_backup_staging.id + )`, + ) + .bind(input.stagingId, input.actualBackupId) + .run(); + return (result.meta.changes ?? 0) === 1; +} + +export async function revokeSandboxBackupStage( + database: D1Database, + input: { readonly onlyIfStale?: boolean; readonly stagingId: SandboxBackupId }, +): Promise<{ readonly actualBackupId: SandboxBackupId | null } | null> { + const row = await database + .prepare( + `DELETE FROM sandbox_backup_staging AS stage + WHERE id = ?${input.onlyIfStale === true ? ` AND NOT (${currentAuthority("stage")})` : ""} + RETURNING actual_backup_id`, + ) + .bind(input.stagingId) + .first<{ actual_backup_id: SandboxBackupId | null }>(); + return row === null ? null : { actualBackupId: row.actual_backup_id }; +} + +export async function listSandboxBackupStages( + database: D1Database, + limit: number, +): Promise { + assertPositiveSafeInteger(limit, "Sandbox backup stage limit"); + return getAppDatabase(database) + .select() + .from(sandboxBackupStagingTable) + .orderBy(asc(sandboxBackupStagingTable.updatedAt), asc(sandboxBackupStagingTable.id)) + .limit(limit) + .all(); +} + +export async function deferSandboxBackupStageRepair( + database: D1Database, + stage: Pick, +): Promise { + return ( + (await database + .prepare( + `UPDATE sandbox_backup_staging + SET updated_at = max(${DATABASE_NOW_MS_SQL}, updated_at + 1) + WHERE id = ? AND updated_at = ? AND updated_at < 9007199254740991 + RETURNING id`, + ) + .bind(stage.id, stage.updatedAt) + .first()) !== null + ); +} + +export interface FinalizeSandboxBackupResult { + readonly backup: SandboxBackupRecord; + readonly candidateAccepted: boolean; + readonly complete: boolean; +} + +export async function finalizeSandboxBackupStage( + database: D1Database, + input: { readonly actualBackupId: SandboxBackupId; readonly stagingId: SandboxBackupId }, +): Promise { + const stage = await getSandboxBackupStage(database, input.stagingId); + if (stage === null) { + const finalized = await getSandboxBackupRecordByStagingId(database, input.stagingId); + return finalized === null + ? null + : { + backup: finalized, + candidateAccepted: finalized.id === input.actualBackupId, + complete: true, + }; + } + const scope: SandboxBackupScope = { + dir: stage.dir, + operationId: stage.operationId, + sandboxId: stage.sandboxId, + sandboxIncarnation: stage.sandboxIncarnation, + sessionRunId: stage.sessionRunId, + }; + const existing = await getFinalizedSandboxBackupByScope(database, scope); + if (existing !== null && existing.stagingId !== stage.id) { + await revokeSandboxBackupStage(database, { stagingId: stage.id }); + return { backup: existing, candidateAccepted: false, complete: true }; + } + if (stage.actualBackupId !== input.actualBackupId) { + return null; + } + + await database.batch([ + database + .prepare( + `INSERT INTO sandbox_backup ( + created_at, dir, id, keep, operation_id, sandbox_id, sandbox_incarnation, + session_run_id, staging_id, status, ttl_seconds, updated_at, workspace_session_id + ) + SELECT created_at, dir, actual_backup_id, 0, operation_id, sandbox_id, + sandbox_incarnation, session_run_id, id, 'ready', ttl_seconds, + ${DATABASE_NOW_MS_SQL}, workspace_session_id + FROM sandbox_backup_staging AS stage + WHERE id = ? AND actual_backup_id = ? AND ${currentAuthority("stage")} + AND NOT EXISTS ( + SELECT 1 FROM sandbox_backup_delete_intent AS deletion + WHERE deletion.backup_id = stage.actual_backup_id + ) + AND NOT EXISTS ( + SELECT 1 FROM sandbox_backup AS existing + WHERE existing.id = stage.actual_backup_id + OR existing.staging_id = stage.id + OR (existing.sandbox_id = stage.sandbox_id + AND existing.sandbox_incarnation = stage.sandbox_incarnation + AND existing.dir = stage.dir + AND ((stage.operation_id IS NOT NULL + AND existing.operation_id = stage.operation_id) + OR (stage.session_run_id IS NOT NULL + AND existing.session_run_id = stage.session_run_id))) + ) + AND NOT EXISTS ( + SELECT 1 FROM environment_package_artifact_backup + WHERE backup_id = stage.actual_backup_id + UNION ALL + SELECT 1 FROM environment_package_artifact_backup_staging + WHERE actual_backup_id = stage.actual_backup_id + ) + ON CONFLICT DO NOTHING`, + ) + .bind(stage.id, input.actualBackupId), + database + .prepare( + `UPDATE native_resume_ref + SET committed_session_run_id = observed_session_run_id, committed_value = value + WHERE EXISTS ( + SELECT 1 FROM sandbox_backup_staging AS stage + JOIN sandbox_backup AS finalized + ON finalized.staging_id = stage.id AND finalized.id = stage.actual_backup_id + WHERE stage.id = ? AND stage.session_run_id = observed_session_run_id + AND stage.workspace_session_id = native_resume_ref.session_id + AND ${currentAuthority("stage")} + )`, + ) + .bind(stage.id), + database + .prepare( + `UPDATE sandbox + SET last_backup_id = ?, updated_at = ${DATABASE_NOW_MS_SQL} + WHERE id = ? AND incarnation = ? + AND status = 'backing_up' AND status_operation_id IS ? + AND EXISTS ( + SELECT 1 FROM sandbox_backup_staging AS stage + JOIN sandbox_backup AS finalized + ON finalized.staging_id = stage.id AND finalized.id = stage.actual_backup_id + WHERE stage.id = ? AND stage.updates_subject_backup = 1 + AND ${currentAuthority("stage")} + )`, + ) + .bind( + input.actualBackupId, + stage.sandboxId, + stage.sandboxIncarnation, + stage.operationId, + stage.id, + ), + database + .prepare( + `DELETE FROM sandbox_backup_staging AS stage + WHERE id = ? AND actual_backup_id = ? + AND EXISTS ( + SELECT 1 FROM sandbox_backup AS finalized + WHERE finalized.staging_id = stage.id AND finalized.id = stage.actual_backup_id + AND finalized.status = 'ready' + ) + AND (updates_subject_backup = 0 OR EXISTS ( + SELECT 1 FROM sandbox AS subject + WHERE subject.id = stage.sandbox_id + AND subject.incarnation = stage.sandbox_incarnation + AND subject.last_backup_id = stage.actual_backup_id + )) + AND NOT EXISTS ( + SELECT 1 FROM native_resume_ref AS native_ref + WHERE native_ref.session_id = stage.workspace_session_id + AND native_ref.observed_session_run_id = stage.session_run_id + AND native_ref.committed_session_run_id IS NOT stage.session_run_id + )`, + ) + .bind(stage.id, input.actualBackupId), + ]); + const finalized = + (await getSandboxBackupRecordByStagingId(database, stage.id)) ?? + (await getFinalizedSandboxBackupByScope(database, scope)); + return finalized === null + ? null + : { + backup: finalized, + candidateAccepted: finalized.id === input.actualBackupId, + complete: (await getSandboxBackupStage(database, stage.id)) === null, + }; +} + +export async function listReadySandboxBackupsForSessionRun( + database: D1Database, + input: { readonly sandboxId: string; readonly sessionRunId: string }, +): Promise> { + const sandboxId = parsePlatformId(input.sandboxId, "sandbox id"); + const sessionRunId = parsePlatformId(input.sessionRunId, "session run id"); + return getAppDatabase(database) + .select({ dir: sandboxBackupsTable.dir, id: sandboxBackupsTable.id }) + .from(sandboxBackupsTable) + .where( + and( + eq(sandboxBackupsTable.sandboxId, sandboxId), + eq(sandboxBackupsTable.sessionRunId, sessionRunId), + eq(sandboxBackupsTable.status, "ready"), + ), + ) + .all(); +} + +export interface ReadySandboxBackupForPruning { + readonly createdAt: number; + readonly dir: string; + readonly id: SandboxBackupId; + readonly keep: boolean; + readonly protected: boolean; + readonly sandboxId: SandboxId; +} - if (getD1ChangeCount(updated) === 0) { - throw new Error("Runtime subject changed before checkpoint backup was recorded."); +export async function listReadySandboxBackupsForPruning( + database: D1Database, + sandboxIdInput: string, +): Promise { + const sandboxId = parsePlatformId(sandboxIdInput, "sandbox id"); + const rows = await database + .prepare( + `SELECT backup.created_at, backup.dir, backup.id, backup.keep, backup.sandbox_id, + CASE WHEN EXISTS (SELECT 1 FROM sandbox WHERE last_backup_id = backup.id) + OR EXISTS ( + SELECT 1 FROM sandbox + WHERE status = 'restoring' AND last_restore_backup_id = backup.id + ) + OR EXISTS ( + SELECT 1 FROM session + WHERE id = backup.workspace_session_id + AND runtime_provisioning_operation_id IS NOT NULL + ) + OR EXISTS ( + SELECT 1 FROM sandbox_backup_staging AS stage + WHERE stage.id = backup.staging_id OR stage.actual_backup_id = backup.id + ) + THEN 1 ELSE 0 END AS protected + FROM sandbox_backup AS backup + WHERE backup.sandbox_id = ? AND backup.status = 'ready' + ORDER BY backup.dir ASC, backup.created_at DESC, backup.id DESC`, + ) + .bind(sandboxId) + .all<{ + created_at: number; + dir: string; + id: SandboxBackupId; + keep: number; + protected: number; + sandbox_id: SandboxId; + }>(); + return rows.results.map((row) => ({ + createdAt: row.created_at, + dir: row.dir, + id: row.id, + keep: row.keep === 1, + protected: row.protected === 1, + sandboxId: row.sandbox_id, + })); +} + +export async function markSandboxBackupsPruned( + database: D1Database, + backupIds: readonly SandboxBackupId[], +): Promise { + const pruned: SandboxBackupId[] = []; + for (const id of backupIds) { + const row = await database + .prepare( + `UPDATE sandbox_backup AS backup + SET status = 'pruned', updated_at = ${DATABASE_NOW_MS_SQL} + WHERE id = ? AND status = 'ready' AND keep = 0 + AND NOT EXISTS (SELECT 1 FROM sandbox WHERE last_backup_id = backup.id) + AND NOT EXISTS ( + SELECT 1 FROM sandbox + WHERE status = 'restoring' AND last_restore_backup_id = backup.id + ) + AND NOT EXISTS ( + SELECT 1 FROM session + WHERE id = backup.workspace_session_id + AND runtime_provisioning_operation_id IS NOT NULL + ) + AND NOT EXISTS ( + SELECT 1 FROM sandbox_backup_staging AS stage + WHERE stage.id = backup.staging_id OR stage.actual_backup_id = backup.id + ) + RETURNING id`, + ) + .bind(id) + .first<{ id: SandboxBackupId }>(); + if (row !== null) { + pruned.push(row.id); + } } + return pruned; +} + +export interface SandboxSessionBackupCandidate { + readonly cwd: string; + readonly lastMessageAt: number | null; + readonly sessionId: SessionId; + readonly sessionStatus: SessionStatus; } export async function listSandboxSessionBackupCandidates( database: D1Database, - sandboxId: string, + sandboxIdInput: string, + sandboxIncarnation: number, ): Promise { - const parsedSandboxId = parsePlatformId(sandboxId, "sandbox id"); - const results = await getAppDatabase(database) + assertPositiveSafeInteger(sandboxIncarnation, "Sandbox backup candidate incarnation"); + const sandboxId = parsePlatformId(sandboxIdInput, "sandbox id"); + return getAppDatabase(database) .select({ cwd: sandboxSessionsTable.cwd, - last_message_at: sessionsTable.lastMessageAt, - session_id: sandboxSessionsTable.sessionId, - session_status: sessionsTable.status, + lastMessageAt: sessionsTable.lastMessageAt, + sessionId: sandboxSessionsTable.sessionId, + sessionStatus: sessionsTable.status, }) .from(sandboxSessionsTable) .innerJoin(sessionsTable, eq(sessionsTable.id, sandboxSessionsTable.sessionId)) .where( and( - eq(sandboxSessionsTable.sandboxId, parsedSandboxId), + eq(sandboxSessionsTable.sandboxId, sandboxId), + eq(sandboxSessionsTable.sandboxIncarnation, sandboxIncarnation), inArray(sandboxSessionsTable.status, ["active", "closed"]), + isNull(sandboxSessionsTable.cleanupOperationId), + isNull(sessionsTable.archivedAt), + isNull(sessionsTable.cleanupOperationKind), ), ) .all(); - - return results.map((row) => ({ - cwd: row.cwd, - lastMessageAt: row.last_message_at, - sessionId: row.session_id, - sessionStatus: row.session_status, - })); } -export async function listSandboxBackupIdsByDir( +export async function revokeSandboxBackupsForSessionDelete( database: D1Database, - dir: string, + input: { + readonly cwd: string; + readonly operationId: RuntimeOperationId; + readonly sandboxId: SandboxId; + readonly sessionId: SessionId; + }, ): Promise { - const results = await getAppDatabase(database) - .select({ id: sandboxBackupsTable.id }) - .from(sandboxBackupsTable) - .where(eq(sandboxBackupsTable.dir, dir)) - .all(); - - return results.map((backup) => backup.id); -} - -export async function deleteSandboxBackupRecordsForDir( - database: D1Database, - dir: string, -): Promise { - await getAppDatabase(database) - .delete(sandboxBackupsTable) - .where(eq(sandboxBackupsTable.dir, dir)) - .run(); + const owned = (alias: string) => `EXISTS ( + SELECT 1 FROM session AS deleting_session + JOIN sandbox_session AS workspace ON workspace.session_id = deleting_session.id + WHERE deleting_session.id = ? + AND deleting_session.cleanup_operation_kind = 'delete' + AND deleting_session.status_operation_id = ? + AND workspace.sandbox_id = ? AND workspace.cwd = ? + AND ${alias}.sandbox_id = workspace.sandbox_id + AND ${alias}.dir = workspace.cwd + )`; + const results = await database.batch([ + database + .prepare( + `DELETE FROM sandbox_backup_staging AS stage + WHERE stage.workspace_session_id = ? AND stage.sandbox_id = ? AND stage.dir = ? + AND ${owned("stage")} + RETURNING actual_backup_id`, + ) + .bind( + input.sessionId, + input.sandboxId, + input.cwd, + input.sessionId, + input.operationId, + input.sandboxId, + input.cwd, + ), + database + .prepare( + `UPDATE sandbox_backup AS backup + SET status = 'pruned', updated_at = ${DATABASE_NOW_MS_SQL} + WHERE status = 'ready' + AND workspace_session_id = ? AND sandbox_id = ? AND dir = ? + AND ${owned("backup")} + RETURNING id`, + ) + .bind( + input.sessionId, + input.sandboxId, + input.cwd, + input.sessionId, + input.operationId, + input.sandboxId, + input.cwd, + ), + ]); + const ids = new Set(); + const [stagedResult, finalizedResult] = results; + if (stagedResult === undefined || finalizedResult === undefined) { + throw new Error("Session backup cleanup lost its D1 batch results."); + } + for (const row of stagedResult.results as Array<{ + actual_backup_id: SandboxBackupId | null; + }>) { + if (row.actual_backup_id !== null) { + ids.add(row.actual_backup_id); + } + } + for (const row of finalizedResult.results as Array<{ id: SandboxBackupId }>) { + ids.add(row.id); + } + return [...ids]; } diff --git a/apps/api/src/modules/runtime/infrastructure/sandbox-backup.service.ts b/apps/api/src/modules/runtime/infrastructure/sandbox-backup.service.ts index 7c3920e9..58f7d8c5 100644 --- a/apps/api/src/modules/runtime/infrastructure/sandbox-backup.service.ts +++ b/apps/api/src/modules/runtime/infrastructure/sandbox-backup.service.ts @@ -1,220 +1,229 @@ +import type { + DriverInstanceId, + RuntimeOperationId, + SandboxBackupId, + SandboxId, + SessionId, + SessionRunId, +} from "@mosoo/id"; +import { parsePlatformId } from "@mosoo/id"; + import { logWarn } from "../../../platform/cloudflare/logger"; import type { ApiBindings } from "../../../platform/cloudflare/worker-types"; import { shouldBackupSandboxSession } from "../../sessions/domain/session-lifecycle"; import type { RuntimeCheckpointRule } from "../domain/runtime-kind-policy"; import { RuntimeSubjectCheckpointFailedError } from "./runtime-subject-lifecycle/runtime-subject-errors"; -import { createRuntimeSandboxBackup, deleteSandboxBackupObjects } from "./sandbox-backup-platform"; +import type { RuntimeSubjectOperationLease } from "./runtime-subject-lifecycle/runtime-subject-store"; +import { + createRuntimeSandboxBackup, + deleteAuthorizedSandboxBackupObjects, + isRuntimeSandboxBackupObjectReady, +} from "./sandbox-backup-platform"; import { selectSandboxBackupPruneIds } from "./sandbox-backup-pruning"; -import type { CreatedSandboxBackupWrite } from "./sandbox-backup-store"; +import type { + SandboxBackupAdmission, + SandboxBackupDeletionAuthority, + SandboxBackupStage, + SandboxBackupTarget, +} from "./sandbox-backup-store"; import { - deleteSandboxBackupRecordsForDir, - listReadySandboxBackupsForSessionRun, + authorizeSandboxBackupDeletion, + claimSandboxBackupStageActual, + clearMissingSandboxBackupStageActual, + deferSandboxBackupStageRepair, + finalizeSandboxBackupStage, + getSandboxBackupStage, + isSandboxBackupStageCurrent, listReadySandboxBackupsForPruning, - listSandboxBackupIdsByDir, + listReadySandboxBackupsForSessionRun, + listSandboxBackupStages, listSandboxSessionBackupCandidates, markSandboxBackupsPruned, - recordCreatedSandboxBackups, + revokeSandboxBackupsForSessionDelete, + revokeSandboxBackupStage, + stageSandboxBackupWrites, } from "./sandbox-backup-store"; const BACKUP_TTL_SECONDS = 10 * 365 * 24 * 60 * 60; -export interface SandboxSessionBackupTarget { - cwd: string; - sessionId: string; +async function deleteSandboxBackup( + bindings: ApiBindings, + backupId: SandboxBackupId, + authority: SandboxBackupDeletionAuthority, +): Promise { + if (!(await authorizeSandboxBackupDeletion(bindings.DB, { authority, backupId }))) { + return false; + } + await deleteAuthorizedSandboxBackupObjects(bindings, [backupId]); + return true; +} + +export interface TerminalSandboxBackupAuthority { + readonly driverGeneration: number; + readonly driverInstanceId: DriverInstanceId; + readonly incarnation: number; + readonly sessionId: SessionId; + readonly sessionRunId: SessionRunId; } -interface SandboxCheckpointBackupTarget { - readonly dir: string; - readonly sessionId: string | null; - readonly updateSandboxLastBackup: boolean; +export interface SandboxSessionBackupTarget { + readonly cwd: string; + readonly sessionId: string; } async function pruneSandboxBackups(bindings: ApiBindings, sandboxId: string): Promise { const backups = await listReadySandboxBackupsForPruning(bindings.DB, sandboxId); - const pruneIds = selectSandboxBackupPruneIds(backups); - - await deleteSandboxBackupObjects(bindings, pruneIds); - await markSandboxBackupsPruned(bindings.DB, pruneIds); + const candidates = selectSandboxBackupPruneIds(backups); + const pruned = await markSandboxBackupsPruned(bindings.DB, candidates); + for (const backupId of pruned) { + await deleteSandboxBackup(bindings, backupId, { kind: "pruned" }); + } } async function listSandboxSessionBackupTargets( database: D1Database, sandboxId: string, + sandboxIncarnation: number, ): Promise { - const candidates = await listSandboxSessionBackupCandidates(database, sandboxId); - - return candidates + return (await listSandboxSessionBackupCandidates(database, sandboxId, sandboxIncarnation)) .filter((candidate) => shouldBackupSandboxSession({ lastMessageAt: candidate.lastMessageAt, sessionStatus: candidate.sessionStatus, }), ) - .map((candidate) => ({ - cwd: candidate.cwd, - sessionId: candidate.sessionId, - })); + .map((candidate) => ({ cwd: candidate.cwd, sessionId: candidate.sessionId })); } -async function listSandboxCheckpointBackupTargets( +async function listCheckpointTargets( database: D1Database, input: { readonly rules: readonly RuntimeCheckpointRule[]; readonly sandboxId: string; + readonly sandboxIncarnation: number; }, -): Promise { - const targets: SandboxCheckpointBackupTarget[] = []; +): Promise { + const targets: SandboxBackupTarget[] = []; let sessionTargets: SandboxSessionBackupTarget[] | null = null; - for (const rule of input.rules) { switch (rule.type) { case "subject_memory": { targets.push({ dir: rule.path, - sessionId: null, updateSandboxLastBackup: rule.updateSubjectCheckpoint, + workspaceSessionId: null, }); break; } case "session_workspaces": { - sessionTargets ??= await listSandboxSessionBackupTargets(database, input.sandboxId); + sessionTargets ??= await listSandboxSessionBackupTargets( + database, + input.sandboxId, + input.sandboxIncarnation, + ); targets.push( ...sessionTargets.map((target) => ({ dir: target.cwd, - sessionId: rule.sanitizeTransientState ? target.sessionId : null, updateSandboxLastBackup: false, + workspaceSessionId: target.sessionId, })), ); break; } } } - return targets; } -async function createSandboxBackupsForTargets( +async function resolveStageActual( bindings: ApiBindings, - input: { - readonly sandboxId: string; - readonly targets: readonly SandboxCheckpointBackupTarget[]; - }, -): Promise { - const results = await Promise.allSettled( - input.targets.map(async (target) => ({ - backup: await createRuntimeSandboxBackup(bindings, { - dir: target.dir, - sandboxId: input.sandboxId, - sessionId: target.sessionId, - ttlSeconds: BACKUP_TTL_SECONDS, - }).catch((error: unknown) => { - throw new RuntimeSubjectCheckpointFailedError({ - cause: error, - dir: target.dir, - runtimeSubjectId: input.sandboxId, - }); - }), - updateSandboxLastBackup: target.updateSandboxLastBackup, - })), - ); - const createdBackups = results.flatMap((result) => - result.status === "fulfilled" ? [result.value] : [], - ); - const failedBackup = results.find((result) => result.status === "rejected"); - - if (failedBackup?.status === "rejected") { - await deleteSandboxBackupObjects( - bindings, - createdBackups.map((entry) => entry.backup.id), - ); - throw failedBackup.reason; + initial: SandboxBackupStage, +): Promise<{ + readonly actualId: NonNullable; + readonly stage: SandboxBackupStage; +}> { + let stage = initial; + if (stage.actualBackupId !== null) { + if ( + await isRuntimeSandboxBackupObjectReady(bindings, { + backupId: stage.actualBackupId, + dir: stage.dir, + stagingId: stage.id, + }) + ) { + return { actualId: stage.actualBackupId, stage }; + } + await clearMissingSandboxBackupStageActual(bindings.DB, { + actualBackupId: stage.actualBackupId, + stagingId: stage.id, + }); + const cleared = await getSandboxBackupStage(bindings.DB, stage.id); + if (cleared === null) { + throw new Error("Sandbox backup stage was revoked while its object was verified."); + } + stage = cleared; } - return createdBackups; + const candidate = await createRuntimeSandboxBackup(bindings, { + dir: stage.dir, + incarnation: stage.sandboxIncarnation, + sandboxId: stage.sandboxId, + sessionId: stage.workspaceSessionId, + stagingId: stage.id, + ttlSeconds: stage.ttlSeconds, + }); + if (candidate.dir !== stage.dir) { + await deleteSandboxBackup(bindings, candidate.id, { + kind: "runtime_invalid", + stagingId: stage.id, + }); + throw new Error("Sandbox backup platform result changed its staged directory."); + } + const claimed = await claimSandboxBackupStageActual(bindings.DB, { + actualBackupId: candidate.id, + dir: stage.dir, + sandboxIncarnation: stage.sandboxIncarnation, + stagingId: stage.id, + }); + if (claimed === null || claimed.actualBackupId === null) { + await deleteSandboxBackup(bindings, candidate.id, { + kind: "runtime_candidate", + stagingId: stage.id, + }); + throw new Error("Sandbox backup creation lost its durable stage."); + } + if (claimed.actualBackupId !== candidate.id) { + await deleteSandboxBackup(bindings, candidate.id, { + kind: "runtime_candidate", + stagingId: stage.id, + }); + } + return { actualId: claimed.actualBackupId, stage }; } -async function recordCreatedCheckpointBackups( +async function createAndFinalizeStage( bindings: ApiBindings, - input: { - readonly backups: readonly CreatedSandboxBackupWrite[]; - readonly checkpointSessionId?: string; - readonly operationId?: string | null; - readonly sandboxId: string; - readonly sessionRunId?: string; - }, + stage: SandboxBackupStage, ): Promise { - try { - await recordCreatedSandboxBackups(bindings.DB, { - backups: input.backups, - ...(input.checkpointSessionId === undefined - ? {} - : { checkpointSessionId: input.checkpointSessionId }), - ...(input.operationId === undefined ? {} : { operationId: input.operationId }), - sandboxId: input.sandboxId, - ...(input.sessionRunId === undefined ? {} : { sessionRunId: input.sessionRunId }), - ttlSeconds: BACKUP_TTL_SECONDS, + const { actualId } = await resolveStageActual(bindings, stage); + const finalized = await finalizeSandboxBackupStage(bindings.DB, { + actualBackupId: actualId, + stagingId: stage.id, + }); + if (finalized === null) { + await deleteSandboxBackup(bindings, actualId, { + kind: "runtime_candidate", + stagingId: stage.id, }); - } catch (error) { - if (input.sessionRunId !== undefined) { - let recordedBackups: readonly CreatedSandboxBackupWrite["backup"][]; - - try { - recordedBackups = await listReadySandboxBackupsForSessionRun(bindings.DB, { - sandboxId: input.sandboxId, - sessionRunId: input.sessionRunId, - }); - } catch { - // An ambiguous database failure must not delete objects that may already - // back a committed marker. Unreferenced objects are safer than a ready - // row whose Cloudflare backup was deleted. - throw new RuntimeSubjectCheckpointFailedError({ - cause: error, - runtimeSubjectId: input.sandboxId, - }); - } - - const recordedIds = new Set(recordedBackups.map((backup) => backup.id)); - const recordedDirs = new Set(recordedBackups.map((backup) => backup.dir)); - const checkpointRecorded = input.backups.every((entry) => recordedDirs.has(entry.backup.dir)); - const orphanedBackupIds = input.backups - .map((entry) => entry.backup.id) - .filter((backupId) => !recordedIds.has(backupId)); - - try { - await deleteSandboxBackupObjects(bindings, orphanedBackupIds); - } catch (cleanupError) { - if (!checkpointRecorded) { - throw new RuntimeSubjectCheckpointFailedError({ - cause: cleanupError, - runtimeSubjectId: input.sandboxId, - }); - } - - logWarn("runtime.sandbox_checkpoint.orphan_cleanup_failed", { - backupCount: orphanedBackupIds.length, - error: cleanupError instanceof Error ? cleanupError.message : String(cleanupError), - sandboxId: input.sandboxId, - sessionRunId: input.sessionRunId, - }); - } - - if (checkpointRecorded) { - return; - } - - throw new RuntimeSubjectCheckpointFailedError({ - cause: error, - runtimeSubjectId: input.sandboxId, - }); - } - - await deleteSandboxBackupObjects( - bindings, - input.backups.map((entry) => entry.backup.id), - ); - throw new RuntimeSubjectCheckpointFailedError({ - cause: error, - runtimeSubjectId: input.sandboxId, + throw new Error("Sandbox backup finalization lost its durable stage."); + } + if (!finalized.complete) { + throw new Error("Sandbox backup finalization did not commit every required relation."); + } + if (!finalized.candidateAccepted) { + await deleteSandboxBackup(bindings, actualId, { + kind: "runtime_candidate", + stagingId: stage.id, }); } } @@ -222,73 +231,80 @@ async function recordCreatedCheckpointBackups( async function createSandboxCheckpointBackups( bindings: ApiBindings, input: { - readonly operationId?: string | null; + readonly admission: SandboxBackupAdmission; readonly requiredSessionId?: string; readonly rules: readonly RuntimeCheckpointRule[]; readonly sandboxId: string; - readonly sessionRunId?: string; }, ): Promise { - let targets = await listSandboxCheckpointBackupTargets(bindings.DB, input); - + let targets = await listCheckpointTargets(bindings.DB, { + ...input, + sandboxIncarnation: + input.admission.kind === "operation" + ? input.admission.lease.incarnation + : input.admission.incarnation, + }); if ( input.requiredSessionId !== undefined && - !targets.some((target) => target.sessionId === input.requiredSessionId) + !targets.some((target) => target.workspaceSessionId === input.requiredSessionId) ) { throw new RuntimeSubjectCheckpointFailedError({ - cause: new Error( - `Session ${input.requiredSessionId} has no eligible workspace checkpoint target.`, - ), + cause: new Error(`Session ${input.requiredSessionId} has no eligible checkpoint target.`), runtimeSubjectId: input.sandboxId, }); } - if (targets.length === 0) { return; } - - if (input.sessionRunId !== undefined) { + if (input.admission.kind === "terminal") { const readyDirs = new Set( ( await listReadySandboxBackupsForSessionRun(bindings.DB, { sandboxId: input.sandboxId, - sessionRunId: input.sessionRunId, + sessionRunId: input.admission.sessionRunId, }) ).map((backup) => backup.dir), ); targets = targets.filter((target) => !readyDirs.has(target.dir)); - if (targets.length === 0) { return; } } - const backups = await createSandboxBackupsForTargets(bindings, { + const writes = await stageSandboxBackupWrites(bindings.DB, { + admission: input.admission, sandboxId: input.sandboxId, targets, + ttlSeconds: BACKUP_TTL_SECONDS, }); - - await recordCreatedCheckpointBackups(bindings, { - backups, - ...(input.requiredSessionId === undefined - ? {} - : { checkpointSessionId: input.requiredSessionId }), - ...(input.operationId === undefined ? {} : { operationId: input.operationId }), - sandboxId: input.sandboxId, - ...(input.sessionRunId === undefined ? {} : { sessionRunId: input.sessionRunId }), - }); + for (const write of writes) { + if (write.kind === "finalized") { + if (write.backup.status !== "ready") { + throw new Error("A finalized sandbox checkpoint is no longer restorable."); + } + continue; + } + try { + await createAndFinalizeStage(bindings, write.stage); + } catch (cause) { + throw new RuntimeSubjectCheckpointFailedError({ + cause, + dir: write.stage.dir, + runtimeSubjectId: input.sandboxId, + }); + } + } try { await pruneSandboxBackups(bindings, input.sandboxId); } catch (error) { - if (input.sessionRunId === undefined) { + if (input.admission.kind === "operation") { throw error; } - logWarn("runtime.sandbox_checkpoint.prune_failed", { error: error instanceof Error ? error.message : String(error), sandboxId: input.sandboxId, - sessionRunId: input.sessionRunId, + sessionRunId: input.admission.sessionRunId, }); } } @@ -296,24 +312,78 @@ async function createSandboxCheckpointBackups( export async function createSandboxCheckpoints( bindings: ApiBindings, input: { - operationId?: string | null; - requiredSessionId?: string; - rules: readonly RuntimeCheckpointRule[]; - sandboxId: string; - sessionRunId?: string; + readonly operationLease?: RuntimeSubjectOperationLease; + readonly requiredSessionId?: string; + readonly rules: readonly RuntimeCheckpointRule[]; + readonly sandboxId: string; + readonly terminalAuthority?: TerminalSandboxBackupAuthority; }, ): Promise { - await createSandboxCheckpointBackups(bindings, input); + if ((input.operationLease === undefined) === (input.terminalAuthority === undefined)) { + throw new Error("Sandbox checkpoints require exactly one durable admission authority."); + } + const admission: SandboxBackupAdmission = + input.operationLease === undefined + ? { ...input.terminalAuthority!, kind: "terminal" } + : { kind: "operation", lease: input.operationLease }; + await createSandboxCheckpointBackups(bindings, { ...input, admission }); +} + +async function repairSandboxBackupStage( + bindings: ApiBindings, + stage: SandboxBackupStage, +): Promise { + if (!(await isSandboxBackupStageCurrent(bindings.DB, stage.id))) { + const revoked = await revokeSandboxBackupStage(bindings.DB, { + onlyIfStale: true, + stagingId: stage.id, + }); + if (revoked?.actualBackupId !== null && revoked?.actualBackupId !== undefined) { + await deleteSandboxBackup(bindings, revoked.actualBackupId, { + kind: "runtime_candidate", + stagingId: stage.id, + }); + } + return; + } + await createAndFinalizeStage(bindings, stage); } -export async function deleteSandboxBackupsForDir( +export async function repairStagedSandboxBackups( + bindings: ApiBindings, + limit: number, +): Promise { + const stages = await listSandboxBackupStages(bindings.DB, limit); + for (const stage of stages) { + try { + await repairSandboxBackupStage(bindings, stage); + } catch (error) { + await deferSandboxBackupStageRepair(bindings.DB, stage); + logWarn("runtime.sandbox_checkpoint.repair_failed", { + error: error instanceof Error ? error.message : String(error), + stagingId: stage.id, + }); + } + } + return stages.length; +} + +export async function deleteSandboxBackupsForSession( bindings: ApiBindings, input: { - dir: string; + readonly cwd: string; + readonly operationId: string; + readonly sandboxId: string; + readonly sessionId: string; }, ): Promise { - const backupIds = await listSandboxBackupIdsByDir(bindings.DB, input.dir); - - await deleteSandboxBackupObjects(bindings, backupIds); - await deleteSandboxBackupRecordsForDir(bindings.DB, input.dir); + const backupIds = await revokeSandboxBackupsForSessionDelete(bindings.DB, { + cwd: input.cwd, + operationId: parsePlatformId(input.operationId, "session delete operation"), + sandboxId: parsePlatformId(input.sandboxId, "sandbox id"), + sessionId: parsePlatformId(input.sessionId, "session id"), + }); + for (const backupId of backupIds) { + await deleteSandboxBackup(bindings, backupId, { kind: "unattributed" }); + } } diff --git a/apps/api/src/modules/runtime/infrastructure/sandbox-file-bytes.ts b/apps/api/src/modules/runtime/infrastructure/sandbox-file-bytes.ts index 91ea5cc0..eb6b5c24 100644 --- a/apps/api/src/modules/runtime/infrastructure/sandbox-file-bytes.ts +++ b/apps/api/src/modules/runtime/infrastructure/sandbox-file-bytes.ts @@ -1,49 +1,43 @@ +import { fromBase64, toBase64 } from "../../../shared/bytes"; +import { quoteShellArg } from "../../../shared/shell"; import type { ExecutionSessionHandle } from "./sandbox-handles"; -function decodeBase64(value: string): Uint8Array { - if (value.length === 0) { - return new Uint8Array(); - } - - const binary = atob(value); - const bytes = new Uint8Array(binary.length); - - for (let index = 0; index < binary.length; index += 1) { - bytes[index] = binary.codePointAt(index) ?? 0; - } - - return bytes; -} - export async function readSandboxFileBytes( handle: ExecutionSessionHandle, path: string, + maxBytes?: number, ): Promise { + if (maxBytes !== undefined) { + if (!Number.isSafeInteger(maxBytes) || maxBytes < 0) { + throw new Error("Sandbox file byte limit is invalid."); + } + const command = [ + "runtime_file_read=$(mktemp)", + "trap 'rm -f \"$runtime_file_read\"' EXIT", + `head -c ${maxBytes + 1} -- ${quoteShellArg(path)} > "$runtime_file_read"`, + 'base64 -w 0 "$runtime_file_read"', + ].join(" && "); + const result = await handle.exec(`sh -lc ${quoteShellArg(command)}`); + if (!result.success || result.exitCode !== 0) { + throw new Error( + result.stderr.trim() || result.stdout.trim() || `Failed to read sandbox file ${path}.`, + ); + } + return fromBase64(result.stdout); + } const file = await handle.readFile(path, { encoding: "base64" }); if (file.encoding === "base64") { - return decodeBase64(file.content); + return fromBase64(file.content); } return new TextEncoder().encode(file.content); } -function encodeBase64(bytes: Uint8Array): string { - let binary = ""; - // Chunked conversion keeps String.fromCharCode off argument-count limits. - const chunkSize = 0x8000; - - for (let index = 0; index < bytes.length; index += chunkSize) { - binary += String.fromCharCode(...bytes.subarray(index, index + chunkSize)); - } - - return btoa(binary); -} - export async function writeSandboxFileBytes( handle: ExecutionSessionHandle, path: string, bytes: Uint8Array, ): Promise { - await handle.writeFile(path, encodeBase64(bytes), { encoding: "base64" }); + await handle.writeFile(path, toBase64(bytes), { encoding: "base64" }); } diff --git a/apps/api/src/modules/runtime/infrastructure/sandbox-handles.ts b/apps/api/src/modules/runtime/infrastructure/sandbox-handles.ts index 56ee685b..dc256eef 100644 --- a/apps/api/src/modules/runtime/infrastructure/sandbox-handles.ts +++ b/apps/api/src/modules/runtime/infrastructure/sandbox-handles.ts @@ -71,7 +71,9 @@ export interface SandboxHandle extends ExecutionSessionHandle { configureNetworkConstraints(constraints: SandboxNetworkConstraints): Promise; createBackup(options: { dir: string; + excludes?: string[]; localBucket?: boolean; + name?: string; ttl?: number; }): Promise<{ dir: string; id: string }>; createSession(options?: { @@ -100,6 +102,33 @@ export interface SandboxHandle extends ExecutionSessionHandle { wsConnect(request: Request, port: number): Promise; } +export interface RuntimeSubjectIncarnationHandle { + activateRuntimeSubjectIncarnation( + incarnation: number, + networkConstraintsHash: string, + ): Promise; + createRuntimeSubjectBackup( + incarnation: number, + options: { + dir: string; + excludes?: string[]; + forbiddenPaths?: string[]; + localBucket?: boolean; + name: string; + ttl?: number; + }, + ): Promise<{ dir: string; id: string }>; + destroyRuntimeSubjectIncarnation(incarnation: number): Promise<{ kind: "destroyed" | "stale" }>; + inspectRuntimeSubjectIncarnation( + incarnation: number, + networkConstraintsHash: string, + ): Promise<{ kind: "healthy" | "missing" | "retired" | "stale" | "unknown" }>; + markRuntimeSubjectIncarnationReady( + incarnation: number, + networkConstraintsHash: string, + ): Promise; +} + const SANDBOX_HANDLE_METHODS = [ "configureNetworkConstraints", "createBackup", @@ -134,3 +163,23 @@ export function toSandboxHandle(value: unknown): SandboxHandle { return value as SandboxHandle; } + +export function toRuntimeSubjectIncarnationHandle(value: unknown): RuntimeSubjectIncarnationHandle { + if (typeof value !== "object" || value === null) { + throw new Error("Cloudflare Sandbox incarnation handle is not an object."); + } + + for (const method of [ + "activateRuntimeSubjectIncarnation", + "createRuntimeSubjectBackup", + "destroyRuntimeSubjectIncarnation", + "inspectRuntimeSubjectIncarnation", + "markRuntimeSubjectIncarnationReady", + ] as const) { + if (typeof Reflect.get(value, method) !== "function") { + throw new TypeError(`Cloudflare Sandbox incarnation handle is missing ${method}.`); + } + } + + return value as RuntimeSubjectIncarnationHandle; +} diff --git a/apps/api/src/modules/runtime/infrastructure/sandbox-session.service.ts b/apps/api/src/modules/runtime/infrastructure/sandbox-session.service.ts index a65aa899..c5dd0552 100644 --- a/apps/api/src/modules/runtime/infrastructure/sandbox-session.service.ts +++ b/apps/api/src/modules/runtime/infrastructure/sandbox-session.service.ts @@ -2,4 +2,5 @@ export { closeIdleCattleConversationSession, closeSandboxConversationSession, ensureSandboxConversationSession, + repairPendingSandboxConversationSessionCleanups, } from "./sandbox-session/sandbox-conversation-session.service"; diff --git a/apps/api/src/modules/runtime/infrastructure/sandbox-session/sandbox-conversation-session-delete.ts b/apps/api/src/modules/runtime/infrastructure/sandbox-session/sandbox-conversation-session-delete.ts index c1b1aae1..0be8a4d7 100644 --- a/apps/api/src/modules/runtime/infrastructure/sandbox-session/sandbox-conversation-session-delete.ts +++ b/apps/api/src/modules/runtime/infrastructure/sandbox-session/sandbox-conversation-session-delete.ts @@ -5,28 +5,46 @@ import { withDisposedRpcResult, } from "../../../../platform/cloudflare/rpc-disposal"; import type { ApiBindings } from "../../../../platform/cloudflare/worker-types"; +import { withRuntimeProvisionTimeout } from "../runtime-provision-timeout"; import { getRuntimeSubjectKeepAliveHandle } from "../runtime-subject-lifecycle/runtime-subject-platform"; +function isMissingSandboxSession(error: unknown): boolean { + return ( + error instanceof Error && + (error.name === "SessionNotFoundError" || + error.message.includes("SessionNotFoundError") || + (error.message.includes("Session '") && error.message.includes("' not found"))) + ); +} + export async function deleteActiveSandboxConversationSession( bindings: ApiBindings, input: { readonly sandboxSessionId: SandboxSessionId; readonly sandboxId: SandboxId; + readonly sandboxIncarnation: number; }, ): Promise { - await withDisposedRpcResource( - await getRuntimeSubjectKeepAliveHandle(bindings, input.sandboxId), - async (sandbox) => { - const deleted = await withDisposedRpcResult( - sandbox.deleteSession(input.sandboxSessionId), - (deletedSession) => ({ - success: deletedSession.success, - }), - ); + try { + await withDisposedRpcResource( + await getRuntimeSubjectKeepAliveHandle(bindings, input.sandboxId, input.sandboxIncarnation), + async (sandbox) => { + const deleted = await withDisposedRpcResult( + withRuntimeProvisionTimeout( + sandbox.deleteSession(input.sandboxSessionId), + `Sandbox session deletion for ${input.sandboxSessionId}`, + ), + (deletedSession) => ({ success: deletedSession.success }), + ); - if (!deleted.success) { - throw new Error(`Sandbox session ${input.sandboxSessionId} could not be deleted.`); - } - }, - ); + if (!deleted.success) { + throw new Error(`Sandbox session ${input.sandboxSessionId} could not be deleted.`); + } + }, + ); + } catch (error) { + if (!isMissingSandboxSession(error)) { + throw error; + } + } } diff --git a/apps/api/src/modules/runtime/infrastructure/sandbox-session/sandbox-conversation-session-platform.ts b/apps/api/src/modules/runtime/infrastructure/sandbox-session/sandbox-conversation-session-platform.ts index 0c030ae9..ff2477d6 100644 --- a/apps/api/src/modules/runtime/infrastructure/sandbox-session/sandbox-conversation-session-platform.ts +++ b/apps/api/src/modules/runtime/infrastructure/sandbox-session/sandbox-conversation-session-platform.ts @@ -2,6 +2,7 @@ import { discardPromiseResult } from "@mosoo/effects"; import type { SandboxBackupId, SandboxSessionId } from "@mosoo/id"; import { withDisposedRpcResult } from "../../../../platform/cloudflare/rpc-disposal"; +import { quoteShellArg } from "../../../../shared/shell"; import { getRuntimeSessionOutputDirectory } from "../driver-instance/runtime-session-outputs"; import { withRuntimeProvisionTimeout } from "../runtime-provision-timeout"; import { decodeSandboxBackupIdForPlatform } from "../sandbox-backup-id"; @@ -12,10 +13,6 @@ interface SandboxConversationDirectoryBackup { readonly id: SandboxBackupId; } -function quoteShellArg(value: string): string { - return `'${value.replaceAll("'", `'"'"'`)}'`; -} - function isSessionAlreadyExistsError(error: unknown): boolean { return ( error instanceof Error && @@ -63,8 +60,14 @@ export async function prepareSandboxConversationDirectories(input: { readonly cwd: string; readonly sandbox: SandboxHandle; }): Promise { - await input.sandbox.mkdir(input.cwd, { recursive: true }); - await input.sandbox.mkdir(getRuntimeSessionOutputDirectory(input.cwd), { recursive: true }); + await withRuntimeProvisionTimeout( + input.sandbox.mkdir(input.cwd, { recursive: true }), + `Sandbox session cwd creation for ${input.cwd}`, + ); + await withRuntimeProvisionTimeout( + input.sandbox.mkdir(getRuntimeSessionOutputDirectory(input.cwd), { recursive: true }), + `Sandbox session output directory creation for ${input.cwd}`, + ); } export async function deleteSandboxConversationSessionBestEffort(input: { @@ -72,7 +75,10 @@ export async function deleteSandboxConversationSessionBestEffort(input: { readonly sandbox: SandboxHandle; }): Promise { try { - await input.sandbox.deleteSession(input.sandboxSessionId); + await withRuntimeProvisionTimeout( + input.sandbox.deleteSession(input.sandboxSessionId), + `Sandbox session deletion for ${input.sandboxSessionId}`, + ); } catch { // Best-effort cleanup for a partially configured session. } @@ -88,10 +94,13 @@ export async function openSandboxConversationSession(input: { try { return { created: true, - session: await input.sandbox.createSession({ - cwd: input.cwd, - id: input.sandboxSessionId, - }), + session: await withRuntimeProvisionTimeout( + input.sandbox.createSession({ + cwd: input.cwd, + id: input.sandboxSessionId, + }), + `Sandbox session creation for ${input.sandboxSessionId}`, + ), }; } catch (error) { if (!isSessionAlreadyExistsError(error)) { @@ -100,13 +109,19 @@ export async function openSandboxConversationSession(input: { return { created: false, - session: await input.sandbox.getSession(input.sandboxSessionId), + session: await withRuntimeProvisionTimeout( + input.sandbox.getSession(input.sandboxSessionId), + `Sandbox session lookup for ${input.sandboxSessionId}`, + ), }; } } return { created: false, - session: await input.sandbox.getSession(input.sandboxSessionId), + session: await withRuntimeProvisionTimeout( + input.sandbox.getSession(input.sandboxSessionId), + `Sandbox session lookup for ${input.sandboxSessionId}`, + ), }; } diff --git a/apps/api/src/modules/runtime/infrastructure/sandbox-session/sandbox-conversation-session.service.ts b/apps/api/src/modules/runtime/infrastructure/sandbox-session/sandbox-conversation-session.service.ts index 1fef7bc5..45bcc9f8 100644 --- a/apps/api/src/modules/runtime/infrastructure/sandbox-session/sandbox-conversation-session.service.ts +++ b/apps/api/src/modules/runtime/infrastructure/sandbox-session/sandbox-conversation-session.service.ts @@ -1,6 +1,6 @@ import { getSessionOrganizationPath } from "@mosoo/agent-driver/paths"; import { createPlatformId } from "@mosoo/id"; -import type { SandboxId, SandboxSessionId, SessionId } from "@mosoo/id"; +import type { RuntimeOperationId, SandboxId, SandboxSessionId, SessionId } from "@mosoo/id"; import { RUNTIME_DIAGNOSTIC_EVENT } from "@mosoo/runtime-events"; import { disposeRpcResource } from "../../../../platform/cloudflare/rpc-disposal"; @@ -15,12 +15,18 @@ import { getRuntimeSubjectInactiveDeadline, runtimeCheckpointRulesInclude, } from "../../domain/runtime-kind-policy"; +import { + heartbeatRuntimeRunProvisioningLease, + recordRuntimeProvisioningConversationTarget, +} from "../runtime-subject-lifecycle/runtime-provisioning-lease-store"; import type { RuntimeConversationSessionRecord } from "../runtime-subject-lifecycle/runtime-subject-store"; import { + claimRuntimeConversationSessionCleanup, claimIdleSessionScopedConversationForClose, ensureRuntimeConversationSessionRecord, getRuntimeConversationSession, getRuntimeConversationSessionState, + listPendingRuntimeConversationSessionCleanups, recordRuntimeConversationSessionActive, recordRuntimeConversationSessionClosed, recordRuntimeConversationSessionError, @@ -52,6 +58,7 @@ function measureOptional( function resolveConversationContinuationPlan(input: { existingSession: RuntimeConversationSessionRecord | null; kind: EnsureSandboxConversationSessionInput["kind"]; + replaceClosedExecutionSession: boolean; }): { sandboxSessionId?: SandboxSessionId; requireCwdCheckpoint: boolean; @@ -93,12 +100,17 @@ function resolveConversationContinuationPlan(input: { }; } + if (input.existingSession.status === "cleanup_pending") { + throw new Error("Sandbox conversation cleanup is still pending; retry after maintenance."); + } + const shouldRestoreCwd = runtimeCheckpointRulesInclude( policy.checkpoint.restoreOnActivate, "session_workspaces", ); const shouldUseNewCloudflareSession = - input.existingSession.status === "closed" && policy.subject.scope === "session"; + input.existingSession.status === "closed" && + (policy.subject.scope === "session" || input.replaceClosedExecutionSession); return { ...(shouldUseNewCloudflareSession @@ -160,12 +172,20 @@ export async function ensureSandboxConversationSession( const continuation = resolveConversationContinuationPlan({ existingSession, kind: input.kind, + replaceClosedExecutionSession: input.replaceClosedExecutionSession ?? false, }); const cwd = existingSession?.cwd ?? getSessionOrganizationPath(input.sessionId); if (existingSession && existingSession.sandboxId !== input.sandboxId) { throw new Error("Sandbox session is already bound to a different sandbox."); } + if ( + existingSession && + existingSession.status !== "closed" && + existingSession.sandboxIncarnation !== input.sandboxIncarnation + ) { + throw new Error("Sandbox session belongs to a retired sandbox incarnation."); + } const frozenOrigin = existingSession ? parseSandboxConversationOrigin(existingSession.originJson) @@ -181,13 +201,44 @@ export async function ensureSandboxConversationSession( now, originJson: JSON.stringify(frozenOrigin), runtimeSubjectId: input.sandboxId, + sandboxIncarnation: input.sandboxIncarnation, sessionId: input.sessionId, }), )); const sandboxSessionId = continuation.sandboxSessionId ?? sessionRecord.sandboxSessionId; + let provisioningLease = input.provisioningLease; + + if (provisioningLease !== undefined) { + const recordedLease = await recordRuntimeProvisioningConversationTarget(bindings.DB, { + lease: provisioningLease, + sandboxIncarnation: input.sandboxIncarnation, + sandboxSessionId, + }); + if (recordedLease === null) { + throw new Error("Sandbox conversation provisioning lost lifecycle ownership."); + } + provisioningLease = recordedLease; + } + + const measureProvisioning = async (name: string, task: () => Promise): Promise => { + if ( + provisioningLease !== undefined && + !(await heartbeatRuntimeRunProvisioningLease(bindings.DB, provisioningLease)) + ) { + throw new Error("Sandbox conversation provisioning lost lifecycle ownership."); + } + const result = await measureOptional(input.timing, name, task); + if ( + provisioningLease !== undefined && + !(await heartbeatRuntimeRunProvisioningLease(bindings.DB, provisioningLease)) + ) { + throw new Error("Sandbox conversation provisioning lost lifecycle ownership."); + } + return result; + }; if (continuation.shouldRestoreCwd && existingSession) { - await measureOptional(input.timing, "conversation.restoreCwd", () => + await measureProvisioning("conversation.restoreCwd", () => restoreSandboxSessionCwdIfMissing({ cwd, latestReadyBackup: existingSession.latestReadyBackup, @@ -199,7 +250,7 @@ export async function ensureSandboxConversationSession( } if (continuation.shouldCreateCloudflareSession) { - await measureOptional(input.timing, "conversation.prepareDirectories", () => + await measureProvisioning("conversation.prepareDirectories", () => prepareSandboxConversationDirectories({ cwd, sandbox: input.sandbox, @@ -207,7 +258,7 @@ export async function ensureSandboxConversationSession( ); if (continuation.shouldRestoreSessionArtifacts) { - await measureOptional(input.timing, "conversation.restoreSessionArtifacts", () => + await measureProvisioning("conversation.restoreSessionArtifacts", () => restoreSessionArtifactsToWorkspace(bindings, { agentId: input.agentId, cwd, @@ -220,7 +271,7 @@ export async function ensureSandboxConversationSession( } if (input.mountSessionResources) { - await measureOptional(input.timing, "conversation.mountResources", () => + await measureProvisioning("conversation.mountResources", () => ensureSessionResourcesMounted({ bindings, sandbox: input.sandbox, @@ -230,7 +281,7 @@ export async function ensureSandboxConversationSession( } if (continuation.shouldDeleteErrorSession) { - await measureOptional(input.timing, "conversation.deleteErrorSession", () => + await measureProvisioning("conversation.deleteErrorSession", () => deleteSandboxConversationSessionBestEffort({ sandboxSessionId: sessionRecord.sandboxSessionId, sandbox: input.sandbox, @@ -238,46 +289,61 @@ export async function ensureSandboxConversationSession( ); } - const openedCloudflareSession = await measureOptional( - input.timing, - "conversation.openSession", - () => - openSandboxConversationSession({ - sandboxSessionId, - cwd, - sandbox: input.sandbox, - shouldCreate: continuation.shouldCreateCloudflareSession, - }), + const openedCloudflareSession = await measureProvisioning("conversation.openSession", () => + openSandboxConversationSession({ + sandboxSessionId, + cwd, + sandbox: input.sandbox, + shouldCreate: continuation.shouldCreateCloudflareSession, + }), ); const cloudflareSession = openedCloudflareSession.session; + const activatedAt = currentTimestampMs(); try { - await measureOptional(input.timing, "conversation.activateRecord", () => + const activated = await measureProvisioning("conversation.activateRecord", () => recordRuntimeConversationSessionActive(bindings.DB, { sandboxSessionId, cwd, - now, + ...(provisioningLease === undefined + ? {} + : { expectedProvisioningOperationId: provisioningLease.operationId }), + now: activatedAt, originJson: JSON.stringify(frozenOrigin), runtimeSubjectId: input.sandboxId, + sandboxIncarnation: input.sandboxIncarnation, sessionId: input.sessionId, }), ); + if (!activated) { + throw new Error("Sandbox conversation activation lost lifecycle ownership."); + } } catch (error) { const message = error instanceof Error ? error.message : "Sandbox conversation session activation failed."; - await recordRuntimeConversationSessionError(bindings.DB, { + const recordedError = await recordRuntimeConversationSessionError(bindings.DB, { sandboxSessionId, + sandboxIncarnation: input.sandboxIncarnation, cwd, errorCode: "runtime.conversation_mount_failed", + ...(provisioningLease === undefined + ? {} + : { expectedProvisioningOperationId: provisioningLease.operationId }), message, - now, + now: activatedAt, originJson: JSON.stringify(frozenOrigin), runtimeSubjectId: input.sandboxId, sessionId: input.sessionId, }); disposeRpcResource(cloudflareSession); + if (!recordedError) { + await deleteSandboxConversationSessionBestEffort({ + sandbox: input.sandbox, + sandboxSessionId, + }); + } throw new Error(message, { cause: error }); } @@ -286,29 +352,68 @@ export async function ensureSandboxConversationSession( sandboxSessionId, cwd, origin: frozenOrigin, + ...(provisioningLease === undefined ? {} : { provisioningLease }), }; } export async function closeSandboxConversationSession( bindings: ApiBindings, input: { + expectedProvisioningOperationId?: RuntimeOperationId; + expectedSandboxSessionId?: SandboxSessionId; sandboxId: SandboxId; sessionId: SessionId; }, ): Promise { const state = await getRuntimeConversationSessionState(bindings.DB, { + ...(input.expectedProvisioningOperationId === undefined + ? {} + : { expectedProvisioningOperationId: input.expectedProvisioningOperationId }), + ...(input.expectedSandboxSessionId === undefined + ? {} + : { expectedSandboxSessionId: input.expectedSandboxSessionId }), runtimeSubjectId: input.sandboxId, sessionId: input.sessionId, }); - if (!state || state.status !== "active") { + if (!state) { return; } + let cleanupOperationId = + state.status === "cleanup_pending" + ? state.cleanupOperationId + : await claimRuntimeConversationSessionCleanup(bindings.DB, { + ...(input.expectedProvisioningOperationId === undefined + ? {} + : { expectedProvisioningOperationId: input.expectedProvisioningOperationId }), + now: currentTimestampMs(), + runtimeSubjectId: input.sandboxId, + sandboxIncarnation: state.sandboxIncarnation, + sandboxSessionId: state.sandboxSessionId, + sessionId: input.sessionId, + }); + if (cleanupOperationId === null) { + const adopted = await getRuntimeConversationSessionState(bindings.DB, { + expectedSandboxIncarnation: state.sandboxIncarnation, + expectedSandboxSessionId: state.sandboxSessionId, + runtimeSubjectId: input.sandboxId, + sessionId: input.sessionId, + }); + if (adopted?.status !== "cleanup_pending" || adopted.cleanupOperationId === null) { + throw new Error("Sandbox conversation cleanup lost lifecycle ownership."); + } + cleanupOperationId = adopted.cleanupOperationId; + } + // Force-close: session-end / cleanup callers must tear down regardless of // idleness. The idle sweep uses closeIdleCattleConversationSession instead. await finalizeSandboxConversationClose(bindings, { sandboxId: input.sandboxId, + cleanupOperationId, + ...(input.expectedProvisioningOperationId === undefined + ? {} + : { expectedProvisioningOperationId: input.expectedProvisioningOperationId }), sessionId: input.sessionId, state, }); @@ -337,20 +442,22 @@ export async function closeIdleCattleConversationSession( return false; } - const claimed = await claimIdleSessionScopedConversationForClose(bindings.DB, { + const cleanupOperationId = await claimIdleSessionScopedConversationForClose(bindings.DB, { idleSinceLte: input.idleSinceLte, now: currentTimestampMs(), runtimeSubjectId: input.sandboxId, + sandboxIncarnation: state.sandboxIncarnation, sandboxSessionId: state.sandboxSessionId, sessionId: input.sessionId, }); - if (!claimed) { + if (cleanupOperationId === null) { return false; } await finalizeSandboxConversationClose(bindings, { sandboxId: input.sandboxId, + cleanupOperationId, sessionId: input.sessionId, state, }); @@ -358,10 +465,32 @@ export async function closeIdleCattleConversationSession( return true; } +export async function repairPendingSandboxConversationSessionCleanups( + bindings: ApiBindings, + limit: number, +): Promise { + const pending = await listPendingRuntimeConversationSessionCleanups(bindings.DB, limit); + + await Promise.allSettled( + pending.map((cleanup) => + finalizeSandboxConversationClose(bindings, { + cleanupOperationId: cleanup.cleanupOperationId, + sandboxId: cleanup.sandboxId, + sessionId: cleanup.sessionId, + state: cleanup, + }), + ), + ); + + return pending.length; +} + async function finalizeSandboxConversationClose( bindings: ApiBindings, input: { sandboxId: SandboxId; + cleanupOperationId: RuntimeOperationId; + expectedProvisioningOperationId?: RuntimeOperationId; sessionId: SessionId; state: RuntimeConversationSessionState; }, @@ -370,36 +499,43 @@ async function finalizeSandboxConversationClose( const { deleteActiveSandboxConversationSession } = await import("./sandbox-conversation-session-delete"); - try { - await deleteActiveSandboxConversationSession(bindings, { - sandboxSessionId: input.state.sandboxSessionId, - sandboxId: input.sandboxId, - }); + await deleteActiveSandboxConversationSession(bindings, { + sandboxSessionId: input.state.sandboxSessionId, + sandboxId: input.sandboxId, + sandboxIncarnation: input.state.sandboxIncarnation, + }); - if (input.state.agentId) { - await appendRuntimeDiagnosticEvent(bindings, { - eventName: RUNTIME_DIAGNOSTIC_EVENT.sandboxSessionDestroyed.name, - sessionId: input.sessionId, - value: { - ...toRuntimeDiagnosticBaseValue({ - agentId: input.state.agentId, - sessionId: input.sessionId, - }), - reason: "runtime_subject_session_closed", - sandboxId: input.sandboxId, - }, - }); - } - } finally { - // Remote cleanup must not strand the local subject outside reclamation. - await recordRuntimeConversationSessionClosed(bindings.DB, { - inactiveDeadlineAt: getRuntimeSubjectInactiveDeadline( - getRuntimeKindPolicy(input.state.kind), - now, - ), + const recorded = await recordRuntimeConversationSessionClosed(bindings.DB, { + cleanupOperationId: input.cleanupOperationId, + ...(input.expectedProvisioningOperationId === undefined + ? {} + : { expectedProvisioningOperationId: input.expectedProvisioningOperationId }), + inactiveDeadlineAt: getRuntimeSubjectInactiveDeadline( + getRuntimeKindPolicy(input.state.kind), now, - runtimeSubjectId: input.sandboxId, + ), + now, + runtimeSubjectId: input.sandboxId, + sandboxIncarnation: input.state.sandboxIncarnation, + sandboxSessionId: input.state.sandboxSessionId, + sessionId: input.sessionId, + }); + if (!recorded) { + throw new Error("Sandbox conversation cleanup lost lifecycle ownership."); + } + + if (input.state.agentId) { + await appendRuntimeDiagnosticEvent(bindings, { + eventName: RUNTIME_DIAGNOSTIC_EVENT.sandboxSessionDestroyed.name, sessionId: input.sessionId, + value: { + ...toRuntimeDiagnosticBaseValue({ + agentId: input.state.agentId, + sessionId: input.sessionId, + }), + reason: "runtime_subject_session_closed", + sandboxId: input.sandboxId, + }, }); } } diff --git a/apps/api/src/modules/runtime/infrastructure/sandbox-session/sandbox-session.types.ts b/apps/api/src/modules/runtime/infrastructure/sandbox-session/sandbox-session.types.ts index 0c5fd2b3..9641a9dc 100644 --- a/apps/api/src/modules/runtime/infrastructure/sandbox-session/sandbox-session.types.ts +++ b/apps/api/src/modules/runtime/infrastructure/sandbox-session/sandbox-session.types.ts @@ -3,6 +3,7 @@ import type { AgentId, SandboxId, SandboxSessionId, SessionId } from "@mosoo/id" import type { RuntimeTimingRecorder } from "../../application/session-runs/session-runtime-timing"; import type { DriverOrigin as DriverOriginValue } from "../../domain/driver-snapshot"; +import type { RuntimeRunProvisioningLease } from "../runtime-subject-lifecycle/runtime-provisioning-lease-store"; import type { ExecutionSessionHandle, SandboxHandle } from "../sandbox-handles"; export interface EnsureSandboxConversationSessionInput { @@ -10,8 +11,11 @@ export interface EnsureSandboxConversationSessionInput { kind: AgentKind; mountSessionResources: boolean; origin: DriverOriginValue; + provisioningLease?: RuntimeRunProvisioningLease; + replaceClosedExecutionSession?: boolean; sandbox: SandboxHandle; sandboxId: SandboxId; + sandboxIncarnation: number; sessionId: SessionId; timing?: RuntimeTimingRecorder; } @@ -21,4 +25,5 @@ export interface SandboxConversationSessionResult { sandboxSessionId: SandboxSessionId; cwd: string; origin: DriverOriginValue; + provisioningLease?: RuntimeRunProvisioningLease; } diff --git a/apps/api/src/modules/runtime/infrastructure/session-resources/session-resource-mount.service.ts b/apps/api/src/modules/runtime/infrastructure/session-resources/session-resource-mount.service.ts index 37e5e886..8c7f7d7d 100644 --- a/apps/api/src/modules/runtime/infrastructure/session-resources/session-resource-mount.service.ts +++ b/apps/api/src/modules/runtime/infrastructure/session-resources/session-resource-mount.service.ts @@ -1,6 +1,11 @@ -import { getSessionResourceRootPath } from "@mosoo/agent-driver/paths"; +import { + SANDBOX_WORKSPACE_ROOT, + getSessionResourceBackingPath, + getSessionResourceRootPath, +} from "@mosoo/agent-driver/paths"; import type { ApiBindings } from "../../../../platform/cloudflare/worker-types"; +import { quoteShellArg } from "../../../../shared/shell"; import { createRuntimeSandboxBucketMountOptions, isRuntimeSandboxLocalBucketEnabled, @@ -10,12 +15,8 @@ import { toRuntimeBucketMountConflictError } from "../runtime-sandbox-mount-erro import { RuntimeBucketMountConflictError } from "../runtime-subject-lifecycle/runtime-subject-errors"; import type { SandboxHandle } from "../sandbox-handles"; -function quoteShellArg(value: string): string { - return `'${value.replaceAll("'", `'"'"'`)}'`; -} - function getSessionResourceMountPath(sessionId: string): string { - return getSessionResourceRootPath(sessionId); + return getSessionResourceBackingPath(sessionId); } function getSessionResourceBucketPrefix(sessionId: string): string { @@ -37,64 +38,82 @@ async function sandboxBucketMountIsReady(input: { return probe.success && probe.exitCode === 0; } +async function ensureSessionResourceAlias(input: { + readonly mountPath: string; + readonly publicPath: string; + readonly sandbox: SandboxHandle; +}): Promise { + const workspacePrefix = `${SANDBOX_WORKSPACE_ROOT}/`; + const publicParent = input.publicPath.slice(0, input.publicPath.lastIndexOf("/")); + if (!publicParent.startsWith(workspacePrefix) || !input.mountPath.startsWith(workspacePrefix)) { + throw new Error("Session resource paths must be inside the workspace root."); + } + const parentDepth = publicParent.slice(workspacePrefix.length).split("/").length; + const target = `${"../".repeat(parentDepth)}${input.mountPath.slice(workspacePrefix.length)}`; + const command = [ + "set -eu", + `link=${quoteShellArg(input.publicPath)}`, + `target=${quoteShellArg(target)}`, + 'if [ -L "$link" ]; then [ "$(readlink "$link")" = "$target" ];', + 'elif [ -e "$link" ]; then exit 42;', + 'else ln -s "$target" "$link" 2>/dev/null || { [ -L "$link" ] && [ "$(readlink "$link")" = "$target" ]; }; fi', + ].join("; "); + const result = await input.sandbox.exec(`sh -lc ${quoteShellArg(command)}`); + if (!result.success || result.exitCode !== 0) { + throw new Error("Session resource alias conflicts with its reserved workspace path."); + } +} + export async function ensureSessionResourcesMounted(input: { bindings: ApiBindings; sandbox: SandboxHandle; sessionId: string; }): Promise { const mountPath = getSessionResourceMountPath(input.sessionId); + const publicPath = getSessionResourceRootPath(input.sessionId); const bucket = resolveRuntimeSandboxBucketMountTarget(input.bindings); const prefix = getSessionResourceBucketPrefix(input.sessionId); const localBucket = isRuntimeSandboxLocalBucketEnabled(input.bindings); - if ( - await sandboxBucketMountIsReady({ - localBucket, - mountPath, - sandbox: input.sandbox, - }) - ) { - return; - } + const ready = await sandboxBucketMountIsReady({ + localBucket, + mountPath, + sandbox: input.sandbox, + }); - await input.sandbox.mkdir(mountPath, { recursive: true }); + if (!ready) { + await input.sandbox.mkdir(mountPath, { recursive: true }); - try { - await input.sandbox.mountBucket( - bucket, - mountPath, - createRuntimeSandboxBucketMountOptions(input.bindings, { - prefix, - readOnly: true, - }), - ); - } catch (cause) { - const error = - toRuntimeBucketMountConflictError(cause, { + try { + await input.sandbox.mountBucket( + bucket, mountPath, - }) ?? cause; - - if ( - error instanceof RuntimeBucketMountConflictError && - (localBucket || - (await sandboxBucketMountIsReady({ - localBucket, + createRuntimeSandboxBucketMountOptions(input.bindings, { + prefix, + readOnly: true, + }), + ); + } catch (cause) { + const error = + toRuntimeBucketMountConflictError(cause, { mountPath, - sandbox: input.sandbox, - }))) - ) { - return; - } + }) ?? cause; - if ( - !localBucket && - error instanceof RuntimeBucketMountConflictError && - error.bucket === bucket && - error.prefix === prefix - ) { - return; - } + const sameMount = + error instanceof RuntimeBucketMountConflictError && + (localBucket || + (await sandboxBucketMountIsReady({ + localBucket, + mountPath, + sandbox: input.sandbox, + })) || + (error.bucket === bucket && error.prefix === prefix)); - throw error; + if (!sameMount) { + throw error; + } + } } + + await ensureSessionResourceAlias({ mountPath, publicPath, sandbox: input.sandbox }); } diff --git a/apps/api/src/modules/runtime/infrastructure/session-runs/external-tool-effect-store.repository.ts b/apps/api/src/modules/runtime/infrastructure/session-runs/external-tool-effect-store.repository.ts index 17d66daa..f19bc1a1 100644 --- a/apps/api/src/modules/runtime/infrastructure/session-runs/external-tool-effect-store.repository.ts +++ b/apps/api/src/modules/runtime/infrastructure/session-runs/external-tool-effect-store.repository.ts @@ -1,10 +1,18 @@ +import { + ExternalToolEffectClaimToken, + ExternalToolEffectSettlement, +} from "@mosoo/contracts/external-tool-effect"; import type { ExternalToolEffectClaim, + ExternalToolEffectState, ExternalToolEffectStatus, } from "@mosoo/contracts/external-tool-effect"; -import { McpExecuteCommandResult } from "@mosoo/contracts/runtime-command"; +import { McpExecuteCommandResult, RuntimeCommand } from "@mosoo/contracts/runtime-command"; +import type { McpExecuteCommand } from "@mosoo/contracts/runtime-command"; import { parseSchemaValue } from "@mosoo/contracts/validation"; import { + driverCommandsTable, + driverInstancesTable, externalToolEffectAttemptsTable, externalToolEffectsTable, sessionRunsTable, @@ -17,7 +25,7 @@ import type { McpServerId, SessionRunId, } from "@mosoo/id"; -import { and, eq, inArray } from "drizzle-orm"; +import { and, eq, exists, inArray, sql } from "drizzle-orm"; import { getAppDatabase, @@ -25,37 +33,115 @@ import { runAppDatabaseBatch, } from "../../../../platform/db/drizzle"; import { currentTimestampMs } from "../../../../time"; +import { LIVE_DRIVER_INSTANCE_STATUSES } from "../../domain/driver-instance-lifecycle.machine"; import { ACTIVE_SESSION_RUN_STATUSES } from "../../domain/session-run-lifecycle.machine"; interface ExternalToolEffectRow { attemptCount: number; + claimToken: string | null; + command: McpExecuteCommand; id: ExternalToolEffectId; idempotencyKey: string; + requestId: string; resultJson: string | null; + serverId: string; status: ExternalToolEffectStatus; + toolName: string; +} + +type EffectLookup = { + commandId: DriverCommandId; + driverGeneration: number; + driverInstanceId: DriverInstanceId; +}; + +function serializeRuntimeSettlement(value: unknown): string { + const serialized = JSON.stringify(value); + if (serialized === undefined) { + throw new TypeError("External tool effect settlement is not JSON serializable."); + } + return serialized; } function parseMcpResult(value: string): typeof McpExecuteCommandResult.infer { return parseSchemaValue(McpExecuteCommandResult, JSON.parse(value)); } +function selectedValue(value: Value, alias: string) { + return sql`${value}`.as(alias); +} + +function toExternalToolEffectState(effect: ExternalToolEffectRow): ExternalToolEffectState { + switch (effect.status) { + case "intent": + return { effectId: effect.id, kind: "intent" }; + case "claimed": + if (effect.claimToken === null || effect.attemptCount < 1) { + throw new Error("Claimed external tool effect is missing its claim audit data."); + } + return { + attempt: effect.attemptCount, + effectId: effect.id, + idempotencyKey: effect.idempotencyKey, + kind: "claimed", + }; + case "succeeded": + if (effect.resultJson === null) { + throw new Error("Succeeded external tool effect is missing its command result."); + } + return { + effectId: effect.id, + kind: "succeeded", + result: parseMcpResultForCommand(effect.resultJson, effect.command), + }; + case "unknown": + return { effectId: effect.id, kind: "unknown" }; + } +} + +function parseMcpResultForCommand( + resultJson: string, + command: McpExecuteCommand, +): typeof McpExecuteCommandResult.infer { + const result = parseMcpResult(resultJson); + if ( + result.requestId !== command.requestId || + result.serverId !== command.serverId || + result.toolName !== command.toolName + ) { + throw new Error("External tool effect result does not match its immutable command intent."); + } + + return result; +} + async function getExternalToolEffect( database: D1Database, - input: { - commandId: DriverCommandId; - driverInstanceId: DriverInstanceId; - }, + input: EffectLookup, ): Promise { const effect = (await getAppDatabase(database) .select({ attemptCount: externalToolEffectsTable.attemptCount, + claimToken: externalToolEffectsTable.claimToken, id: externalToolEffectsTable.id, idempotencyKey: externalToolEffectsTable.idempotencyKey, + payloadJson: driverCommandsTable.payloadJson, resultJson: externalToolEffectsTable.resultJson, + serverId: externalToolEffectsTable.serverId, + sessionRunId: externalToolEffectsTable.sessionRunId, status: externalToolEffectsTable.status, + toolName: externalToolEffectsTable.toolName, }) .from(externalToolEffectsTable) + .innerJoin( + driverCommandsTable, + and( + eq(driverCommandsTable.id, externalToolEffectsTable.commandId), + eq(driverCommandsTable.driverGeneration, input.driverGeneration), + eq(driverCommandsTable.driverInstanceId, externalToolEffectsTable.driverInstanceId), + ), + ) .where( and( eq(externalToolEffectsTable.commandId, input.commandId), @@ -68,8 +154,18 @@ async function getExternalToolEffect( if (effect === null) { throw new Error("External tool effect intent was not found for the runtime command."); } + const command = parseSchemaValue(RuntimeCommand, JSON.parse(effect.payloadJson)); + if ( + command.kind !== "mcp.execute" || + command.commandId !== input.commandId || + command.runId !== effect.sessionRunId || + command.serverId !== effect.serverId || + command.toolName !== effect.toolName + ) { + throw new Error("External tool effect does not match its immutable command intent."); + } - return effect; + return { ...effect, command, requestId: command.requestId }; } /** @@ -77,41 +173,24 @@ async function getExternalToolEffect( * caller commits it in the same D1 batch as the command record, so either both * become visible to a Driver or neither does. */ -export async function prepareExternalToolEffectIntent( - database: D1Database, - input: { - command: { - commandId: string; - serverId: string; - toolName: string; - }; - driverInstanceId: DriverInstanceId; - }, -): Promise { +export function prepareExternalToolEffectIntent(input: { + command: { + commandId: string; + runId: string; + serverId: string; + toolName: string; + }; + driverInstanceId: DriverInstanceId; +}) { const commandId = parsePlatformId(input.command.commandId, "driver command id"); + const sessionRunId = parsePlatformId(input.command.runId, "Session Run id"); const serverId = parsePlatformId(input.command.serverId, "MCP server id"); - const activeRun = - (await getAppDatabase(database) - .select({ id: sessionRunsTable.id }) - .from(sessionRunsTable) - .where( - and( - eq(sessionRunsTable.driverInstanceId, input.driverInstanceId), - inArray(sessionRunsTable.status, ACTIVE_SESSION_RUN_STATUSES), - ), - ) - .limit(1) - .get()) ?? null; - - if (activeRun === null) { - throw new Error("MCP external tool effects require an active Session Run."); - } - const effectId = createPlatformId(); const nowMs = currentTimestampMs(); return { attemptCount: 0, + claimToken: null, commandId, createdAt: nowMs, driverInstanceId: input.driverInstanceId, @@ -120,248 +199,339 @@ export async function prepareExternalToolEffectIntent( providerReceiptJson: null, resultJson: null, serverId, - sessionRunId: activeRun.id, + // This scalar subquery is evaluated in the command+intent D1 batch. If the + // exact Run is no longer active it yields NULL, so the NOT NULL constraint + // rolls the whole batch back instead of leaving a command without a fence. + sessionRunId: sql`( + SELECT ${sessionRunsTable.id} + FROM ${sessionRunsTable} + WHERE ${sessionRunsTable.id} = ${sessionRunId} + AND ${sessionRunsTable.driverInstanceId} = ${input.driverInstanceId} + AND ${sessionRunsTable.status} IN (${sql.join( + ACTIVE_SESSION_RUN_STATUSES.map((status) => sql`${status}`), + sql`, `, + )}) + LIMIT 1 + )`, status: "intent" as const, toolName: input.command.toolName, updatedAt: nowMs, }; } +/** Reads the authoritative ledger without acquiring permission to execute. */ +export async function observeExternalToolEffect( + database: D1Database, + input: EffectLookup, +): Promise { + return toExternalToolEffectState(await getExternalToolEffect(database, input)); +} + /** - * Atomically fences the only permitted provider invocation. An interrupted - * execution becomes unknown instead of being replayed by a new Driver. + * Fences the only permitted provider invocation. A repeated token recovers a + * lost claim response; any different token closes the ambiguous claim instead + * of replaying the provider call. */ export async function claimExternalToolEffect( database: D1Database, - input: { - commandId: DriverCommandId; - driverInstanceId: DriverInstanceId; - }, -): Promise { - const effect = await getExternalToolEffect(database, input); + input: EffectLookup & { claimToken: string }, +): Promise { + parseSchemaValue(ExternalToolEffectClaimToken, input.claimToken); + + for (let retry = 0; retry < 3; retry += 1) { + const effect = await getExternalToolEffect(database, input); - if (effect.status === "succeeded") { - if (effect.resultJson === null) { - throw new Error("Succeeded external tool effect is missing its command result."); + if (effect.status === "succeeded" || effect.status === "unknown") { + const canonical = toExternalToolEffectState(effect); + if (canonical.kind === "intent" || canonical.kind === "claimed") { + throw new Error("A terminal external tool effect returned a non-terminal state."); + } + return canonical; } - return { - effectId: effect.id, - kind: "completed", - result: parseMcpResult(effect.resultJson), - }; - } + if (effect.status === "claimed") { + if (effect.claimToken === input.claimToken) { + const canonical = toExternalToolEffectState(effect); + if (canonical.kind !== "claimed") { + throw new Error("A claimed external tool effect returned a different state."); + } + return canonical; + } - if (effect.status === "unknown") { - return { effectId: effect.id, kind: "unknown" }; - } - - if (effect.status === "executing") { - await markExternalToolEffectUnknown(database, input); - return { effectId: effect.id, kind: "unknown" }; - } + await markClaimedExternalToolEffectUnknown(database, effect); + const canonical = await observeExternalToolEffect(database, input); + if (canonical.kind === "intent") { + throw new Error("A claimed external tool effect unexpectedly returned to intent."); + } + return canonical; + } - const attempt = effect.attemptCount + 1; - const nowMs = currentTimestampMs(); - const [update] = await runAppDatabaseBatch(database, (appDatabase) => [ - appDatabase - .update(externalToolEffectsTable) - .set({ - attemptCount: attempt, - status: "executing", - updatedAt: nowMs, - }) - .where( - and( - eq(externalToolEffectsTable.id, effect.id), - eq(externalToolEffectsTable.attemptCount, effect.attemptCount), - eq(externalToolEffectsTable.status, "intent"), + const attempt = effect.attemptCount + 1; + const nowMs = currentTimestampMs(); + const [update] = await runAppDatabaseBatch(database, (appDatabase) => [ + appDatabase + .update(externalToolEffectsTable) + .set({ + attemptCount: attempt, + claimToken: input.claimToken, + status: "claimed", + updatedAt: nowMs, + }) + .where( + and( + eq(externalToolEffectsTable.id, effect.id), + eq(externalToolEffectsTable.attemptCount, effect.attemptCount), + eq(externalToolEffectsTable.status, "intent"), + exists( + appDatabase + .select({ id: driverInstancesTable.id }) + .from(driverInstancesTable) + .where( + and( + eq(driverInstancesTable.id, input.driverInstanceId), + eq(driverInstancesTable.generation, input.driverGeneration), + inArray(driverInstancesTable.status, LIVE_DRIVER_INSTANCE_STATUSES), + ), + ), + ), + exists( + appDatabase + .select({ id: sessionRunsTable.id }) + .from(sessionRunsTable) + .where( + and( + eq(sessionRunsTable.id, externalToolEffectsTable.sessionRunId), + eq(sessionRunsTable.driverInstanceId, input.driverInstanceId), + inArray(sessionRunsTable.status, ACTIVE_SESSION_RUN_STATUSES), + ), + ), + ), + exists( + appDatabase + .select({ id: driverCommandsTable.id }) + .from(driverCommandsTable) + .where( + and( + eq(driverCommandsTable.id, input.commandId), + eq(driverCommandsTable.driverGeneration, input.driverGeneration), + eq(driverCommandsTable.driverInstanceId, input.driverInstanceId), + eq(driverCommandsTable.status, "accepted"), + ), + ), + ), + ), ), - ), - appDatabase - .insert(externalToolEffectAttemptsTable) - .values({ + appDatabase + .insert(externalToolEffectAttemptsTable) + .select( + appDatabase + .select({ + attempt: selectedValue(attempt, "attempt"), + claimToken: selectedValue(input.claimToken, "claim_token"), + completedAt: selectedValue(null, "completed_at"), + createdAt: selectedValue(nowMs, "created_at"), + effectId: externalToolEffectsTable.id, + providerReceiptJson: selectedValue(null, "provider_receipt_json"), + resultJson: selectedValue(null, "result_json"), + status: selectedValue("claimed" as const, "status"), + }) + .from(externalToolEffectsTable) + .where( + and( + eq(externalToolEffectsTable.id, effect.id), + eq(externalToolEffectsTable.attemptCount, attempt), + eq(externalToolEffectsTable.claimToken, input.claimToken), + eq(externalToolEffectsTable.status, "claimed"), + ), + ), + ) + .onConflictDoNothing(), + ]); + + if (getD1ChangeCount(update) > 0) { + return { attempt, - completedAt: null, - createdAt: nowMs, effectId: effect.id, - providerReceiptJson: null, - resultJson: null, - status: "executing", - }) - .onConflictDoNothing(), - ]); - - if (getD1ChangeCount(update) === 0) { - return claimExternalToolEffect(database, input); + idempotencyKey: effect.idempotencyKey, + kind: "claimed", + }; + } } - return { - attempt, - effectId: effect.id, - idempotencyKey: effect.idempotencyKey, - kind: "execute", - }; + throw new Error("External tool effect claim did not reach a stable state."); } /** - * Stores the outcome before the Driver sends the command terminal receipt. - * The Driver preserves provider `_meta` as an opaque receipt when one exists. - * MCP does not standardize reconciliation, so absent receipts remain null and - * a later delivery uncertainty is fenced as unknown rather than replayed. + * Persists an owner's terminal observation and always returns the state that + * actually won the ledger CAS. This makes settlement-response loss retryable. */ -export async function completeExternalToolEffect( +export async function settleExternalToolEffect( database: D1Database, - input: { - commandId: DriverCommandId; - driverInstanceId: DriverInstanceId; - providerReceiptJson?: string | null; - result: typeof McpExecuteCommandResult.infer; + input: EffectLookup & { + claimToken: string; + effectId: ExternalToolEffectId; + settlement: ExternalToolEffectSettlement; }, -): Promise { +): Promise { + parseSchemaValue(ExternalToolEffectClaimToken, input.claimToken); + const settlement = parseSchemaValue( + ExternalToolEffectSettlement, + JSON.parse(serializeRuntimeSettlement(input.settlement)), + ); + const effect = await getExternalToolEffect(database, input); - if (effect.status === "succeeded") { - return; + if (effect.id !== input.effectId) { + throw new Error("External tool effect settlement does not match the command's effect id."); + } + + if (effect.status !== "claimed" || effect.claimToken !== input.claimToken) { + return toExternalToolEffectState(effect); } - if (effect.status !== "executing") { - throw new Error(`Cannot complete an external tool effect in ${effect.status} state.`); + + if ( + settlement.kind === "succeeded" && + (settlement.result.requestId !== effect.requestId || + settlement.result.serverId !== effect.serverId || + settlement.result.toolName !== effect.toolName) + ) { + throw new Error("External tool effect result does not match its immutable command intent."); } const nowMs = currentTimestampMs(); - const resultJson = JSON.stringify(input.result); - const [update] = await runAppDatabaseBatch(database, (appDatabase) => [ + const providerReceiptJson = + settlement.kind === "succeeded" ? (settlement.providerReceiptJson ?? null) : null; + const resultJson = settlement.kind === "succeeded" ? JSON.stringify(settlement.result) : null; + const status = settlement.kind === "succeeded" ? "succeeded" : "unknown"; + + await runAppDatabaseBatch(database, (appDatabase) => [ appDatabase .update(externalToolEffectsTable) .set({ - providerReceiptJson: input.providerReceiptJson ?? null, + providerReceiptJson, resultJson, - status: "succeeded", + status, updatedAt: nowMs, }) .where( and( eq(externalToolEffectsTable.id, effect.id), - eq(externalToolEffectsTable.status, "executing"), + eq(externalToolEffectsTable.claimToken, input.claimToken), + eq(externalToolEffectsTable.status, "claimed"), ), ), appDatabase .update(externalToolEffectAttemptsTable) - .set({ - completedAt: nowMs, - providerReceiptJson: input.providerReceiptJson ?? null, - resultJson, - status: "succeeded", - }) + .set({ completedAt: nowMs, providerReceiptJson, resultJson, status }) .where( and( eq(externalToolEffectAttemptsTable.effectId, effect.id), eq(externalToolEffectAttemptsTable.attempt, effect.attemptCount), - eq(externalToolEffectAttemptsTable.status, "executing"), + eq(externalToolEffectAttemptsTable.claimToken, input.claimToken), + eq(externalToolEffectAttemptsTable.status, "claimed"), ), ), ]); - if (getD1ChangeCount(update) === 0) { - throw new Error("External tool effect completion lost its execution claim."); - } + return observeExternalToolEffect(database, input); } -/** - * No generic MCP provider reconciliation or compensation exists. This is a - * deliberate terminal fence: callers must resolve an unknown effect explicitly - * before they create another external action. - */ -export async function markExternalToolEffectUnknown( +async function markClaimedExternalToolEffectUnknown( database: D1Database, - input: { - commandId: DriverCommandId; - driverInstanceId: DriverInstanceId; - }, + effect: ExternalToolEffectRow, ): Promise { - const effect = await getExternalToolEffect(database, input); - - if (effect.status !== "executing") { + if (effect.status !== "claimed" || effect.claimToken === null) { return; } + const claimToken = effect.claimToken; const nowMs = currentTimestampMs(); - const [update] = await runAppDatabaseBatch(database, (appDatabase) => [ + await runAppDatabaseBatch(database, (appDatabase) => [ appDatabase .update(externalToolEffectsTable) - .set({ - status: "unknown", - updatedAt: nowMs, - }) + .set({ status: "unknown", updatedAt: nowMs }) .where( and( eq(externalToolEffectsTable.id, effect.id), - eq(externalToolEffectsTable.status, "executing"), + eq(externalToolEffectsTable.claimToken, claimToken), + eq(externalToolEffectsTable.status, "claimed"), ), ), appDatabase .update(externalToolEffectAttemptsTable) - .set({ - completedAt: nowMs, - status: "unknown", - }) + .set({ completedAt: nowMs, status: "unknown" }) .where( and( eq(externalToolEffectAttemptsTable.effectId, effect.id), eq(externalToolEffectAttemptsTable.attempt, effect.attemptCount), - eq(externalToolEffectAttemptsTable.status, "executing"), + eq(externalToolEffectAttemptsTable.claimToken, claimToken), + eq(externalToolEffectAttemptsTable.status, "claimed"), ), ), ]); - - if (getD1ChangeCount(update) === 0) { - return; - } } -/** Marks all in-flight effects for a terminal Driver in two bounded writes. */ -export async function markExecutingExternalToolEffectsUnknownForDriver( +/** Fences every unresolved claim owned by a terminal Driver. */ +export async function markClaimedExternalToolEffectsUnknownForDriver( database: D1Database, - driverInstanceId: DriverInstanceId, + input: { + driverGeneration: number; + driverInstanceId: DriverInstanceId; + }, ): Promise { - const effects = await getAppDatabase(database) - .select({ - attemptCount: externalToolEffectsTable.attemptCount, - id: externalToolEffectsTable.id, - }) - .from(externalToolEffectsTable) - .where( - and( - eq(externalToolEffectsTable.driverInstanceId, driverInstanceId), - eq(externalToolEffectsTable.status, "executing"), - ), - ) - .all(); - - if (effects.length === 0) { - return; - } - - const effectIds = effects.map((effect) => effect.id); const nowMs = currentTimestampMs(); await runAppDatabaseBatch(database, (appDatabase) => [ appDatabase - .update(externalToolEffectsTable) - .set({ status: "unknown", updatedAt: nowMs }) + .update(externalToolEffectAttemptsTable) + .set({ completedAt: nowMs, status: "unknown" }) .where( and( - eq(externalToolEffectsTable.driverInstanceId, driverInstanceId), - eq(externalToolEffectsTable.status, "executing"), - inArray(externalToolEffectsTable.id, effectIds), + eq(externalToolEffectAttemptsTable.status, "claimed"), + exists( + appDatabase + .select({ id: externalToolEffectsTable.id }) + .from(externalToolEffectsTable) + .where( + and( + eq(externalToolEffectsTable.id, externalToolEffectAttemptsTable.effectId), + eq(externalToolEffectsTable.driverInstanceId, input.driverInstanceId), + eq(externalToolEffectsTable.status, "claimed"), + exists( + appDatabase + .select({ id: driverCommandsTable.id }) + .from(driverCommandsTable) + .where( + and( + eq(driverCommandsTable.id, externalToolEffectsTable.commandId), + eq(driverCommandsTable.driverGeneration, input.driverGeneration), + eq(driverCommandsTable.driverInstanceId, input.driverInstanceId), + ), + ), + ), + ), + ), + ), ), ), appDatabase - .update(externalToolEffectAttemptsTable) - .set({ completedAt: nowMs, status: "unknown" }) + .update(externalToolEffectsTable) + .set({ status: "unknown", updatedAt: nowMs }) .where( and( - inArray(externalToolEffectAttemptsTable.effectId, effectIds), - eq(externalToolEffectAttemptsTable.status, "executing"), + eq(externalToolEffectsTable.driverInstanceId, input.driverInstanceId), + eq(externalToolEffectsTable.status, "claimed"), + exists( + appDatabase + .select({ id: driverCommandsTable.id }) + .from(driverCommandsTable) + .where( + and( + eq(driverCommandsTable.id, externalToolEffectsTable.commandId), + eq(driverCommandsTable.driverGeneration, input.driverGeneration), + eq(driverCommandsTable.driverInstanceId, input.driverInstanceId), + ), + ), + ), ), ), ]); @@ -369,14 +539,14 @@ export async function markExecutingExternalToolEffectsUnknownForDriver( export async function getExternalToolEffectForCommand( database: D1Database, - input: { - commandId: DriverCommandId; - driverInstanceId: DriverInstanceId; - }, + input: EffectLookup, ): Promise<{ attemptCount: number; + claimToken: string | null; + id: ExternalToolEffectId; idempotencyKey: string; providerReceiptJson: string | null; + resultJson: string | null; sessionRunId: SessionRunId; status: ExternalToolEffectStatus; } | null> { @@ -384,12 +554,23 @@ export async function getExternalToolEffectForCommand( (await getAppDatabase(database) .select({ attemptCount: externalToolEffectsTable.attemptCount, + claimToken: externalToolEffectsTable.claimToken, + id: externalToolEffectsTable.id, idempotencyKey: externalToolEffectsTable.idempotencyKey, providerReceiptJson: externalToolEffectsTable.providerReceiptJson, + resultJson: externalToolEffectsTable.resultJson, sessionRunId: externalToolEffectsTable.sessionRunId, status: externalToolEffectsTable.status, }) .from(externalToolEffectsTable) + .innerJoin( + driverCommandsTable, + and( + eq(driverCommandsTable.id, externalToolEffectsTable.commandId), + eq(driverCommandsTable.driverGeneration, input.driverGeneration), + eq(driverCommandsTable.driverInstanceId, externalToolEffectsTable.driverInstanceId), + ), + ) .where( and( eq(externalToolEffectsTable.commandId, input.commandId), diff --git a/apps/api/src/modules/runtime/infrastructure/session-runs/runtime-command-record.mapper.ts b/apps/api/src/modules/runtime/infrastructure/session-runs/runtime-command-record.mapper.ts index 6021ee41..390fa885 100644 --- a/apps/api/src/modules/runtime/infrastructure/session-runs/runtime-command-record.mapper.ts +++ b/apps/api/src/modules/runtime/infrastructure/session-runs/runtime-command-record.mapper.ts @@ -1,7 +1,10 @@ import { RuntimeCommandRecord } from "@mosoo/contracts/runtime-command"; import type { RuntimeCommand, RuntimeCommandStatus } from "@mosoo/contracts/runtime-command"; +import { DurableRunError } from "@mosoo/contracts/session-run"; import { parseSchemaValue } from "@mosoo/contracts/validation"; +import { NonEmptyString } from "@mosoo/contracts/validation"; import type { DriverCommandId, DriverInstanceId } from "@mosoo/id"; +import { type } from "arktype"; import { toIsoString } from "../../../../time"; @@ -25,6 +28,7 @@ class RuntimeCommandStoreCorruptionError extends Error { export interface RuntimeCommandRecordRow { ackedAt: number | null; completedAt: number | null; + driverGeneration: number | null; driverInstanceId: DriverInstanceId; errorJson: string | null; expiresAt: number | null; @@ -37,6 +41,56 @@ export interface RuntimeCommandRecordRow { status: RuntimeCommandStatus; } +const legacyTerminalRuntimeCommandRecordBase = { + ackedAt: "string | null", + completedAt: "string | null", + driverInstanceId: NonEmptyString, + error: DurableRunError.or("null"), + expiresAt: "string | null", + id: NonEmptyString, + issuedAt: "string", + seq: "number >= 0", + status: '"completed" | "failed" | "expired" | "cancelled"', +} as const; + +const LegacyTerminalRuntimeCommandRecord = type({ + ...legacyTerminalRuntimeCommandRecordBase, + kind: '"turn.cancel"', + payload: type({ + commandId: NonEmptyString, + kind: '"turn.cancel"', + "reason?": "string", + }).onUndeclaredKey("reject"), + result: "null", +}) + .onUndeclaredKey("reject") + .or( + type({ + ...legacyTerminalRuntimeCommandRecordBase, + kind: '"permission.resolve"', + payload: type({ + commandId: NonEmptyString, + decision: '"allow_once" | "reject_once"', + kind: '"permission.resolve"', + requestId: NonEmptyString, + }).onUndeclaredKey("reject"), + result: "null", + }).onUndeclaredKey("reject"), + ); +export type LegacyTerminalRuntimeCommandRecord = typeof LegacyTerminalRuntimeCommandRecord.infer; + +export type RuntimeCommandStorageRecord = + | { + driverGeneration: number; + format: "v3"; + record: RuntimeCommandRecord; + } + | { + driverGeneration: null; + format: "legacy-v2-terminal"; + record: LegacyTerminalRuntimeCommandRecord | RuntimeCommandRecord; + }; + type RuntimeCommandJsonColumn = "errorJson" | "payloadJson" | "resultJson"; function readRuntimeCommandJsonRaw( @@ -92,9 +146,11 @@ function parseRuntimeCommandJsonColumn( } } -export function toRuntimeCommandRecordFromRow(row: RuntimeCommandRecordRow): RuntimeCommandRecord { +export function toRuntimeCommandStorageRecordFromRow( + row: RuntimeCommandRecordRow, +): RuntimeCommandStorageRecord { try { - return parseSchemaValue(RuntimeCommandRecord, { + const recordInput = { ackedAt: row.ackedAt === null ? null : toIsoString(row.ackedAt), completedAt: row.completedAt === null ? null : toIsoString(row.completedAt), driverInstanceId: row.driverInstanceId, @@ -107,7 +163,47 @@ export function toRuntimeCommandRecordFromRow(row: RuntimeCommandRecordRow): Run result: row.resultJson === null ? null : parseRuntimeCommandJsonColumn(row, "resultJson"), seq: row.seq, status: row.status, - }); + }; + + if ( + (recordInput.result !== null && recordInput.error !== null) || + (recordInput.result !== null && recordInput.status !== "completed") || + (recordInput.error !== null && recordInput.status === "completed") || + ((recordInput.result !== null || recordInput.error !== null) && + !["completed", "failed", "expired", "cancelled"].includes(recordInput.status)) + ) { + throw new TypeError("Runtime command terminal payload does not match its stored status."); + } + + if (row.driverGeneration === null) { + if (!["completed", "failed", "expired", "cancelled"].includes(row.status)) { + throw new TypeError("Only terminal legacy runtime commands may omit driver generation."); + } + + if (!LegacyTerminalRuntimeCommandRecord.allows(recordInput)) { + return { + driverGeneration: null, + format: "legacy-v2-terminal", + record: parseSchemaValue(RuntimeCommandRecord, recordInput), + }; + } + + return { + driverGeneration: null, + format: "legacy-v2-terminal", + record: parseSchemaValue(LegacyTerminalRuntimeCommandRecord, recordInput), + }; + } + + if (!Number.isSafeInteger(row.driverGeneration) || row.driverGeneration < 0) { + throw new TypeError("Runtime command driver generation is invalid."); + } + + return { + driverGeneration: row.driverGeneration, + format: "v3", + record: parseSchemaValue(RuntimeCommandRecord, recordInput), + }; } catch (error) { if (error instanceof RuntimeCommandStoreCorruptionError) { throw error; @@ -116,7 +212,7 @@ export function toRuntimeCommandRecordFromRow(row: RuntimeCommandRecordRow): Run throw new RuntimeCommandStoreCorruptionError({ cause: error, commandId: row.id, - message: `Runtime command ${row.id} does not match the runtime command contract.`, + message: `Runtime command ${row.id} does not match a supported storage contract.`, }); } } diff --git a/apps/api/src/modules/runtime/infrastructure/session-runs/runtime-command-store.repository.ts b/apps/api/src/modules/runtime/infrastructure/session-runs/runtime-command-store.repository.ts index b190c407..0ed32750 100644 --- a/apps/api/src/modules/runtime/infrastructure/session-runs/runtime-command-store.repository.ts +++ b/apps/api/src/modules/runtime/infrastructure/session-runs/runtime-command-store.repository.ts @@ -1,26 +1,46 @@ -import { RuntimeCommandRecord } from "@mosoo/contracts/runtime-command"; -import type { +import { createMcpExecuteFailedEventIdentity } from "@mosoo/agent-driver/events"; +import { + InputStartCommandResult, + McpExecuteCommandResult, + RUNTIME_COMMAND_MAX_UTF8_BYTES, + RUNTIME_COMMAND_TERMINAL_PAYLOAD_MAX_UTF8_BYTES, RuntimeCommand, RuntimeCommandResult, RuntimeCommandStatus, } from "@mosoo/contracts/runtime-command"; +import type { RuntimeCommandRecord } from "@mosoo/contracts/runtime-command"; +import { DurableRunError } from "@mosoo/contracts/session-run"; import type { RunError } from "@mosoo/contracts/session-run"; -import { parseSchemaValue } from "@mosoo/contracts/validation"; -import { driverCommandsTable, driverInstancesTable, externalToolEffectsTable } from "@mosoo/db"; +import { PrimitiveRecord, parseSchemaValue } from "@mosoo/contracts/validation"; +import { + driverCommandsTable, + driverInstancesTable, + externalToolEffectsTable, + sessionEventsTable, + sessionRunsTable, + sessionsTable, +} from "@mosoo/db"; import { parsePlatformId } from "@mosoo/id"; -import type { DriverCommandId, DriverInstanceId, SessionRunId } from "@mosoo/id"; +import type { DriverCommandId, DriverInstanceId, SessionId, SessionRunId } from "@mosoo/id"; +import { stringifyRuntimeEventSemanticValue } from "@mosoo/runtime-events"; import { and, asc, eq, exists, gt, inArray, isNull, lte, ne, or, sql } from "drizzle-orm"; +import type { SQL } from "drizzle-orm"; import { getAppDatabase, getD1ChangeCount, runAppDatabaseBatch, } from "../../../../platform/db/drizzle"; -import { currentTimestampMs, toIsoString } from "../../../../time"; +import { currentTimestampMs } from "../../../../time"; import { LIVE_DRIVER_INSTANCE_STATUSES } from "../../domain/driver-instance-lifecycle.machine"; +import { ACTIVE_SESSION_RUN_STATUSES } from "../../domain/session-run-lifecycle.machine"; +import { createSessionRunTerminalSourceId } from "../../domain/session-run-terminal-event-id"; import { prepareExternalToolEffectIntent } from "./external-tool-effect-store.repository"; -import { toRuntimeCommandRecordFromRow } from "./runtime-command-record.mapper"; -import type { RuntimeCommandRecordRow } from "./runtime-command-record.mapper"; +import { toRuntimeCommandStorageRecordFromRow } from "./runtime-command-record.mapper"; +import type { + RuntimeCommandRecordRow, + RuntimeCommandStorageRecord, +} from "./runtime-command-record.mapper"; import { createRuntimeCommandBatchTransitionOutcome, decideRuntimeCommandTransition, @@ -39,29 +59,80 @@ export interface RuntimeCommandMaintenanceOutcome { recovered: RuntimeCommandBatchTransitionOutcome; } -export interface RuntimeCommandGlobalMaintenanceOutcome extends RuntimeCommandMaintenanceOutcome { +export interface RuntimeCommandTerminalRepairOutcome { + completed: RuntimeCommandBatchTransitionOutcome; failed: RuntimeCommandBatchTransitionOutcome; } -async function getNextRuntimeCommandSeq( - database: D1Database, - driverInstanceId: DriverInstanceId, -): Promise { - const row = - (await getAppDatabase(database) - .update(driverInstancesTable) - .set({ - commandSeqCursor: sql`${driverInstancesTable.commandSeqCursor} + 1`, - }) - .where(eq(driverInstancesTable.id, driverInstanceId)) - .returning({ seq: driverInstancesTable.commandSeqCursor }) - .get()) ?? null; +export interface AcceptedMcpCommandRepair { + command: Extract; + commandId: DriverCommandId; + effectId: string; + effectStatus: "intent" | "succeeded" | "unknown"; + runtimeId: string; + sessionId: SessionId; + terminal: + | { + result: typeof McpExecuteCommandResult.infer; + status: "completed"; + } + | { + error: RunError; + status: "failed"; + }; +} + +export interface AcceptedInputStartCommandRepair { + command: Extract; + commandId: DriverCommandId; + runtimeId: string; + sessionId: SessionId; + terminal: + | { + result: typeof InputStartCommandResult.infer; + status: "completed"; + } + | { + status: "cancelled"; + } + | { + error: RunError; + status: "failed"; + }; +} - if (row === null) { - throw new Error("Driver instance not found while allocating a runtime command sequence."); +function parseSessionRunRepairError(input: { + errorCode: string | null; + errorDetailsJson: string | null; + errorMessage: string | null; + errorRetryable: boolean | null; +}): RunError | null { + if (input.errorCode === null && input.errorDetailsJson === null && input.errorMessage === null) { + return null; } + if ( + input.errorCode === null || + input.errorCode.length === 0 || + input.errorMessage === null || + input.errorMessage.length === 0 + ) { + throw new Error("Session Run has an incomplete authoritative durable error."); + } + + const details = + input.errorDetailsJson === null + ? {} + : parseSchemaValue(PrimitiveRecord, JSON.parse(input.errorDetailsJson)); + return parseSchemaValue(DurableRunError, { + code: input.errorCode, + details, + message: input.errorMessage, + retryable: input.errorRetryable ?? false, + }); +} - return row.seq; +function selectedValue(value: Value, alias: string) { + return sql`${value}`.as(alias); } function isDriverCommandSeqConflict(error: unknown): boolean { @@ -75,6 +146,10 @@ function isDriverCommandSeqConflict(error: unknown): boolean { ); } +function isExternalToolEffectRunAdmissionConflict(error: unknown): boolean { + return error instanceof Error && error.message.includes("external_tool_effect.session_run_id"); +} + type RuntimeCommandErrorDetails = Record; function createRuntimeCommandDeliveryExpiredError(details: RuntimeCommandErrorDetails): RunError { @@ -95,33 +170,522 @@ function createRuntimeCommandDriverTerminalError(details: RuntimeCommandErrorDet }; } +function createRuntimeCommandExternalToolEffectUnknownError(input: { + command: Extract; + effectId: string; +}): RunError { + const message = `External effect ${input.effectId} for MCP tool ${input.command.toolName} has an unknown outcome and will not be replayed.`; + return { + code: "driver.external_tool_effect_unknown", + details: { + commandId: input.command.commandId, + effectId: input.effectId, + requestId: input.command.requestId, + runId: input.command.runId, + serverId: input.command.serverId, + toolName: input.command.toolName, + }, + message, + retryable: false, + }; +} + +function createRuntimeCommandExternalToolEffectNotExecutedError( + details: RuntimeCommandErrorDetails, +): RunError { + return { + code: "driver.external_tool_effect_not_executed", + details, + message: "The Driver stopped before the external tool invocation was claimed.", + retryable: true, + }; +} + +const runtimeCommandJsonEncoder = new TextEncoder(); + +function serializeRuntimeCommandJson(value: unknown, label: string): string { + const serialized = JSON.stringify(value); + + if (serialized === undefined) { + throw new TypeError(`${label} is not JSON serializable.`); + } + + return serialized; +} + +function assertRuntimeCommandJsonLimit(serialized: string, limit: number, label: string): void { + const byteLength = runtimeCommandJsonEncoder.encode(serialized).byteLength; + + if (byteLength > limit) { + throw new RangeError(`${label} exceeds ${limit} UTF-8 bytes.`); + } +} + +function externalToolEffectCommandTerminalGuard( + database: ReturnType, + input: { + commandId: DriverCommandId; + resultJson: string | null; + status: RuntimeCommandStatus; + }, +): SQL { + if (input.status !== "completed") { + return exists( + database + .select({ id: externalToolEffectsTable.id }) + .from(externalToolEffectsTable) + .where( + and( + eq(externalToolEffectsTable.commandId, input.commandId), + inArray(externalToolEffectsTable.status, ["intent", "unknown"]), + ), + ), + ); + } + + if (input.resultJson === null) { + return sql`FALSE`; + } + + const proposed = parseSchemaValue(McpExecuteCommandResult, JSON.parse(input.resultJson)); + return exists( + database + .select({ id: externalToolEffectsTable.id }) + .from(externalToolEffectsTable) + .where( + and( + eq(externalToolEffectsTable.commandId, input.commandId), + eq(externalToolEffectsTable.status, "succeeded"), + sql`json_extract(${externalToolEffectsTable.resultJson}, '$.outputText') = ${proposed.outputText}`, + sql`json_extract(${externalToolEffectsTable.resultJson}, '$.requestId') = ${proposed.requestId}`, + sql`json_extract(${externalToolEffectsTable.resultJson}, '$.serverId') = ${proposed.serverId}`, + sql`json_extract(${externalToolEffectsTable.resultJson}, '$.toolName') = ${proposed.toolName}`, + sql`COALESCE(json_extract(${externalToolEffectsTable.resultJson}, '$.isError'), 0) = ${proposed.isError === true ? 1 : 0}`, + ), + ), + ); +} + +interface RuntimeCommandTerminalState { + readonly ackedAt: number | null; + readonly errorJson: string | null; + readonly kind: RuntimeCommand["kind"]; + readonly payloadJson: string; + readonly resultJson: string | null; + readonly status: RuntimeCommandStatus; +} + +function mcpTerminalSourceEventId( + commandId: DriverCommandId, + status: "cancelled" | "completed", +): string { + return `mcp.execute.${status}:${commandId}`; +} + +function primitiveRecordJsonEquals(column: SQL, value: Record): SQL { + const serialized = JSON.stringify(value); + + return sql`NOT EXISTS ( + SELECT 1 + FROM json_each(${column}) AS stored + WHERE NOT EXISTS ( + SELECT 1 + FROM json_each(${serialized}) AS proposed + WHERE proposed.key = stored.key + AND proposed.type = stored.type + AND proposed.value IS stored.value + ) + ) AND NOT EXISTS ( + SELECT 1 + FROM json_each(${serialized}) AS proposed + WHERE NOT EXISTS ( + SELECT 1 + FROM json_each(${column}) AS stored + WHERE stored.key = proposed.key + AND stored.type = proposed.type + AND stored.value IS proposed.value + ) + )`; +} + +function runtimeCommandTerminalEventGuard( + database: ReturnType, + input: { + command: RuntimeCommand; + driverInstanceId: DriverInstanceId; + error: RunError | null; + result: RuntimeCommandResult | null; + status: RuntimeCommandStatus; + }, +): SQL { + if (!isRuntimeCommandTerminalStatus(input.status)) { + return sql`TRUE`; + } + + if (input.command.kind === "input.start") { + const runId = parsePlatformId( + input.command.runId, + "input.start terminal Session Run ID", + ); + const eventType = + input.status === "completed" + ? "run.completed" + : input.status === "failed" + ? "run.failed" + : "run.cancelled"; + const runStatusGuard = + input.status === "cancelled" || input.status === "expired" + ? inArray(sessionRunsTable.status, ["cancelled", "expired"]) + : input.status === "completed" || input.status === "failed" + ? eq(sessionRunsTable.status, input.status) + : sql`FALSE`; + const runErrorGuard = (() => { + if (input.status === "failed") { + return input.error === null + ? sql`FALSE` + : and( + eq(sessionRunsTable.errorCode, input.error.code), + eq(sessionRunsTable.errorMessage, input.error.message), + eq(sessionRunsTable.errorRetryable, input.error.retryable), + primitiveRecordJsonEquals( + sql`${sessionRunsTable.errorDetailsJson}`, + input.error.details, + ), + ); + } + + return input.status === "completed" + ? and( + isNull(sessionRunsTable.errorCode), + isNull(sessionRunsTable.errorDetailsJson), + isNull(sessionRunsTable.errorMessage), + isNull(sessionRunsTable.errorRetryable), + ) + : sql`TRUE`; + })(); + const terminalRun = database + .select({ id: sessionRunsTable.id }) + .from(sessionRunsTable) + .where( + and( + eq(sessionRunsTable.id, runId), + eq(sessionRunsTable.driverInstanceId, input.driverInstanceId), + runStatusGuard, + runErrorGuard, + ), + ); + + return exists( + database + .select({ id: sessionEventsTable.id }) + .from(sessionEventsTable) + .where( + and( + eq(sessionEventsTable.runId, runId), + eq(sessionEventsTable.eventType, eventType), + eq( + sessionEventsTable.sourceEventId, + createSessionRunTerminalSourceId(runId, eventType), + ), + exists(terminalRun), + ), + ), + ); + } + + if (input.command.kind !== "mcp.execute") { + return sql`TRUE`; + } + + const commandId = parsePlatformId( + input.command.commandId, + "mcp.execute terminal command ID", + ); + const runId = parsePlatformId( + input.command.runId, + "mcp.execute terminal Session Run ID", + ); + + const toolStatus = + input.status === "completed" ? "completed" : input.status === "failed" ? "failed" : "cancelled"; + const outputGuard = + input.status === "completed" && input.result !== null + ? eq( + sessionEventsTable.toolOutputText, + parseSchemaValue(McpExecuteCommandResult, input.result).outputText, + ) + : input.status === "failed" && input.error !== null + ? eq(sessionEventsTable.toolOutputText, input.error.message) + : isNull(sessionEventsTable.toolOutputText); + const sourceEventId = + input.status === "failed" + ? input.error === null + ? null + : createMcpExecuteFailedEventIdentity({ + commandId, + rawInput: input.command.argumentsJson, + rawOutput: input.error.message, + title: input.command.toolName, + toolCallId: input.command.toolCallId, + }).sourceEventId + : mcpTerminalSourceEventId( + commandId, + input.status === "completed" ? "completed" : "cancelled", + ); + + return exists( + database + .select({ id: sessionEventsTable.id }) + .from(sessionEventsTable) + .where( + and( + eq(sessionEventsTable.mcpCommandId, commandId), + eq(sessionEventsTable.runId, runId), + eq(sessionEventsTable.eventType, "tool.call.updated"), + sourceEventId === null ? sql`FALSE` : eq(sessionEventsTable.sourceEventId, sourceEventId), + eq(sessionEventsTable.toolCallId, input.command.toolCallId), + eq(sessionEventsTable.toolInputJson, input.command.argumentsJson), + eq(sessionEventsTable.toolName, input.command.toolName), + eq(sessionEventsTable.toolStatus, toolStatus), + outputGuard, + ), + ), + ); +} + +async function assertRuntimeCommandTerminalEvent( + database: D1Database, + input: Parameters[1], +): Promise { + const row = await getAppDatabase(database) + .select({ + valid: sql`CASE WHEN ${runtimeCommandTerminalEventGuard( + getAppDatabase(database), + input, + )} THEN 1 ELSE 0 END`, + }) + .from(driverCommandsTable) + .where( + eq( + driverCommandsTable.id, + parsePlatformId(input.command.commandId, "Runtime command ID"), + ), + ) + .limit(1) + .get(); + + if (row?.valid !== 1) { + throw new Error( + `Runtime command ${input.command.commandId} terminal update is missing its durable event.`, + ); + } +} + +function parseRuntimeCommandResultJson( + kind: RuntimeCommand["kind"], + value: string | null, +): RuntimeCommandResult | null { + if (value === null) { + return null; + } + + const result = parseSchemaValue(RuntimeCommandResult, JSON.parse(value)); + + if (kind === "input.start") { + return parseSchemaValue(InputStartCommandResult, result); + } + + if (kind === "mcp.execute") { + const parsed = parseSchemaValue(McpExecuteCommandResult, result); + return { ...parsed, isError: parsed.isError ?? false }; + } + + throw new Error(`${kind} runtime commands cannot contain a durable result.`); +} + +function parseRuntimeCommandErrorJson(value: string | null): RunError | null { + return value === null ? null : parseSchemaValue(DurableRunError, JSON.parse(value)); +} + +function assertEqualRuntimeCommandPayload( + expected: unknown, + actual: unknown, + commandId: DriverCommandId, +): void { + if (stringifyRuntimeEventSemanticValue(expected) !== stringifyRuntimeEventSemanticValue(actual)) { + throw new Error( + `Runtime command ${commandId} duplicate conflicts with its durable terminal payload.`, + ); + } +} + +async function assertMcpTerminalEffectState( + database: D1Database, + input: { + commandId: DriverCommandId; + driverGeneration: number; + driverInstanceId: DriverInstanceId; + proposedResult: RuntimeCommandResult | null; + storedResult: RuntimeCommandResult | null; + status: RuntimeCommandStatus; + }, +): Promise { + const effect = + (await getAppDatabase(database) + .select({ + resultJson: externalToolEffectsTable.resultJson, + status: externalToolEffectsTable.status, + }) + .from(externalToolEffectsTable) + .innerJoin( + driverCommandsTable, + and( + eq(driverCommandsTable.id, externalToolEffectsTable.commandId), + eq(driverCommandsTable.driverGeneration, input.driverGeneration), + eq(driverCommandsTable.driverInstanceId, input.driverInstanceId), + ), + ) + .where( + and( + eq(externalToolEffectsTable.commandId, input.commandId), + eq(externalToolEffectsTable.driverInstanceId, input.driverInstanceId), + ), + ) + .limit(1) + .get()) ?? null; + + if (effect === null) { + throw new Error(`MCP runtime command ${input.commandId} is missing its durable effect.`); + } + + if (input.status === "completed") { + if (effect.status !== "succeeded" || effect.resultJson === null) { + throw new Error(`MCP runtime command ${input.commandId} has no succeeded durable effect.`); + } + + const effectResult = parseRuntimeCommandResultJson("mcp.execute", effect.resultJson); + assertEqualRuntimeCommandPayload(effectResult, input.storedResult, input.commandId); + assertEqualRuntimeCommandPayload(effectResult, input.proposedResult, input.commandId); + return; + } + + if (!isRuntimeCommandTerminalStatus(input.status)) { + return; + } + + if ((effect.status !== "intent" && effect.status !== "unknown") || effect.resultJson !== null) { + throw new Error( + `MCP runtime command ${input.commandId} terminal state conflicts with its effect.`, + ); + } +} + +async function assertRuntimeCommandDuplicatePayload( + database: D1Database, + input: { + commandId: DriverCommandId; + driverGeneration: number; + driverInstanceId: DriverInstanceId; + errorJson: string | null; + resultJson: string | null; + stored: RuntimeCommandTerminalState; + }, +): Promise { + const proposedError = parseRuntimeCommandErrorJson(input.errorJson); + const storedError = parseRuntimeCommandErrorJson(input.stored.errorJson); + const proposedResult = parseRuntimeCommandResultJson(input.stored.kind, input.resultJson); + const storedResult = parseRuntimeCommandResultJson(input.stored.kind, input.stored.resultJson); + const command = parseSchemaValue(RuntimeCommand, JSON.parse(input.stored.payloadJson)); + + assertEqualRuntimeCommandPayload(storedError, proposedError, input.commandId); + assertEqualRuntimeCommandPayload(storedResult, proposedResult, input.commandId); + + if (isRuntimeCommandTerminalStatus(input.stored.status)) { + await assertRuntimeCommandTerminalEvent(database, { + command, + driverInstanceId: input.driverInstanceId, + error: storedError, + result: storedResult, + status: input.stored.status, + }); + } + + if (input.stored.kind === "mcp.execute") { + await assertMcpTerminalEffectState(database, { + commandId: input.commandId, + driverGeneration: input.driverGeneration, + driverInstanceId: input.driverInstanceId, + proposedResult, + status: input.stored.status, + storedResult, + }); + } +} + +function runtimeCommandActiveRunGuard(database: ReturnType): SQL { + return or( + eq(driverCommandsTable.kind, "session.stop"), + exists( + database + .select({ id: sessionRunsTable.id }) + .from(sessionRunsTable) + .where( + and( + eq( + sessionRunsTable.id, + sql`json_extract(${driverCommandsTable.payloadJson}, '$.runId')`, + ), + eq(sessionRunsTable.driverInstanceId, driverCommandsTable.driverInstanceId), + inArray(sessionRunsTable.status, ACTIVE_SESSION_RUN_STATUSES), + ), + ), + ), + )!; +} + export async function createRuntimeCommandRecord( database: D1Database, input: { command: RuntimeCommand; + driverGeneration: number; driverInstanceId: DriverInstanceId; expiresAt?: number | null; status?: RuntimeCommandStatus; }, ): Promise { + const payloadJson = serializeRuntimeCommandJson(input.command, "Runtime command"); + assertRuntimeCommandJsonLimit(payloadJson, RUNTIME_COMMAND_MAX_UTF8_BYTES, "Runtime command"); + const command = parseSchemaValue(RuntimeCommand, JSON.parse(payloadJson)); + if (!Number.isSafeInteger(input.driverGeneration) || input.driverGeneration < 0) { + throw new TypeError("Driver generation must be a non-negative safe integer."); + } const issuedAt = currentTimestampMs(); - const commandId = parsePlatformId(input.command.commandId, "Runtime command ID"); - const status = input.status ?? "queued"; - const payloadJson = JSON.stringify(input.command); - - return createRuntimeCommandRecordAttempt(database, input, { - attempt: 0, - commandId, - issuedAt, - payloadJson, - status, - }); + const commandId = parsePlatformId(command.commandId, "Runtime command ID"); + const sessionRunId = + command.kind === "session.stop" + ? null + : parsePlatformId(command.runId, "Runtime command Session Run ID"); + const status = parseSchemaValue(RuntimeCommandStatus, input.status ?? "queued"); + + return createRuntimeCommandRecordAttempt( + database, + { ...input, command }, + { + attempt: 0, + commandId, + issuedAt, + payloadJson, + sessionRunId, + status, + }, + ); } async function createRuntimeCommandRecordAttempt( database: D1Database, input: { command: RuntimeCommand; + driverGeneration: number; driverInstanceId: DriverInstanceId; expiresAt?: number | null; status?: RuntimeCommandStatus; @@ -131,6 +695,7 @@ async function createRuntimeCommandRecordAttempt( commandId: DriverCommandId; issuedAt: number; payloadJson: string; + sessionRunId: SessionRunId | null; status: RuntimeCommandStatus; }, ): Promise { @@ -141,57 +706,87 @@ async function createRuntimeCommandRecordAttempt( try { const externalToolEffectIntent = input.command.kind === "mcp.execute" - ? await prepareExternalToolEffectIntent(database, { + ? prepareExternalToolEffectIntent({ command: input.command, driverInstanceId: input.driverInstanceId, }) : null; - const seq = await getNextRuntimeCommandSeq(database, input.driverInstanceId); const commandValues = { - ackedAt: null, - completedAt: null, - deliveryConnectionId: null, - driverInstanceId: input.driverInstanceId, - errorJson: null, - expiresAt: input.expiresAt ?? null, - id: state.commandId, - issuedAt: state.issuedAt, - kind: input.command.kind, - payloadJson: state.payloadJson, - resultJson: null, - seq, - status: state.status, + ackedAt: selectedValue(null, "acked_at"), + completedAt: selectedValue(null, "completed_at"), + deliveryConnectionId: selectedValue(null, "delivery_connection_id"), + driverGeneration: driverInstancesTable.generation, + driverInstanceId: driverInstancesTable.id, + errorJson: selectedValue(null, "error_json"), + expiresAt: selectedValue(input.expiresAt ?? null, "expires_at"), + id: selectedValue(state.commandId, "id"), + issuedAt: selectedValue(state.issuedAt, "issued_at"), + kind: selectedValue(input.command.kind, "kind"), + payloadJson: selectedValue(state.payloadJson, "payload_json"), + resultJson: selectedValue(null, "result_json"), + seq: driverInstancesTable.commandSeqCursor, + status: selectedValue(state.status, "status"), }; + const results = await runAppDatabaseBatch(database, (appDatabase) => { + const activeRun = + state.sessionRunId === null + ? null + : appDatabase + .select({ id: sessionRunsTable.id }) + .from(sessionRunsTable) + .where( + and( + eq(sessionRunsTable.id, state.sessionRunId), + eq(sessionRunsTable.driverInstanceId, input.driverInstanceId), + inArray(sessionRunsTable.status, ACTIVE_SESSION_RUN_STATUSES), + ), + ); + const admission = and( + eq(driverInstancesTable.id, input.driverInstanceId), + eq(driverInstancesTable.generation, input.driverGeneration), + inArray(driverInstancesTable.status, [...LIVE_DRIVER_INSTANCE_STATUSES]), + ...(activeRun === null ? [] : [exists(activeRun)]), + ); + const commandInsert = appDatabase + .insert(driverCommandsTable) + .select(appDatabase.select(commandValues).from(driverInstancesTable).where(admission)); + const cursorUpdate = appDatabase + .update(driverInstancesTable) + .set({ commandSeqCursor: sql`${driverInstancesTable.commandSeqCursor} + 1` }) + .where(admission); + + return externalToolEffectIntent === null + ? [cursorUpdate, commandInsert] + : [ + cursorUpdate, + commandInsert, + appDatabase.insert(externalToolEffectsTable).values(externalToolEffectIntent), + ]; + }); - if (externalToolEffectIntent === null) { - await getAppDatabase(database).insert(driverCommandsTable).values(commandValues).run(); - } else { - await runAppDatabaseBatch(database, (appDatabase) => [ - appDatabase.insert(driverCommandsTable).values(commandValues), - appDatabase.insert(externalToolEffectsTable).values(externalToolEffectIntent), - ]); + if (getD1ChangeCount((results as readonly unknown[])[1]) === 0) { + throw new Error("Driver generation is no longer current."); } - const record = parseSchemaValue(RuntimeCommandRecord, { - ackedAt: null, - completedAt: null, - driverInstanceId: input.driverInstanceId, - error: null, - expiresAt: - input.expiresAt === null || input.expiresAt === undefined - ? null - : toIsoString(input.expiresAt), - id: state.commandId, - issuedAt: toIsoString(state.issuedAt), - kind: input.command.kind, - payload: input.command, - result: null, - seq, - status: state.status, - }); + const record = await getRuntimeCommandRecord( + database, + input.driverInstanceId, + input.driverGeneration, + state.commandId, + ); + + if (record === null) { + throw new Error("Runtime command was not persisted."); + } return record; } catch (error) { + if (input.command.kind === "mcp.execute" && isExternalToolEffectRunAdmissionConflict(error)) { + throw new Error("MCP external tool effects require the command's active Session Run.", { + cause: error, + }); + } + if (state.attempt < 4 && isDriverCommandSeqConflict(error)) { return createRuntimeCommandRecordAttempt(database, input, { ...state, @@ -208,22 +803,67 @@ export async function updateRuntimeCommandRecord( input: { commandId: DriverCommandId; deliveryConnectionId?: string; + driverGeneration: number; driverInstanceId: DriverInstanceId; error?: RunError; result?: RuntimeCommandResult; status: RuntimeCommandStatus; }, ): Promise { + const status = parseSchemaValue(RuntimeCommandStatus, input.status); + if (input.error !== undefined && input.result !== undefined) { + throw new TypeError("Runtime command terminal update cannot contain both error and result."); + } + + const errorJson = + input.error === undefined + ? null + : serializeRuntimeCommandJson(input.error, "Runtime command terminal error"); + const resultJson = + input.result === undefined + ? null + : serializeRuntimeCommandJson(input.result, "Runtime command result"); + const terminalPayloadJson = errorJson ?? resultJson; + if (terminalPayloadJson !== null) { + assertRuntimeCommandJsonLimit( + terminalPayloadJson, + RUNTIME_COMMAND_TERMINAL_PAYLOAD_MAX_UTF8_BYTES, + "Runtime command terminal payload", + ); + } + if (errorJson !== null) { + parseSchemaValue(DurableRunError, JSON.parse(errorJson)); + } + const parsedResult = + resultJson === null + ? undefined + : parseSchemaValue(RuntimeCommandResult, JSON.parse(resultJson)); + const storedResultJson = parsedResult === null ? null : resultJson; + if ( + (!isRuntimeCommandTerminalStatus(status) && + (errorJson !== null || storedResultJson !== null)) || + (status === "completed" && errorJson !== null) || + (status !== "completed" && isRuntimeCommandTerminalStatus(status) && storedResultJson !== null) + ) { + throw new TypeError("Runtime command terminal payload does not match its target status."); + } + const current = (await getAppDatabase(database) .select({ + ackedAt: driverCommandsTable.ackedAt, deliveryConnectionId: driverCommandsTable.deliveryConnectionId, + errorJson: driverCommandsTable.errorJson, + kind: driverCommandsTable.kind, + payloadJson: driverCommandsTable.payloadJson, + resultJson: driverCommandsTable.resultJson, status: driverCommandsTable.status, }) .from(driverCommandsTable) .where( and( eq(driverCommandsTable.id, input.commandId), + eq(driverCommandsTable.driverGeneration, input.driverGeneration), eq(driverCommandsTable.driverInstanceId, input.driverInstanceId), ), ) @@ -235,10 +875,42 @@ export async function updateRuntimeCommandRecord( currentStatus: null, kind: "rejected", reason: "command_not_found", - targetStatus: input.status, + targetStatus: status, }; } + const command = parseSchemaValue(RuntimeCommand, JSON.parse(current.payloadJson)); + if (command.commandId !== input.commandId || command.kind !== current.kind) { + throw new Error(`Runtime command ${input.commandId} has inconsistent immutable payload.`); + } + + if (parsedResult !== undefined && parsedResult !== null) { + if (current.kind === "input.start") { + parseSchemaValue(InputStartCommandResult, parsedResult); + if (Object.keys(parsedResult).some((key) => key !== "requestId")) { + throw new TypeError("input.start runtime command results may only contain requestId."); + } + } else if (current.kind === "mcp.execute") { + parseSchemaValue(McpExecuteCommandResult, parsedResult); + const allowedKeys = new Set(["isError", "outputText", "requestId", "serverId", "toolName"]); + if (Object.keys(parsedResult).some((key) => !allowedKeys.has(key))) { + throw new TypeError("mcp.execute runtime command result contains an unknown field."); + } + } else { + throw new TypeError(`${current.kind} runtime commands cannot store a result.`); + } + } + + if ( + command.kind === "input.start" && + status === "completed" && + (parsedResult === undefined || + parsedResult === null || + parseSchemaValue(InputStartCommandResult, parsedResult).requestId !== command.requestId) + ) { + throw new TypeError("input.start completion must carry its exact requestId."); + } + if ( input.deliveryConnectionId !== undefined && current.deliveryConnectionId !== input.deliveryConnectionId @@ -247,20 +919,42 @@ export async function updateRuntimeCommandRecord( currentStatus: current.status, kind: "rejected", reason: "stale_delivery_connection", - targetStatus: input.status, + targetStatus: status, }; } - const transition = decideRuntimeCommandTransition(current.status, input.status); + const transition = decideRuntimeCommandTransition(current.status, status); + + if (transition.kind === "duplicate") { + await assertRuntimeCommandDuplicatePayload(database, { + commandId: input.commandId, + driverGeneration: input.driverGeneration, + driverInstanceId: input.driverInstanceId, + errorJson, + resultJson: storedResultJson, + stored: { ...current, kind: command.kind }, + }); + return transition; + } if (transition.kind !== "applied") { return transition; } const timestampMs = currentTimestampMs(); - const ackedAt = isRuntimeCommandAcknowledgedStatus(input.status) ? timestampMs : null; - const completedAt = isRuntimeCommandTerminalStatus(input.status) ? timestampMs : null; - + const ackedAt = isRuntimeCommandAcknowledgedStatus(status) ? timestampMs : null; + const completedAt = isRuntimeCommandTerminalStatus(status) ? timestampMs : null; + + const authoritativeResultJson = + current.kind === "mcp.execute" && status === "completed" + ? sql`( + SELECT ${externalToolEffectsTable.resultJson} + FROM ${externalToolEffectsTable} + WHERE ${externalToolEffectsTable.commandId} = ${input.commandId} + AND ${externalToolEffectsTable.status} = 'succeeded' + LIMIT 1 + )` + : storedResultJson; const result = await getAppDatabase(database) .update(driverCommandsTable) .set({ @@ -270,30 +964,95 @@ export async function updateRuntimeCommandRecord( completedAt === null ? undefined : sql`COALESCE(${completedAt}, ${driverCommandsTable.completedAt})`, - errorJson: input.error === undefined ? null : JSON.stringify(input.error), - resultJson: input.result === undefined ? null : JSON.stringify(input.result), - status: input.status, + errorJson, + resultJson: authoritativeResultJson, + status, }) .where( and( eq(driverCommandsTable.id, input.commandId), + eq(driverCommandsTable.driverGeneration, input.driverGeneration), eq(driverCommandsTable.driverInstanceId, input.driverInstanceId), eq(driverCommandsTable.status, current.status), ...(input.deliveryConnectionId === undefined ? [] : [eq(driverCommandsTable.deliveryConnectionId, input.deliveryConnectionId)]), + ...(status === "accepted" ? [runtimeCommandActiveRunGuard(getAppDatabase(database))] : []), + ...(current.kind === "mcp.execute" && isRuntimeCommandTerminalStatus(status) + ? [ + externalToolEffectCommandTerminalGuard(getAppDatabase(database), { + commandId: input.commandId, + resultJson: storedResultJson, + status, + }), + ] + : []), + ...(isRuntimeCommandTerminalStatus(status) && + (command.kind === "input.start" || command.kind === "mcp.execute") + ? [ + runtimeCommandTerminalEventGuard(getAppDatabase(database), { + command, + driverInstanceId: input.driverInstanceId, + error: parseRuntimeCommandErrorJson(errorJson), + result: + storedResultJson === null + ? null + : parseRuntimeCommandResultJson(command.kind, storedResultJson), + status, + }), + ] + : []), ), ) .run(); - return getD1ChangeCount(result) > 0 - ? transition - : { - currentStatus: current.status, - kind: "rejected", - reason: "illegal_transition", - targetStatus: input.status, - }; + if (getD1ChangeCount(result) > 0) { + return transition; + } + + const winner = + (await getAppDatabase(database) + .select({ + ackedAt: driverCommandsTable.ackedAt, + errorJson: driverCommandsTable.errorJson, + kind: driverCommandsTable.kind, + payloadJson: driverCommandsTable.payloadJson, + resultJson: driverCommandsTable.resultJson, + status: driverCommandsTable.status, + }) + .from(driverCommandsTable) + .where( + and( + eq(driverCommandsTable.id, input.commandId), + eq(driverCommandsTable.driverGeneration, input.driverGeneration), + eq(driverCommandsTable.driverInstanceId, input.driverInstanceId), + ), + ) + .limit(1) + .get()) ?? null; + + if (winner?.status === status) { + const winnerCommand = parseSchemaValue(RuntimeCommand, JSON.parse(winner.payloadJson)); + if (winnerCommand.commandId !== input.commandId || winnerCommand.kind !== winner.kind) { + throw new Error(`Runtime command ${input.commandId} has inconsistent immutable payload.`); + } + await assertRuntimeCommandDuplicatePayload(database, { + commandId: input.commandId, + driverGeneration: input.driverGeneration, + driverInstanceId: input.driverInstanceId, + errorJson, + resultJson: storedResultJson, + stored: { ...winner, kind: winnerCommand.kind }, + }); + return { kind: "duplicate", status }; + } + + return { + currentStatus: winner?.status ?? current.status, + kind: "rejected", + reason: "illegal_transition", + targetStatus: status, + }; } export async function markRuntimeCommandRecordDelivered( @@ -301,6 +1060,7 @@ export async function markRuntimeCommandRecordDelivered( input: { commandId: DriverCommandId; connectionId: string; + driverGeneration: number; driverInstanceId: DriverInstanceId; expiresAfter?: number; }, @@ -311,12 +1071,15 @@ export async function markRuntimeCommandRecordDelivered( (await db .select({ deliveryConnectionId: driverCommandsTable.deliveryConnectionId, + kind: driverCommandsTable.kind, + payloadJson: driverCommandsTable.payloadJson, status: driverCommandsTable.status, }) .from(driverCommandsTable) .where( and( eq(driverCommandsTable.id, input.commandId), + eq(driverCommandsTable.driverGeneration, input.driverGeneration), eq(driverCommandsTable.driverInstanceId, input.driverInstanceId), ), ) @@ -330,6 +1093,7 @@ export async function markRuntimeCommandRecordDelivered( and( eq(driverInstancesTable.id, input.driverInstanceId), eq(driverInstancesTable.connectionId, input.connectionId), + eq(driverInstancesTable.generation, input.driverGeneration), ), ) .limit(1) @@ -344,6 +1108,50 @@ export async function markRuntimeCommandRecordDelivered( }; } + if (current.kind !== "session.stop") { + const command = parseSchemaValue(RuntimeCommand, JSON.parse(current.payloadJson)); + + if (command.kind === "session.stop") { + throw new Error("Runtime command kind does not match its payload."); + } + + const runId = parsePlatformId(command.runId, "Runtime command Session Run ID"); + const activeRun = + (await db + .select({ id: sessionRunsTable.id }) + .from(sessionRunsTable) + .where( + and( + eq(sessionRunsTable.id, runId), + eq(sessionRunsTable.driverInstanceId, input.driverInstanceId), + inArray(sessionRunsTable.status, ACTIVE_SESSION_RUN_STATUSES), + ), + ) + .limit(1) + .get()) ?? null; + + if (activeRun === null) { + await db + .update(driverCommandsTable) + .set({ completedAt: currentTimestampMs(), status: "cancelled" }) + .where( + and( + eq(driverCommandsTable.id, input.commandId), + eq(driverCommandsTable.driverGeneration, input.driverGeneration), + eq(driverCommandsTable.driverInstanceId, input.driverInstanceId), + eq(driverCommandsTable.status, current.status), + ), + ) + .run(); + return { + currentStatus: current.status, + kind: "rejected", + reason: "inactive_session_run", + targetStatus, + }; + } + } + if (activeConnection === null) { return { currentStatus: current.status, @@ -379,6 +1187,7 @@ export async function markRuntimeCommandRecordDelivered( and( eq(driverInstancesTable.id, input.driverInstanceId), eq(driverInstancesTable.connectionId, input.connectionId), + eq(driverInstancesTable.generation, input.driverGeneration), ), ); const result = await db @@ -390,6 +1199,7 @@ export async function markRuntimeCommandRecordDelivered( .where( and( eq(driverCommandsTable.id, input.commandId), + eq(driverCommandsTable.driverGeneration, input.driverGeneration), eq(driverCommandsTable.driverInstanceId, input.driverInstanceId), eq(driverCommandsTable.status, current.status), ...(input.expiresAfter === undefined @@ -401,6 +1211,7 @@ export async function markRuntimeCommandRecordDelivered( )!, ]), exists(activeConnectionQuery), + runtimeCommandActiveRunGuard(db), ), ) .run(); @@ -418,13 +1229,27 @@ export async function markRuntimeCommandRecordDelivered( export async function getRuntimeCommandRecord( database: D1Database, driverInstanceId: DriverInstanceId, + driverGeneration: number, commandId: DriverCommandId, ): Promise { + const stored = await getRuntimeCommandStorageRecord(database, driverInstanceId, commandId); + return stored?.format === "v3" && stored.driverGeneration === driverGeneration + ? stored.record + : null; +} + +/** Reads immutable pre-v3 terminal history without making it executable wire data. */ +export async function getRuntimeCommandStorageRecord( + database: D1Database, + driverInstanceId: DriverInstanceId, + commandId: DriverCommandId, +): Promise { const row = (await getAppDatabase(database) .select({ ackedAt: driverCommandsTable.ackedAt, completedAt: driverCommandsTable.completedAt, + driverGeneration: driverCommandsTable.driverGeneration, driverInstanceId: driverCommandsTable.driverInstanceId, errorJson: driverCommandsTable.errorJson, expiresAt: driverCommandsTable.expiresAt, @@ -450,12 +1275,13 @@ export async function getRuntimeCommandRecord( return null; } - return toRuntimeCommandRecordFromRow(row); + return toRuntimeCommandStorageRecordFromRow(row); } async function expireRuntimeCommandDeliveryLeases( database: D1Database, driverInstanceId: DriverInstanceId, + driverGeneration: number, nowMs: number, ): Promise { const expirableStatuses = getRuntimeCommandDeliveryLeaseExpirableStatuses(); @@ -473,6 +1299,7 @@ async function expireRuntimeCommandDeliveryLeases( .where( and( eq(driverCommandsTable.driverInstanceId, driverInstanceId), + eq(driverCommandsTable.driverGeneration, driverGeneration), inArray(driverCommandsTable.status, [...expirableStatuses]), or(eq(driverCommandsTable.status, queuedStatus), isNull(driverCommandsTable.ackedAt)), lte(driverCommandsTable.expiresAt, nowMs), @@ -553,6 +1380,7 @@ async function recoverRuntimeCommandsDeliveredToStaleConnections( database: D1Database, input: { connectionId: string; + driverGeneration: number; driverInstanceId: DriverInstanceId; }, ): Promise { @@ -568,6 +1396,7 @@ async function recoverRuntimeCommandsDeliveredToStaleConnections( .where( and( eq(driverCommandsTable.driverInstanceId, input.driverInstanceId), + eq(driverCommandsTable.driverGeneration, input.driverGeneration), inArray(driverCommandsTable.status, [...recoverableStatuses]), isNull(driverCommandsTable.ackedAt), or( @@ -593,6 +1422,7 @@ async function recoverRuntimeCommandsDeliveredToStaleConnectionsGlobally( .where( and( eq(driverInstancesTable.id, driverCommandsTable.driverInstanceId), + eq(driverInstancesTable.generation, driverCommandsTable.driverGeneration), inArray(driverInstancesTable.status, [...LIVE_DRIVER_INSTANCE_STATUSES]), or( isNull(driverCommandsTable.deliveryConnectionId), @@ -620,71 +1450,382 @@ async function recoverRuntimeCommandsDeliveredToStaleConnectionsGlobally( return createRuntimeCommandBatchTransitionOutcome(targetStatus, getD1ChangeCount(result)); } -export async function failAcceptedRuntimeCommandsForTerminalDriver( +/** Returns accepted input commands whose authoritative Session Run is terminal. */ +export async function listAcceptedInputStartCommandRepairsForTerminalDriver( database: D1Database, input: { + driverGeneration: number; driverInstanceId: DriverInstanceId; - nowMs?: number; }, -): Promise { - const nowMs = input.nowMs ?? currentTimestampMs(); - const targetStatus = "failed" satisfies RuntimeCommandStatus; - const error = createRuntimeCommandDriverTerminalError({ - driverInstanceId: input.driverInstanceId, - }); +): Promise { + const rows = await getAppDatabase(database) + .select({ + commandId: driverCommandsTable.id, + payloadJson: driverCommandsTable.payloadJson, + run: { + errorCode: sessionRunsTable.errorCode, + errorDetailsJson: sessionRunsTable.errorDetailsJson, + errorMessage: sessionRunsTable.errorMessage, + errorRetryable: sessionRunsTable.errorRetryable, + sessionId: sessionRunsTable.sessionId, + status: sessionRunsTable.status, + }, + runtimeId: sql`COALESCE(${sessionRunsTable.runtimeId}, ${sessionsTable.runtimeId})`, + }) + .from(driverCommandsTable) + .innerJoin( + sessionRunsTable, + and( + sql`json_extract(${driverCommandsTable.payloadJson}, '$.runId') = ${sessionRunsTable.id}`, + eq(sessionRunsTable.driverInstanceId, driverCommandsTable.driverInstanceId), + ), + ) + .innerJoin(sessionsTable, eq(sessionsTable.id, sessionRunsTable.sessionId)) + .where( + and( + eq(driverCommandsTable.driverInstanceId, input.driverInstanceId), + eq(driverCommandsTable.driverGeneration, input.driverGeneration), + eq(driverCommandsTable.kind, "input.start"), + eq(driverCommandsTable.status, "accepted"), + ), + ) + .orderBy(asc(driverCommandsTable.seq)) + .all(); + + const repairs: AcceptedInputStartCommandRepair[] = []; + for (const row of rows) { + const command = parseSchemaValue(RuntimeCommand, JSON.parse(row.payloadJson)); + if (command.kind !== "input.start" || command.commandId !== row.commandId) { + throw new Error("Input command repair does not match its immutable command intent."); + } + if (row.runtimeId.length === 0) { + throw new Error("Input command repair is missing its durable completion identity."); + } - const result = await getAppDatabase(database) - .update(driverCommandsTable) - .set({ - completedAt: sql`COALESCE(${driverCommandsTable.completedAt}, ${nowMs})`, - errorJson: JSON.stringify(error), - status: targetStatus, + const runError = parseSessionRunRepairError(row.run); + const common = { + command, + commandId: row.commandId, + runtimeId: row.runtimeId, + sessionId: row.run.sessionId, + }; + + if (row.run.status === "completed") { + if (runError !== null) { + throw new Error("Completed Session Run unexpectedly contains a durable error."); + } + repairs.push({ + ...common, + terminal: { + result: { requestId: command.requestId }, + status: "completed" as const, + }, + }); + continue; + } + if (row.run.status === "cancelled" || row.run.status === "expired") { + repairs.push({ ...common, terminal: { status: "cancelled" as const } }); + continue; + } + if (row.run.status === "failed") { + if (runError === null) { + throw new Error("Failed Session Run is missing its authoritative durable error."); + } + repairs.push({ ...common, terminal: { error: runError, status: "failed" as const } }); + continue; + } + + throw new Error("Input command repair requires an authoritative terminal Session Run."); + } + return repairs; +} + +/** Returns canonical repairs that still need a durable terminal tool event. */ +export async function listAcceptedMcpCommandRepairsForTerminalDriver( + database: D1Database, + input: { + driverGeneration: number; + driverInstanceId: DriverInstanceId; + }, +): Promise { + const rows = await getAppDatabase(database) + .select({ + commandId: driverCommandsTable.id, + effectId: externalToolEffectsTable.id, + effectStatus: externalToolEffectsTable.status, + payloadJson: driverCommandsTable.payloadJson, + resultJson: externalToolEffectsTable.resultJson, + runtimeId: sql`COALESCE(${sessionRunsTable.runtimeId}, ${sessionsTable.runtimeId})`, + sessionId: sessionRunsTable.sessionId, }) + .from(driverCommandsTable) + .innerJoin( + externalToolEffectsTable, + and( + eq(externalToolEffectsTable.commandId, driverCommandsTable.id), + eq(externalToolEffectsTable.driverInstanceId, driverCommandsTable.driverInstanceId), + ), + ) + .innerJoin(sessionRunsTable, eq(sessionRunsTable.id, externalToolEffectsTable.sessionRunId)) + .innerJoin(sessionsTable, eq(sessionsTable.id, sessionRunsTable.sessionId)) .where( and( eq(driverCommandsTable.driverInstanceId, input.driverInstanceId), + eq(driverCommandsTable.driverGeneration, input.driverGeneration), + eq(driverCommandsTable.kind, "mcp.execute"), eq(driverCommandsTable.status, "accepted"), + sql`json_extract(${driverCommandsTable.payloadJson}, '$.runId') = ${externalToolEffectsTable.sessionRunId}`, + sql`json_extract(${driverCommandsTable.payloadJson}, '$.serverId') = ${externalToolEffectsTable.serverId}`, + sql`json_extract(${driverCommandsTable.payloadJson}, '$.toolName') = ${externalToolEffectsTable.toolName}`, ), ) - .run(); + .orderBy(asc(driverCommandsTable.seq)) + .all(); - return createRuntimeCommandBatchTransitionOutcome(targetStatus, getD1ChangeCount(result)); + const repairs: AcceptedMcpCommandRepair[] = []; + for (const row of rows) { + if (row.runtimeId.length === 0) { + throw new Error("MCP command repair is missing its durable completion identity."); + } + + const command = parseSchemaValue(RuntimeCommand, JSON.parse(row.payloadJson)); + if (command.kind !== "mcp.execute" || command.commandId !== row.commandId) { + throw new Error("MCP command repair does not match its immutable command intent."); + } + + if (row.effectStatus === "claimed") { + throw new Error("Terminal Driver repair must fence claimed MCP effects before listing them."); + } + + const common = { + command, + commandId: row.commandId, + effectId: row.effectId, + effectStatus: row.effectStatus, + runtimeId: row.runtimeId, + sessionId: row.sessionId, + }; + + if (row.effectStatus === "succeeded") { + if (row.resultJson === null) { + throw new Error("Succeeded MCP command is missing its durable result."); + } + const result = parseSchemaValue(McpExecuteCommandResult, JSON.parse(row.resultJson)); + if ( + result.requestId !== command.requestId || + result.serverId !== command.serverId || + result.toolName !== command.toolName + ) { + throw new Error("Succeeded MCP result does not match its immutable command intent."); + } + repairs.push({ ...common, terminal: { result, status: "completed" as const } }); + continue; + } + + if (row.resultJson !== null) { + throw new Error("Unresolved MCP command unexpectedly contains a durable result."); + } + const error = + row.effectStatus === "unknown" + ? createRuntimeCommandExternalToolEffectUnknownError({ + command, + effectId: row.effectId, + }) + : createRuntimeCommandExternalToolEffectNotExecutedError({ + commandId: row.commandId, + driverInstanceId: input.driverInstanceId, + effectId: row.effectId, + }); + repairs.push({ ...common, terminal: { error, status: "failed" as const } }); + } + return repairs; } -async function failAcceptedRuntimeCommandsForTerminalDriversGlobally( +export async function repairAcceptedRuntimeCommandsForTerminalDriver( database: D1Database, - nowMs: number, -): Promise { - const targetStatus = "failed" satisfies RuntimeCommandStatus; - const error = createRuntimeCommandDriverTerminalError({}); + input: { + driverGeneration: number; + driverInstanceId: DriverInstanceId; + nowMs?: number; + }, +): Promise { + const nowMs = input.nowMs ?? currentTimestampMs(); const db = getAppDatabase(database); - const terminalDriverQuery = db - .select({ id: driverInstancesTable.id }) - .from(driverInstancesTable) + const unacceptedIntents = await db + .select({ + commandId: driverCommandsTable.id, + effectId: externalToolEffectsTable.id, + }) + .from(driverCommandsTable) + .innerJoin( + externalToolEffectsTable, + and( + eq(externalToolEffectsTable.commandId, driverCommandsTable.id), + eq(externalToolEffectsTable.driverInstanceId, driverCommandsTable.driverInstanceId), + ), + ) .where( and( - eq(driverInstancesTable.id, driverCommandsTable.driverInstanceId), - inArray(driverInstancesTable.status, ["failed", "stopped"]), + eq(driverCommandsTable.driverInstanceId, input.driverInstanceId), + eq(driverCommandsTable.driverGeneration, input.driverGeneration), + inArray(driverCommandsTable.status, ["queued", "delivered"]), + eq(externalToolEffectsTable.status, "intent"), ), - ); + ) + .all(); - const result = await db + let failedCount = 0; + for (const effect of unacceptedIntents) { + const result = await db + .update(driverCommandsTable) + .set({ + completedAt: sql`COALESCE(${driverCommandsTable.completedAt}, ${nowMs})`, + errorJson: JSON.stringify( + createRuntimeCommandExternalToolEffectNotExecutedError({ + commandId: effect.commandId, + driverInstanceId: input.driverInstanceId, + effectId: effect.effectId, + }), + ), + status: "failed", + }) + .where( + and( + eq(driverCommandsTable.id, effect.commandId), + eq(driverCommandsTable.driverGeneration, input.driverGeneration), + eq(driverCommandsTable.driverInstanceId, input.driverInstanceId), + inArray(driverCommandsTable.status, ["queued", "delivered"]), + exists( + db + .select({ id: externalToolEffectsTable.id }) + .from(externalToolEffectsTable) + .where( + and( + eq(externalToolEffectsTable.id, effect.effectId), + eq(externalToolEffectsTable.commandId, effect.commandId), + eq(externalToolEffectsTable.driverInstanceId, input.driverInstanceId), + eq(externalToolEffectsTable.status, "intent"), + ), + ), + ), + ), + ) + .run(); + failedCount += getD1ChangeCount(result); + } + + const pendingEventFirstCommand = + (await db + .select({ id: driverCommandsTable.id }) + .from(driverCommandsTable) + .where( + and( + eq(driverCommandsTable.driverInstanceId, input.driverInstanceId), + eq(driverCommandsTable.driverGeneration, input.driverGeneration), + or( + and( + eq(driverCommandsTable.kind, "mcp.execute"), + inArray(driverCommandsTable.status, ["queued", "delivered", "accepted"]), + ), + and( + eq(driverCommandsTable.kind, "input.start"), + eq(driverCommandsTable.status, "accepted"), + ), + ), + ), + ) + .limit(1) + .get()) ?? null; + if (pendingEventFirstCommand !== null) { + throw new Error("Accepted input and MCP commands require event-first terminal reconciliation."); + } + + const genericResult = await db .update(driverCommandsTable) .set({ completedAt: sql`COALESCE(${driverCommandsTable.completedAt}, ${nowMs})`, - errorJson: JSON.stringify(error), - status: targetStatus, + errorJson: JSON.stringify( + createRuntimeCommandDriverTerminalError({ + driverInstanceId: input.driverInstanceId, + }), + ), + status: "failed", }) - .where(and(eq(driverCommandsTable.status, "accepted"), exists(terminalDriverQuery))) + .where( + and( + eq(driverCommandsTable.driverInstanceId, input.driverInstanceId), + eq(driverCommandsTable.driverGeneration, input.driverGeneration), + ne(driverCommandsTable.kind, "mcp.execute"), + or( + inArray(driverCommandsTable.status, ["queued", "delivered"]), + and( + eq(driverCommandsTable.status, "accepted"), + ne(driverCommandsTable.kind, "input.start"), + ), + ), + ), + ) .run(); + failedCount += getD1ChangeCount(genericResult); - return createRuntimeCommandBatchTransitionOutcome(targetStatus, getD1ChangeCount(result)); + return { + completed: createRuntimeCommandBatchTransitionOutcome("completed", 0), + failed: createRuntimeCommandBatchTransitionOutcome("failed", failedCount), + }; +} + +export async function listTerminalDriversWithPendingRuntimeCommands( + database: D1Database, +): Promise { + const db = getAppDatabase(database); + const pendingCommand = db + .select({ id: driverCommandsTable.id }) + .from(driverCommandsTable) + .where( + and( + eq(driverCommandsTable.driverInstanceId, driverInstancesTable.id), + eq(driverCommandsTable.driverGeneration, driverInstancesTable.generation), + inArray(driverCommandsTable.status, ["queued", "delivered", "accepted"]), + ), + ); + const claimedEffect = db + .select({ id: externalToolEffectsTable.id }) + .from(externalToolEffectsTable) + .where( + and( + eq(externalToolEffectsTable.driverInstanceId, driverInstancesTable.id), + eq(externalToolEffectsTable.status, "claimed"), + exists( + db + .select({ id: driverCommandsTable.id }) + .from(driverCommandsTable) + .where( + and( + eq(driverCommandsTable.id, externalToolEffectsTable.commandId), + eq(driverCommandsTable.driverGeneration, driverInstancesTable.generation), + ), + ), + ), + ), + ); + + return db + .select({ generation: driverInstancesTable.generation, id: driverInstancesTable.id }) + .from(driverInstancesTable) + .where( + and( + inArray(driverInstancesTable.status, ["failed", "stopped"]), + or(exists(pendingCommand), exists(claimedEffect)), + ), + ) + .all(); } export async function maintainRuntimeCommandRecords( database: D1Database, input: { connectionId: string; + driverGeneration: number; driverInstanceId: DriverInstanceId; nowMs?: number; }, @@ -693,9 +1834,15 @@ export async function maintainRuntimeCommandRecords( const recovered = await recoverRuntimeCommandsDeliveredToStaleConnections(database, { connectionId: input.connectionId, + driverGeneration: input.driverGeneration, driverInstanceId: input.driverInstanceId, }); - const expired = await expireRuntimeCommandDeliveryLeases(database, input.driverInstanceId, nowMs); + const expired = await expireRuntimeCommandDeliveryLeases( + database, + input.driverInstanceId, + input.driverGeneration, + nowMs, + ); return { expired, @@ -708,16 +1855,14 @@ export async function repairRuntimeCommandRecords( input: { nowMs?: number; } = {}, -): Promise { +): Promise { const nowMs = input.nowMs ?? currentTimestampMs(); const recovered = await recoverRuntimeCommandsDeliveredToStaleConnectionsGlobally(database); const expired = await expireRuntimeCommandDeliveryLeasesGlobally(database, nowMs); - const failed = await failAcceptedRuntimeCommandsForTerminalDriversGlobally(database, nowMs); return { expired, - failed, recovered, }; } @@ -725,6 +1870,7 @@ export async function repairRuntimeCommandRecords( export async function claimNextQueuedRuntimeCommandRecord( database: D1Database, driverInstanceId: DriverInstanceId, + driverGeneration: number, connectionId: string, ): Promise { const nowMs = currentTimestampMs(); @@ -732,6 +1878,7 @@ export async function claimNextQueuedRuntimeCommandRecord( await maintainRuntimeCommandRecords(database, { connectionId, + driverGeneration, driverInstanceId, nowMs, }); @@ -744,7 +1891,9 @@ export async function claimNextQueuedRuntimeCommandRecord( .where( and( eq(driverCommandsTable.driverInstanceId, driverInstanceId), + eq(driverCommandsTable.driverGeneration, driverGeneration), eq(driverCommandsTable.status, "queued"), + runtimeCommandActiveRunGuard(db), or(isNull(driverCommandsTable.expiresAt), gt(driverCommandsTable.expiresAt, nowMs)), ), ) @@ -759,6 +1908,7 @@ export async function claimNextQueuedRuntimeCommandRecord( const deliveryOutcome = await markRuntimeCommandRecordDelivered(database, { commandId: nextQueued.id, connectionId, + driverGeneration, driverInstanceId, expiresAfter: nowMs, }); @@ -772,7 +1922,7 @@ export async function claimNextQueuedRuntimeCommandRecord( const claimed = deliveryOutcome.kind === "applied" - ? await getRuntimeCommandRecord(database, driverInstanceId, nextQueued.id) + ? await getRuntimeCommandRecord(database, driverInstanceId, driverGeneration, nextQueued.id) : null; if (claimed !== null) { diff --git a/apps/api/src/modules/runtime/infrastructure/session-runs/runtime-command-transition.ts b/apps/api/src/modules/runtime/infrastructure/session-runs/runtime-command-transition.ts index b31f3fb7..5796776b 100644 --- a/apps/api/src/modules/runtime/infrastructure/session-runs/runtime-command-transition.ts +++ b/apps/api/src/modules/runtime/infrastructure/session-runs/runtime-command-transition.ts @@ -16,6 +16,7 @@ export type RuntimeCommandTransitionOutcome = | "command_not_found" | "illegal_transition" | "inactive_delivery_connection" + | "inactive_session_run" | "stale_delivery_connection"; targetStatus: RuntimeCommandStatus; }; @@ -32,7 +33,7 @@ const previousStatusesByTarget = { completed: ["delivered", "accepted"], delivered: ["queued"], expired: ["queued", "delivered", "accepted"], - failed: ["delivered", "accepted"], + failed: ["queued", "delivered", "accepted"], queued: ["delivered"], } as const satisfies Record; diff --git a/apps/api/src/modules/runtime/infrastructure/session-runs/session-run-admission.repository.ts b/apps/api/src/modules/runtime/infrastructure/session-runs/session-run-admission.repository.ts index d00162aa..1d06d123 100644 --- a/apps/api/src/modules/runtime/infrastructure/session-runs/session-run-admission.repository.ts +++ b/apps/api/src/modules/runtime/infrastructure/session-runs/session-run-admission.repository.ts @@ -18,6 +18,7 @@ import type { SessionMessageId, SessionRunId, } from "@mosoo/id"; +import { createRuntimeEventSemanticHash } from "@mosoo/runtime-events"; import type { RuntimeEventEnvelope } from "@mosoo/runtime-events"; import { and, eq, exists, inArray, isNull, ne, notExists, or, sql } from "drizzle-orm"; import type { SQL } from "drizzle-orm"; @@ -57,6 +58,10 @@ interface QueuedMessageAdmissionRecord { timestampMs: number; } +type SourcedRuntimeEventEnvelope = RuntimeEventEnvelope & { + readonly sourceEventId: string; +}; + export interface CommitQueuedSessionRunAdmissionInput { apiCommand: PreparedApiCommand; clientRequestId: string | null; @@ -82,9 +87,50 @@ function admissionSessionPredicate(input: CommitQueuedSessionRunAdmissionInput) eq(sessionsTable.appId, input.session.appId), eq(sessionsTable.lastRunId, input.run.id), eq(sessionsTable.status, "RUNNING"), + isNull(sessionsTable.runtimeProvisioningOperationId), ); } +export function cattleTerminalCheckpointReadyPredicate(db: AppDatabase): SQL { + return or( + ne(sessionsTable.kind, "cattle"), + eq(sessionsTable.workspaceCheckpointRequired, false), + isNull(sessionsTable.lastRunId), + notExists( + db + .select({ id: sessionRunsTable.id }) + .from(sessionRunsTable) + .where( + and( + eq(sessionRunsTable.id, sessionsTable.lastRunId), + eq(sessionRunsTable.status, "completed"), + ), + ), + ), + exists( + db + .select({ id: sandboxBackupsTable.id }) + .from(sandboxBackupsTable) + .innerJoin( + sandboxSessionsTable, + and( + eq(sandboxSessionsTable.sessionId, sessionsTable.id), + eq(sandboxSessionsTable.sandboxId, sandboxBackupsTable.sandboxId), + eq(sandboxSessionsTable.sandboxIncarnation, sandboxBackupsTable.sandboxIncarnation), + eq(sandboxSessionsTable.cwd, sandboxBackupsTable.dir), + eq(sandboxBackupsTable.workspaceSessionId, sandboxSessionsTable.sessionId), + ), + ) + .where( + and( + eq(sandboxBackupsTable.sessionRunId, sessionsTable.lastRunId), + eq(sandboxBackupsTable.status, "ready"), + ), + ), + ), + )!; +} + function claimableSessionPredicate(db: AppDatabase, input: CommitQueuedSessionRunAdmissionInput) { return and( eq(sessionsTable.id, input.session.id), @@ -93,41 +139,8 @@ function claimableSessionPredicate(db: AppDatabase, input: CommitQueuedSessionRu isNull(sessionsTable.archivedAt), eq(sessionsTable.status, "IDLE"), isNull(sessionsTable.statusOperationId), - or( - ne(sessionsTable.kind, "cattle"), - eq(sessionsTable.workspaceCheckpointRequired, false), - isNull(sessionsTable.lastRunId), - notExists( - db - .select({ id: sessionRunsTable.id }) - .from(sessionRunsTable) - .where( - and( - eq(sessionRunsTable.id, sessionsTable.lastRunId), - eq(sessionRunsTable.status, "completed"), - ), - ), - ), - exists( - db - .select({ id: sandboxBackupsTable.id }) - .from(sandboxBackupsTable) - .innerJoin( - sandboxSessionsTable, - and( - eq(sandboxSessionsTable.sessionId, sessionsTable.id), - eq(sandboxSessionsTable.sandboxId, sandboxBackupsTable.sandboxId), - eq(sandboxSessionsTable.cwd, sandboxBackupsTable.dir), - ), - ) - .where( - and( - eq(sandboxBackupsTable.sessionRunId, sessionsTable.lastRunId), - eq(sandboxBackupsTable.status, "ready"), - ), - ), - ), - ), + isNull(sessionsTable.runtimeProvisioningOperationId), + cattleTerminalCheckpointReadyPredicate(db), notExists( db .select({ id: sessionRunsTable.id }) @@ -161,52 +174,21 @@ export async function isCattleTerminalCheckpointReadyForNextRun( sessionId: SessionId, ): Promise { const appDb = getAppDatabase(database); - const session = - (await appDb - .select({ - kind: sessionsTable.kind, - lastRunId: sessionsTable.lastRunId, - lastRunStatus: sessionRunsTable.status, - workspaceCheckpointRequired: sessionsTable.workspaceCheckpointRequired, - }) + const [session, ready] = await Promise.all([ + appDb + .select({ id: sessionsTable.id }) .from(sessionsTable) - .leftJoin(sessionRunsTable, eq(sessionRunsTable.id, sessionsTable.lastRunId)) .where(eq(sessionsTable.id, sessionId)) .limit(1) - .get()) ?? null; - - if ( - session === null || - session.kind !== "cattle" || - !session.workspaceCheckpointRequired || - session.lastRunId === null || - session.lastRunStatus !== "completed" - ) { - return true; - } - - const checkpoint = - (await appDb - .select({ id: sandboxBackupsTable.id }) - .from(sandboxBackupsTable) - .innerJoin( - sandboxSessionsTable, - and( - eq(sandboxSessionsTable.sessionId, sessionId), - eq(sandboxSessionsTable.sandboxId, sandboxBackupsTable.sandboxId), - eq(sandboxSessionsTable.cwd, sandboxBackupsTable.dir), - ), - ) - .where( - and( - eq(sandboxBackupsTable.sessionRunId, session.lastRunId), - eq(sandboxBackupsTable.status, "ready"), - ), - ) + .get(), + appDb + .select({ id: sessionsTable.id }) + .from(sessionsTable) + .where(and(eq(sessionsTable.id, sessionId), cattleTerminalCheckpointReadyPredicate(appDb))) .limit(1) - .get()) ?? null; - - return checkpoint !== null; + .get(), + ]); + return session === undefined || ready !== undefined; } export async function hasSessionRunAdmissionClientRequestReceipt( @@ -274,6 +256,7 @@ function createRunInsertQuery(db: AppDatabase, input: CommitQueuedSessionRunAdmi errorCode: selectedValue(null, "error_code"), errorDetailsJson: selectedValue(null, "error_details_json"), errorMessage: selectedValue(null, "error_message"), + errorRetryable: selectedValue(null, "error_retryable"), id: selectedValue(input.run.id, "id"), model: selectedValue(input.run.model, "model"), provider: selectedValue(input.run.provider, "provider"), @@ -286,6 +269,10 @@ function createRunInsertQuery(db: AppDatabase, input: CommitQueuedSessionRunAdmi statusOperationId: selectedValue(null, "status_operation_id"), statusSeq: selectedValue(0, "status_seq"), statusSource: selectedValue("api", "status_source"), + terminalReconciliationAttemptedAt: selectedValue( + null, + "terminal_reconciliation_attempted_at", + ), traceId: selectedValue(input.run.traceId, "trace_id"), trigger: selectedValue(input.run.trigger, "trigger"), updatedAt: selectedValue(input.run.timestampMs, "updated_at"), @@ -307,6 +294,7 @@ function createMessageInsertQuery(db: AppDatabase, input: CommitQueuedSessionRun ), id: selectedValue(input.message.id, "id"), planJson: selectedValue(null, "plan_json"), + projectionFormat: selectedValue("materialized" as const, "projection_format"), role: selectedValue("user" as const, "role"), segmentsJson: selectedValue(null, "segments_json"), seq: sessionsTable.messageSeqCursor, @@ -333,41 +321,74 @@ function runtimeEventOccurredAt(event: RuntimeEventEnvelope, fallbackMs: number) return Number.isFinite(occurredAt) ? occurredAt : fallbackMs; } +function sourceAdmissionEvent( + event: RuntimeEventEnvelope, + index: number, + clientRequestId: string | null, +): SourcedRuntimeEventEnvelope { + return { + ...event, + sourceEventId: event.sourceEventId ?? (index === 0 ? clientRequestId : null) ?? event.id, + }; +} + function createEventInsertQuery( db: AppDatabase, input: CommitQueuedSessionRunAdmissionInput, - event: RuntimeEventEnvelope, + event: SourcedRuntimeEventEnvelope, index: number, + semanticHash: string, ) { const projection = createSessionRuntimeEventProjection(event); const timestampMs = input.run.timestampMs + index; const occurredAt = runtimeEventOccurredAt(event, timestampMs); - const sourceEventId = - event.sourceEventId ?? (index === 0 ? input.clientRequestId : null) ?? event.id; return db.insert(sessionEventsTable).select( db .select({ agentId: sessionsTable.agentId, + artifactAttemptId: selectedValue(null, "artifact_attempt_id"), + artifactManifestJson: selectedValue(null, "artifact_manifest_json"), + artifactManifestSha256: selectedValue(null, "artifact_manifest_sha256"), contentText: selectedValue(projection.contentText, "content_text"), createdAt: selectedValue(timestampMs, "created_at"), endedAt: selectedValue(Math.max(occurredAt, timestampMs), "ended_at"), eventType: selectedValue(projection.eventType, "event_type"), family: selectedValue(projection.family, "family"), id: selectedValue(event.id, "id"), + mcpCommandId: selectedValue(projection.mcpCommandId, "mcp_command_id"), occurredAt: selectedValue(occurredAt, "occurred_at"), processStatus: selectedValue(projection.processStatus, "process_status"), processType: selectedValue(projection.processType, "process_type"), runId: selectedValue(projection.runId, "run_id"), + runtimeOperationEventJson: selectedValue(null, "runtime_operation_event_json"), + semanticHash: selectedValue(semanticHash, "semantic_hash"), seq: sql`${sessionsTable.runtimeEventSeqCursor} - ${input.events.length - index - 1}`.as( "seq", ), sessionId: sessionsTable.id, - sourceEventId: selectedValue(sourceEventId, "source_event_id"), + sourceEventId: selectedValue(event.sourceEventId, "source_event_id"), source: selectedValue(projection.source, "source"), + streamId: selectedValue(projection.streamId, "stream_id"), + terminalEventJson: selectedValue(null, "terminal_event_json"), toolCallId: selectedValue(projection.toolCallId, "tool_call_id"), + toolInputDeltaJson: selectedValue(projection.toolInputDeltaJson, "tool_input_delta_json"), toolInputJson: selectedValue(projection.toolInputJson, "tool_input_json"), toolName: selectedValue(projection.toolName, "tool_name"), + toolOutputDeltaText: selectedValue( + projection.toolOutputDeltaText, + "tool_output_delta_text", + ), + toolOutputText: selectedValue(projection.toolOutputText, "tool_output_text"), + toolParentMessageId: selectedValue( + projection.toolParentMessageId, + "tool_parent_message_id", + ), + toolResultMessageId: selectedValue( + projection.toolResultMessageId, + "tool_result_message_id", + ), + toolStatus: selectedValue(projection.toolStatus, "tool_status"), tokens: selectedValue(projection.tokens, "tokens"), traceId: selectedValue(projection.traceId, "trace_id"), visibility: selectedValue(projection.visibility, "visibility"), @@ -399,6 +420,7 @@ function createApiCommandInsertQuery(db: AppDatabase, input: CommitQueuedSession completedAt: selectedValue(record.completedAt, "completed_at"), createdAt: selectedValue(record.createdAt, "created_at"), dedupeKey: selectedValue(record.dedupeKey, "dedupe_key"), + deliveryGeneration: selectedValue(record.deliveryGeneration, "delivery_generation"), id: selectedValue(record.id, "id"), kind: selectedValue(record.kind, "kind"), lastErrorCode: selectedValue(record.lastErrorCode, "last_error_code"), @@ -436,42 +458,58 @@ export async function commitQueuedSessionRunAdmission( } } - const results = await runAppDatabaseBatch(database, (db) => [ - createRunInsertQuery(db, input), - db - .update(sessionsTable) - .set({ - lastMessageAt: input.message.timestampMs, - lastRunId: input.run.id, - messageSeqCursor: sql`${sessionsTable.messageSeqCursor} + 1`, - model: sql`COALESCE(${input.run.model}, ${sessionsTable.model})`, - provider: sql`COALESCE(${input.run.provider}, ${sessionsTable.provider})`, - runtimeEventSeqCursor: sql`${sessionsTable.runtimeEventSeqCursor} + ${input.events.length}`, - ...createSessionStatusTransitionPatch({ - status: "RUNNING", - timestampMs: input.run.timestampMs, - }), - }) - .where( - and( - eq(sessionsTable.id, input.session.id), - eq(sessionsTable.agentId, input.session.agentId), - eq(sessionsTable.appId, input.session.appId), - isNull(sessionsTable.archivedAt), - eq(sessionsTable.status, "IDLE"), - isNull(sessionsTable.statusOperationId), - exists( - db - .select({ id: sessionRunsTable.id }) - .from(sessionRunsTable) - .where(eq(sessionRunsTable.id, input.run.id)), + const events = input.events.map((event, index) => + sourceAdmissionEvent(event, index, input.clientRequestId), + ); + const semanticHashes = await Promise.all(events.map(createRuntimeEventSemanticHash)); + + const results = await runAppDatabaseBatch(database, (db) => { + const eventInserts = events.map((event, index) => { + const semanticHash = semanticHashes[index]; + if (semanticHash === undefined) { + throw new Error("Queued Session Run admission event hash is missing."); + } + return createEventInsertQuery(db, input, event, index, semanticHash); + }); + + return [ + createRunInsertQuery(db, input), + db + .update(sessionsTable) + .set({ + lastMessageAt: input.message.timestampMs, + lastRunId: input.run.id, + messageSeqCursor: sql`${sessionsTable.messageSeqCursor} + 1`, + model: sql`COALESCE(${input.run.model}, ${sessionsTable.model})`, + provider: sql`COALESCE(${input.run.provider}, ${sessionsTable.provider})`, + runtimeEventSeqCursor: sql`${sessionsTable.runtimeEventSeqCursor} + ${input.events.length}`, + ...createSessionStatusTransitionPatch({ + status: "RUNNING", + timestampMs: input.run.timestampMs, + }), + }) + .where( + and( + eq(sessionsTable.id, input.session.id), + eq(sessionsTable.agentId, input.session.agentId), + eq(sessionsTable.appId, input.session.appId), + isNull(sessionsTable.archivedAt), + eq(sessionsTable.status, "IDLE"), + isNull(sessionsTable.statusOperationId), + isNull(sessionsTable.runtimeProvisioningOperationId), + exists( + db + .select({ id: sessionRunsTable.id }) + .from(sessionRunsTable) + .where(eq(sessionRunsTable.id, input.run.id)), + ), ), ), - ), - createMessageInsertQuery(db, input), - ...input.events.map((event, index) => createEventInsertQuery(db, input, event, index)), - createApiCommandInsertQuery(db, input), - ]); + createMessageInsertQuery(db, input), + ...eventInserts, + createApiCommandInsertQuery(db, input), + ]; + }); return getD1ChangeCount((results as readonly unknown[])[0]) > 0; } diff --git a/apps/api/src/modules/runtime/infrastructure/session-runs/session-run-read.repository.ts b/apps/api/src/modules/runtime/infrastructure/session-runs/session-run-read.repository.ts index ac084e40..e62ca9b8 100644 --- a/apps/api/src/modules/runtime/infrastructure/session-runs/session-run-read.repository.ts +++ b/apps/api/src/modules/runtime/infrastructure/session-runs/session-run-read.repository.ts @@ -23,6 +23,7 @@ function sessionRunSummaryColumns() { error_code: sessionRunsTable.errorCode, error_details_json: sessionRunsTable.errorDetailsJson, error_message: sessionRunsTable.errorMessage, + error_retryable: sessionRunsTable.errorRetryable, id: sessionRunsTable.id, model: sessionRunsTable.model, provider: sessionRunsTable.provider, diff --git a/apps/api/src/modules/runtime/infrastructure/session-runs/session-run-row.mapper.ts b/apps/api/src/modules/runtime/infrastructure/session-runs/session-run-row.mapper.ts index d8ac7393..ef534c10 100644 --- a/apps/api/src/modules/runtime/infrastructure/session-runs/session-run-row.mapper.ts +++ b/apps/api/src/modules/runtime/infrastructure/session-runs/session-run-row.mapper.ts @@ -1,5 +1,5 @@ +import { DurableRunError } from "@mosoo/contracts/session-run"; import type { - RunError, SessionRunStatus, SessionRunSummary, SessionRunTrigger, @@ -27,6 +27,7 @@ export interface SessionRunRow { error_code: string | null; error_details_json: string | null; error_message: string | null; + error_retryable?: boolean | null; id: SessionRunId; model: string | null; provider: string | null; @@ -86,7 +87,7 @@ function parseRunErrorDetailsJson(raw: string): unknown { } } -function toRunError(row: SessionRunRow): RunError | null { +function toRunError(row: SessionRunRow): DurableRunError | null { if ( row.error_code === null || row.error_code === "" || @@ -96,10 +97,10 @@ function toRunError(row: SessionRunRow): RunError | null { return null; } - return { + return parseSchemaValue(DurableRunError, { code: row.error_code, details: parseJsonRecord(row.error_details_json), message: row.error_message, - retryable: false, - }; + retryable: row.error_retryable ?? false, + }); } diff --git a/apps/api/src/modules/runtime/infrastructure/session-runs/session-run-store.repository.ts b/apps/api/src/modules/runtime/infrastructure/session-runs/session-run-store.repository.ts index 7f190df5..f1ceccd6 100644 --- a/apps/api/src/modules/runtime/infrastructure/session-runs/session-run-store.repository.ts +++ b/apps/api/src/modules/runtime/infrastructure/session-runs/session-run-store.repository.ts @@ -5,7 +5,6 @@ export { hasActiveSessionRun, } from "./session-run-read.repository"; export { - cancelActiveSessionRunsForRuntimeOperation, createSessionRunRecordIfSessionIdle, SessionRunCreationGuardRejectedError, setSessionRunStatus, diff --git a/apps/api/src/modules/runtime/infrastructure/session-runs/session-run-write.repository.ts b/apps/api/src/modules/runtime/infrastructure/session-runs/session-run-write.repository.ts index ce1e4c3f..b71d48f4 100644 --- a/apps/api/src/modules/runtime/infrastructure/session-runs/session-run-write.repository.ts +++ b/apps/api/src/modules/runtime/infrastructure/session-runs/session-run-write.repository.ts @@ -1,7 +1,5 @@ -import type { AgentKind } from "@mosoo/contracts/agent"; -import type { SessionStatus, SessionType } from "@mosoo/contracts/session"; +import type { SessionStatus } from "@mosoo/contracts/session"; import type { - RunError, SessionRunStatus, SessionRunSummary, SessionRunTrigger, @@ -17,10 +15,9 @@ import type { SessionRunId, } from "@mosoo/id"; import { generateTraceId } from "@mosoo/observability"; -import { and, eq, exists, inArray, notInArray, sql } from "drizzle-orm"; +import { and, eq, exists, isNull, notInArray, sql } from "drizzle-orm"; import type { SQL } from "drizzle-orm"; -import { createErrorLogContext, logInfo, logWarn } from "../../../../platform/cloudflare/logger"; import { getAppDatabase, getD1ChangeCount, @@ -30,7 +27,6 @@ import { currentTimestampMs, toIsoString } from "../../../../time"; import { toSessionLifecycleStatusForRunStatus } from "../../../sessions/domain/session-lifecycle"; import type { BoundCapabilityRunProvenance } from "../../domain/bound-capability-run-provenance"; import { - ACTIVE_SESSION_RUN_STATUSES, decideSessionRunTransition, isTerminalSessionRunStatus, toSessionRunStatusLifecycleEventName, @@ -42,10 +38,8 @@ import type { ActiveSessionRunStatus } from "./session-run-row.mapper"; import { updateSessionLastRun } from "./session-run-session.repository"; type SessionRunStatusUpdateInput = { - error?: RunError | null; - operationId?: RuntimeOperationId | null; source?: SessionRunTransitionSource; - status: SessionRunStatus; + status: ActiveSessionRunStatus; }; type UpdateSessionRunStatusInput = SessionRunStatusUpdateInput & { @@ -54,7 +48,6 @@ type UpdateSessionRunStatusInput = SessionRunStatusUpdateInput & { * check is atomic with the write via the status_seq optimistic guard. */ expectedCurrentStatus?: SessionRunStatus; - preserveSessionLifecycle?: boolean; runId: SessionRunId; }; @@ -77,8 +70,6 @@ type SessionRunTransitionSource = | "system" | "viewer"; -const SESSION_RUN_STATUS_WRITE_BATCH_SIZE = 50; - /** * The caller supplied an atomic predicate for Run creation and it no longer * held when the INSERT statement executed. No Run record was created. @@ -98,15 +89,19 @@ interface LoadedSessionRunLifecycleRow { error_code: string | null; error_details_json: string | null; error_message: string | null; + error_retryable: boolean | null; id: SessionRunId; model: string | null; provider: string | null; runtime_id: string; + session_archived_at: number | null; + session_cleanup_operation_kind: "archive" | "delete" | null; session_id: SessionId; - session_kind: AgentKind; session_last_run_id: SessionRunId | null; session_status: SessionStatus; - session_type: SessionType; + session_status_operation_id: RuntimeOperationId | null; + session_status_seq: number; + session_updated_at: number; started_at: number | null; status: SessionRunStatus; status_seq: number; @@ -120,7 +115,7 @@ export type SessionRunTransitionOutcome = kind: "applied"; previousStatus: SessionRunStatus; run: SessionRunSummary; - sessionLifecycle: "current_last_run_updated" | "not_current_last_run" | "preserved"; + sessionLifecycle: "current_last_run_updated" | "not_current_last_run"; statusSeq: number; } | { @@ -154,17 +149,13 @@ export type SessionRunTransitionOutcome = statusSeq: number; }; -export interface NonTerminalSessionRunsStatusUpdateResult { - readonly runIds: readonly SessionRunId[]; - readonly timestampMs: number; -} - function createSessionRunStatusUpdate(input: SessionRunStatusUpdateInput, timestampMs: number) { return { - completedAt: isTerminalSessionRunStatus(input.status) ? timestampMs : undefined, - errorCode: input.error?.code ?? null, - errorDetailsJson: input.error ? JSON.stringify(input.error.details) : null, - errorMessage: input.error?.message ?? null, + completedAt: undefined, + errorCode: null, + errorDetailsJson: null, + errorMessage: null, + errorRetryable: null, startedAt: input.status === "queued" ? undefined @@ -172,7 +163,7 @@ function createSessionRunStatusUpdate(input: SessionRunStatusUpdateInput, timest status: input.status, statusChangedAt: timestampMs, statusEvent: toSessionRunStatusLifecycleEventName(input.status), - statusOperationId: input.operationId ?? null, + statusOperationId: null, statusSeq: sql`${sessionRunsTable.statusSeq} + 1`, statusSource: input.source ?? "system", updatedAt: timestampMs, @@ -180,51 +171,13 @@ function createSessionRunStatusUpdate(input: SessionRunStatusUpdateInput, timest } function createCurrentSessionRunProjectionPatch(input: { - readonly sessionKind: AgentKind; - readonly status: SessionRunStatus; + readonly status: ActiveSessionRunStatus; readonly timestampMs: number; }) { - return { - ...createSessionStatusTransitionPatch({ - status: toSessionLifecycleStatusForRunStatus(input.status), - timestampMs: input.timestampMs, - }), - ...(input.sessionKind === "cattle" && input.status === "completed" - ? { workspaceCheckpointRequired: true } - : {}), - }; -} - -function logTerminalSessionRun( - current: LoadedSessionRunLifecycleRow, - input: SessionRunStatusUpdateInput, - timestampMs: number, -): void { - if (!isTerminalSessionRunStatus(input.status)) return; - - try { - logInfo("session.run.terminal", { - durationMs: Math.max(0, timestampMs - (current.started_at ?? current.created_at)), - endToEndMs: Math.max(0, timestampMs - current.created_at), - errorCode: input.error?.code ?? null, - runId: current.id, - runtimeId: current.runtime_id, - sessionType: current.session_type, - source: input.source ?? "system", - status: input.status, - traceId: current.trace_id, - trigger: current.trigger, - }); - } catch (error) { - try { - logWarn("session.run.terminal_log.failed", { - ...createErrorLogContext(error), - runId: current.id, - }); - } catch { - // Observability must never turn a committed Run transition into an API failure. - } - } + return createSessionStatusTransitionPatch({ + status: toSessionLifecycleStatusForRunStatus(input.status), + timestampMs: input.timestampMs, + }); } function applySessionRunStatusUpdate( @@ -234,17 +187,8 @@ function applySessionRunStatusUpdate( ): SessionRunSummary { return { ...run, - completedAt: isTerminalSessionRunStatus(input.status) - ? toIsoString(timestampMs) - : run.completedAt, - error: input.error - ? { - code: input.error.code, - details: input.error.details, - message: input.error.message, - retryable: input.error.retryable, - } - : null, + completedAt: run.completedAt, + error: null, startedAt: input.status === "queued" ? run.startedAt : (run.startedAt ?? toIsoString(timestampMs)), status: input.status, @@ -261,15 +205,19 @@ function sessionRunLifecycleColumns() { error_code: sessionRunsTable.errorCode, error_details_json: sessionRunsTable.errorDetailsJson, error_message: sessionRunsTable.errorMessage, + error_retryable: sessionRunsTable.errorRetryable, id: sessionRunsTable.id, model: sessionRunsTable.model, provider: sessionRunsTable.provider, runtime_id: sessionsTable.runtimeId, + session_archived_at: sessionsTable.archivedAt, + session_cleanup_operation_kind: sessionsTable.cleanupOperationKind, session_id: sessionRunsTable.sessionId, - session_kind: sessionsTable.kind, session_last_run_id: sessionsTable.lastRunId, session_status: sessionsTable.status, - session_type: sessionsTable.type, + session_status_operation_id: sessionsTable.statusOperationId, + session_status_seq: sessionsTable.statusSeq, + session_updated_at: sessionsTable.updatedAt, started_at: sessionRunsTable.startedAt, status: sessionRunsTable.status, status_seq: sessionRunsTable.statusSeq, @@ -279,6 +227,22 @@ function sessionRunLifecycleColumns() { }; } +function writableObservedSessionCondition(current: LoadedSessionRunLifecycleRow) { + return and( + eq(sessionsTable.id, current.session_id), + current.session_last_run_id === null + ? isNull(sessionsTable.lastRunId) + : eq(sessionsTable.lastRunId, current.session_last_run_id), + eq(sessionsTable.status, current.session_status), + notInArray(sessionsTable.status, ["TERMINATED"]), + eq(sessionsTable.statusSeq, current.session_status_seq), + eq(sessionsTable.updatedAt, current.session_updated_at), + isNull(sessionsTable.archivedAt), + isNull(sessionsTable.cleanupOperationKind), + isNull(sessionsTable.statusOperationId), + ); +} + function toSessionRunSummaryFromLifecycleRow(row: LoadedSessionRunLifecycleRow): SessionRunSummary { return toSessionRunSummary({ completed_at: row.completed_at, @@ -288,6 +252,7 @@ function toSessionRunSummaryFromLifecycleRow(row: LoadedSessionRunLifecycleRow): error_code: row.error_code, error_details_json: row.error_details_json, error_message: row.error_message, + error_retryable: row.error_retryable, id: row.id, model: row.model, provider: row.provider, @@ -324,6 +289,7 @@ export function createInsertedSessionRunSummary( error_code: null, error_details_json: null, error_message: null, + error_retryable: null, id: identifiers.runId, model: input.model ?? null, provider: input.provider ?? null, @@ -393,6 +359,7 @@ export async function createSessionRunRecordIfSessionIdle( error_code, error_message, error_details_json, + error_retryable, started_at, completed_at, created_by_account_id, @@ -425,6 +392,7 @@ export async function createSessionRunRecordIfSessionIdle( NULL, NULL, NULL, + NULL, ${input.startedAt ?? null}, NULL, ${input.createdBy}, @@ -498,130 +466,10 @@ export async function setSessionRunStatus( database: D1Database, input: UpdateSessionRunStatusInput, ): Promise { - return transitionSessionRunStatusAt(database, input, currentTimestampMs()); -} - -export async function cancelActiveSessionRunsForRuntimeOperation( - database: D1Database, - input: { - readonly error: RunError; - readonly operationId: RuntimeOperationId; - readonly runIds: readonly SessionRunId[]; - }, -): Promise { - const runIds = [...new Set(input.runIds)].filter((runId) => runId !== ""); - - if (runIds.length === 0) { - return { - runIds: [], - timestampMs: currentTimestampMs(), - }; + if (isTerminalSessionRunStatus(input.status)) { + throw new Error("Terminal Session Run transitions require the atomic terminal projection."); } - - const cancelledRunIds = new Set(); - let timestampMs = currentTimestampMs(); - - for (let index = 0; index < runIds.length; index += SESSION_RUN_STATUS_WRITE_BATCH_SIZE) { - const runIdBatch = runIds.slice(index, index + SESSION_RUN_STATUS_WRITE_BATCH_SIZE); - const updated = await setNonTerminalSessionRunsStatus(database, { - error: input.error, - operationId: input.operationId, - preserveSessionLifecycle: true, - runIds: runIdBatch, - source: "runtime_operation", - status: "cancelled", - }); - timestampMs = updated.timestampMs; - - const rows = await getAppDatabase(database) - .select({ id: sessionRunsTable.id }) - .from(sessionRunsTable) - .where( - and( - inArray(sessionRunsTable.id, runIdBatch), - eq(sessionRunsTable.status, "cancelled"), - eq(sessionRunsTable.statusOperationId, input.operationId), - ), - ) - .all(); - - for (const row of rows) { - cancelledRunIds.add(row.id); - } - } - - return { - runIds: [...cancelledRunIds], - timestampMs, - }; -} - -async function setNonTerminalSessionRunsStatus( - database: D1Database, - input: SessionRunStatusUpdateInput & { - readonly preserveSessionLifecycle?: boolean; - readonly runIds: readonly SessionRunId[]; - }, -): Promise { - const runIds = [...new Set(input.runIds)].filter((runId) => runId !== ""); - const timestampMs = currentTimestampMs(); - - if (runIds.length === 0) { - return { - runIds: [], - timestampMs, - }; - } - - const updatedRuns = await getAppDatabase(database) - .update(sessionRunsTable) - .set(createSessionRunStatusUpdate(input, timestampMs)) - .where( - and( - inArray(sessionRunsTable.id, runIds), - inArray(sessionRunsTable.status, ACTIVE_SESSION_RUN_STATUSES), - ), - ) - .returning({ - id: sessionRunsTable.id, - sessionId: sessionRunsTable.sessionId, - }) - .all(); - - if (input.preserveSessionLifecycle === true || updatedRuns.length === 0) { - return { - runIds: updatedRuns.map((run) => run.id), - timestampMs, - }; - } - - await getAppDatabase(database) - .update(sessionsTable) - .set( - createSessionStatusTransitionPatch({ - status: toSessionLifecycleStatusForRunStatus(input.status), - timestampMs, - }), - ) - .where( - and( - inArray( - sessionsTable.id, - updatedRuns.map((run) => run.sessionId), - ), - inArray( - sessionsTable.lastRunId, - updatedRuns.map((run) => run.id), - ), - notInArray(sessionsTable.status, ["TERMINATED"]), - ), - ) - .run(); - - return { - runIds: updatedRuns.map((run) => run.id), - timestampMs, - }; + return transitionSessionRunStatusAt(database, input, currentTimestampMs()); } async function transitionSessionRunStatusAt( @@ -646,12 +494,22 @@ async function repairCurrentSessionRunProjection( const projectedStatus = toSessionLifecycleStatusForRunStatus(input.targetStatus); - if (input.current.session_status === projectedStatus) { + if ( + input.current.session_status === projectedStatus && + input.current.session_archived_at === null && + input.current.session_cleanup_operation_kind === null && + input.current.session_status_operation_id === null + ) { return "already_projected"; } - if (input.current.session_status === "TERMINATED") { - return "already_projected"; + if ( + input.current.session_archived_at !== null || + input.current.session_cleanup_operation_kind !== null || + input.current.session_status_operation_id !== null || + input.current.session_status === "TERMINATED" + ) { + return "repair_needed"; } const sessionUpdateResult = await getAppDatabase(database) @@ -662,13 +520,7 @@ async function repairCurrentSessionRunProjection( timestampMs: input.timestampMs, }), ) - .where( - and( - eq(sessionsTable.id, input.current.session_id), - eq(sessionsTable.lastRunId, input.current.id), - notInArray(sessionsTable.status, ["TERMINATED"]), - ), - ) + .where(writableObservedSessionCondition(input.current)) .run(); return getD1ChangeCount(sessionUpdateResult) > 0 ? "repaired" : "repair_needed"; @@ -715,22 +567,20 @@ async function transitionSessionRunStatus( break; } case "duplicate": { - if (input.preserveSessionLifecycle !== true) { - const projection = await repairCurrentSessionRunProjection(database, { - current, - targetStatus: input.status, - timestampMs, - }); - - if (projection === "repair_needed") { - return { - kind: "repair_needed", - previousStatus: current.status, - reason: "session_lifecycle_not_updated", - run: toSessionRunSummaryFromLifecycleRow(current), - statusSeq: current.status_seq, - }; - } + const projection = await repairCurrentSessionRunProjection(database, { + current, + targetStatus: input.status, + timestampMs, + }); + + if (projection === "repair_needed") { + return { + kind: "repair_needed", + previousStatus: current.status, + reason: "session_lifecycle_not_updated", + run: toSessionRunSummaryFromLifecycleRow(current), + statusSeq: current.status_seq, + }; } return { @@ -761,39 +611,6 @@ async function transitionSessionRunStatus( const run = toUpdatedSessionRunSummary(current, input, timestampMs); const statusSeq = current.status_seq + 1; - if (input.preserveSessionLifecycle === true) { - const runUpdateResult = await getAppDatabase(database) - .update(sessionRunsTable) - .set(createSessionRunStatusUpdate(input, timestampMs)) - .where( - and( - eq(sessionRunsTable.id, input.runId), - eq(sessionRunsTable.status, current.status), - eq(sessionRunsTable.statusSeq, current.status_seq), - ), - ) - .run(); - - if (getD1ChangeCount(runUpdateResult) === 0) { - return { - currentStatus: current.status, - kind: "stale", - reason: "concurrent_transition", - targetStatus: input.status, - }; - } - - logTerminalSessionRun(current, input, timestampMs); - - return { - kind: "applied", - previousStatus: current.status, - run, - sessionLifecycle: "preserved", - statusSeq, - }; - } - if (current.session_last_run_id !== input.runId) { const runUpdateResult = await getAppDatabase(database) .update(sessionRunsTable) @@ -803,6 +620,12 @@ async function transitionSessionRunStatus( eq(sessionRunsTable.id, input.runId), eq(sessionRunsTable.status, current.status), eq(sessionRunsTable.statusSeq, current.status_seq), + exists( + getAppDatabase(database) + .select({ id: sessionsTable.id }) + .from(sessionsTable) + .where(writableObservedSessionCondition(current)), + ), ), ) .run(); @@ -816,8 +639,6 @@ async function transitionSessionRunStatus( }; } - logTerminalSessionRun(current, input, timestampMs); - return { kind: "applied", previousStatus: current.status, @@ -836,22 +657,25 @@ async function transitionSessionRunStatus( eq(sessionRunsTable.id, input.runId), eq(sessionRunsTable.status, current.status), eq(sessionRunsTable.statusSeq, current.status_seq), + exists( + db + .select({ id: sessionsTable.id }) + .from(sessionsTable) + .where(writableObservedSessionCondition(current)), + ), ), ), db .update(sessionsTable) .set( createCurrentSessionRunProjectionPatch({ - sessionKind: current.session_kind, status: input.status, timestampMs, }), ) .where( and( - eq(sessionsTable.id, current.session_id), - eq(sessionsTable.lastRunId, input.runId), - notInArray(sessionsTable.status, ["TERMINATED"]), + writableObservedSessionCondition(current), exists( db .select({ id: sessionRunsTable.id }) @@ -877,8 +701,6 @@ async function transitionSessionRunStatus( }; } - logTerminalSessionRun(current, input, timestampMs); - if (getD1ChangeCount(sessionUpdateResult) === 0 && current.session_status !== "TERMINATED") { return { kind: "repair_needed", diff --git a/apps/api/src/modules/sessions/application/session-cleanup.service.ts b/apps/api/src/modules/sessions/application/session-cleanup.service.ts index 935f3c79..b420fcb7 100644 --- a/apps/api/src/modules/sessions/application/session-cleanup.service.ts +++ b/apps/api/src/modules/sessions/application/session-cleanup.service.ts @@ -8,24 +8,26 @@ import { import { createPlatformId } from "@mosoo/id"; import type { RuntimeOperationId, SessionId, SessionRunId } from "@mosoo/id"; import type { SQL } from "drizzle-orm"; -import { and, asc, eq, exists, inArray, isNotNull, lte, or, sql } from "drizzle-orm"; +import { and, asc, eq, exists, inArray, isNotNull, isNull, lte, or, sql } from "drizzle-orm"; import { createErrorLogContext, logWarn } from "../../../platform/cloudflare/logger"; import type { ApiBindings } from "../../../platform/cloudflare/worker-types"; import { getAppDatabase } from "../../../platform/db/drizzle"; import { currentTimestampMs } from "../../../time"; import { fileStore } from "../../files/application/file-store"; +import { recordCanonicalSessionRunTerminal } from "../../runtime/application/session-runs/session-run-terminal-failure.service"; +import { assertCanonicalTerminalSessionRunProjection } from "../../runtime/application/session-runs/terminal-run-reconciliation.service"; +import { + ACTIVE_SESSION_RUN_STATUSES, + isTerminalSessionRunStatus, +} from "../../runtime/domain/session-run-lifecycle.machine"; import { destroyDriverInstanceDurableObject } from "../../runtime/infrastructure/driver-instance/client"; -import { listLiveDriverInstanceIdsForSandboxSessions } from "../../runtime/infrastructure/driver-instance/live-driver-instance.repository"; +import { listLiveDriverInstanceRefsForSandboxSessions } from "../../runtime/infrastructure/driver-instance/live-driver-instance.repository"; import { stopDriverSession } from "../../runtime/infrastructure/driver-session-stop.service"; -import { deleteSandboxBackupsForDir } from "../../runtime/infrastructure/sandbox-backup.service"; +import { deleteSandboxBackupsForSession } from "../../runtime/infrastructure/sandbox-backup.service"; import { closeSandboxConversationSession } from "../../runtime/infrastructure/sandbox-session.service"; -import { - SESSION_DELETE_CLEANUP_STEPS, - completeSessionDeleteCleanupStep, - shouldSkipSessionDeleteCleanupStep, - skipSessionDeleteCleanupStep, -} from "../domain/session-cleanup-plan"; +import { getSessionRunSummary } from "../../runtime/infrastructure/session-runs/session-run-store.repository"; +import { SESSION_DELETE_CLEANUP_STEPS } from "../domain/session-cleanup-plan"; import type { SessionDeleteCleanupStep, SessionDeleteCleanupStepOutcome, @@ -36,14 +38,26 @@ import { destroySessionDurableObject } from "../infrastructure/session/client"; type AppDatabase = ReturnType; export interface SessionDeleteCleanupRepairCandidate { + readonly archivedAt: number; + readonly cleanupOperationKind: "delete" | null; readonly operationId: RuntimeOperationId; readonly sessionId: SessionId; + readonly status: "IDLE" | "RESCHEDULING" | "TERMINATED"; + readonly statusSeq: number; + readonly updatedAt: number; } export interface DeleteSessionCascadeOptions { readonly operationId?: RuntimeOperationId; } +const DELETED_RUN_ERROR = { + code: "session.deleted", + details: {}, + message: "Session was deleted before the run completed.", + retryable: false, +} as const; + function driverInstancesForSessionCondition( db: AppDatabase, sessionId: SessionId, @@ -76,6 +90,8 @@ async function resolveSessionDeleteCleanupOperationId( const existing = (await getAppDatabase(database) .select({ + archived_at: sessionsTable.archivedAt, + cleanup_operation_kind: sessionsTable.cleanupOperationKind, operation_id: sessionsTable.statusOperationId, status: sessionsTable.status, }) @@ -84,7 +100,21 @@ async function resolveSessionDeleteCleanupOperationId( .limit(1) .get()) ?? null; - if (existing?.status === "TERMINATED" && existing.operation_id !== null) { + if ( + existing?.operation_id !== null && + existing?.operation_id !== undefined && + existing.cleanup_operation_kind === "delete" && + (existing.status === "IDLE" || existing.status === "RESCHEDULING") + ) { + return existing.operation_id; + } + if ( + existing?.archived_at !== null && + existing?.archived_at !== undefined && + existing.cleanup_operation_kind === null && + existing.status === "TERMINATED" && + existing.operation_id !== null + ) { return existing.operation_id; } @@ -98,18 +128,59 @@ async function admitSessionDeleteCleanup( readonly sessionId: SessionId; readonly timestampMs: number; }, -): Promise { - await getAppDatabase(database) +): Promise { + const row = await getAppDatabase(database) .update(sessionsTable) .set({ archivedAt: sql`COALESCE(${sessionsTable.archivedAt}, ${input.timestampMs})`, - status: "TERMINATED", + cleanupOperationKind: "delete", + status: sql`CASE + WHEN ${sessionsTable.statusOperationId} = ${input.operationId} + AND ${sessionsTable.status} IN ('IDLE', 'RESCHEDULING') + THEN ${sessionsTable.status} + ELSE 'RESCHEDULING' + END`, statusOperationId: input.operationId, - statusSeq: sql`${sessionsTable.statusSeq} + 1`, - updatedAt: input.timestampMs, + statusSeq: sql`${sessionsTable.statusSeq} + CASE + WHEN ${sessionsTable.statusOperationId} = ${input.operationId} + AND ${sessionsTable.status} IN ('IDLE', 'RESCHEDULING') + THEN 0 + ELSE 1 + END`, + updatedAt: sql`MAX(${sessionsTable.updatedAt}, ${input.timestampMs})`, }) - .where(eq(sessionsTable.id, input.sessionId)) - .run(); + .where( + and( + eq(sessionsTable.id, input.sessionId), + isNull(sessionsTable.runtimeProvisioningOperationId), + or( + and( + eq(sessionsTable.cleanupOperationKind, "delete"), + eq(sessionsTable.statusOperationId, input.operationId), + inArray(sessionsTable.status, ["IDLE", "RESCHEDULING"]), + ), + and(isNull(sessionsTable.cleanupOperationKind), isNull(sessionsTable.statusOperationId)), + and( + eq(sessionsTable.cleanupOperationKind, "archive"), + eq(sessionsTable.status, "IDLE"), + isNull(sessionsTable.statusOperationId), + ), + and( + isNotNull(sessionsTable.archivedAt), + isNull(sessionsTable.cleanupOperationKind), + eq(sessionsTable.status, "TERMINATED"), + eq(sessionsTable.statusOperationId, input.operationId), + ), + ), + ), + ) + .returning({ archivedAt: sessionsTable.archivedAt }) + .get(); + + if (row?.archivedAt === null || row?.archivedAt === undefined) { + throw new Error("Session delete cleanup could not acquire lifecycle ownership."); + } + return row.archivedAt; } async function listSessionDeleteCleanupRepairCandidates( @@ -125,14 +196,25 @@ async function listSessionDeleteCleanupRepairCandidates( const rows = await getAppDatabase(database) .select({ + archivedAt: sessionsTable.archivedAt, + cleanupOperationKind: sessionsTable.cleanupOperationKind, operationId: sessionsTable.statusOperationId, sessionId: sessionsTable.id, + status: sessionsTable.status, + statusSeq: sessionsTable.statusSeq, + updatedAt: sessionsTable.updatedAt, }) .from(sessionsTable) .where( and( isNotNull(sessionsTable.archivedAt), - eq(sessionsTable.status, "TERMINATED"), + or( + and( + eq(sessionsTable.cleanupOperationKind, "delete"), + inArray(sessionsTable.status, ["IDLE", "RESCHEDULING"]), + ), + and(isNull(sessionsTable.cleanupOperationKind), eq(sessionsTable.status, "TERMINATED")), + ), isNotNull(sessionsTable.statusOperationId), lte(sessionsTable.updatedAt, input.staleUpdatedAtLte), ), @@ -141,9 +223,120 @@ async function listSessionDeleteCleanupRepairCandidates( .limit(input.limit) .all(); - return rows.flatMap((row) => - row.operationId === null ? [] : [{ operationId: row.operationId, sessionId: row.sessionId }], - ); + return rows.flatMap((row) => { + if ( + row.archivedAt === null || + row.operationId === null || + !["IDLE", "RESCHEDULING", "TERMINATED"].includes(row.status) || + (row.cleanupOperationKind !== null && row.cleanupOperationKind !== "delete") + ) { + return []; + } + return [ + { + archivedAt: row.archivedAt, + cleanupOperationKind: row.cleanupOperationKind, + operationId: row.operationId, + sessionId: row.sessionId, + status: row.status as "IDLE" | "RESCHEDULING" | "TERMINATED", + statusSeq: row.statusSeq, + updatedAt: row.updatedAt, + }, + ]; + }); +} + +async function normalizeSessionDeleteRuntimeLifecycle( + bindings: ApiBindings, + input: { + readonly operationId: RuntimeOperationId; + readonly sessionId: SessionId; + readonly timestampMs: number; + }, +): Promise { + const activeRuns = await getAppDatabase(bindings.DB) + .select({ id: sessionRunsTable.id }) + .from(sessionRunsTable) + .where( + and( + eq(sessionRunsTable.sessionId, input.sessionId), + inArray(sessionRunsTable.status, ACTIVE_SESSION_RUN_STATUSES), + ), + ) + .all(); + + if (activeRuns.length > 1) { + throw new Error("Session delete found more than one active Session Run."); + } + const [activeRun] = activeRuns; + if (activeRun !== undefined) { + const outcome = await recordCanonicalSessionRunTerminal(bindings, { + assistantMessage: null, + deliver: false, + error: DELETED_RUN_ERROR, + expectedSessionOperationId: input.operationId, + lifecycle: "IDLE", + runId: activeRun.id, + sessionId: input.sessionId, + source: "runtime_operation", + status: "cancelled", + timestampMs: input.timestampMs, + }); + if ( + outcome.kind === "stale" && + !["cancelled", "completed", "expired", "failed"].includes(outcome.run.status) + ) { + throw new Error("Session delete lost ownership of its active Session Run."); + } + } + + await getAppDatabase(bindings.DB) + .update(sessionsTable) + .set({ + status: "IDLE", + statusSeq: sql`${sessionsTable.statusSeq} + 1`, + updatedAt: sql`MAX(${sessionsTable.updatedAt}, ${input.timestampMs})`, + }) + .where( + and( + eq(sessionsTable.id, input.sessionId), + eq(sessionsTable.status, "RESCHEDULING"), + eq(sessionsTable.statusOperationId, input.operationId), + ), + ) + .run(); + + const projection = await getAppDatabase(bindings.DB) + .select({ + cleanupOperationKind: sessionsTable.cleanupOperationKind, + lastRunId: sessionsTable.lastRunId, + operationId: sessionsTable.statusOperationId, + status: sessionsTable.status, + }) + .from(sessionsTable) + .where(eq(sessionsTable.id, input.sessionId)) + .limit(1) + .get(); + if ( + projection === undefined || + projection.cleanupOperationKind !== "delete" || + projection.operationId !== input.operationId || + projection.status !== "IDLE" + ) { + throw new Error("Session delete cleanup lost its lifecycle ownership."); + } + if (projection.lastRunId === null) { + return; + } + const lastRun = await getSessionRunSummary(bindings.DB, projection.lastRunId); + if (lastRun === null || !isTerminalSessionRunStatus(lastRun.status)) { + throw new Error("Session delete cleanup did not reach a terminal current Run."); + } + await assertCanonicalTerminalSessionRunProjection(bindings, { + runId: lastRun.id, + sessionId: input.sessionId, + status: lastRun.status, + }); } export async function deleteSessionCascade( @@ -159,6 +352,7 @@ export async function deleteSessionCascade( sessionId, }); const outcomes: SessionDeleteCleanupStepOutcome[] = []; + let cleanupTimestampMs = timestampMs; let targets: SessionDeleteCleanupTargets | null = null; async function loadCleanupTargets(): Promise { @@ -170,7 +364,7 @@ export async function deleteSessionCascade( .limit(1) .get()) ?? null; - const liveDriverInstanceIds = await listLiveDriverInstanceIdsForSandboxSessions(bindings.DB, [ + const liveDriverInstances = await listLiveDriverInstanceRefsForSandboxSessions(bindings.DB, [ sessionId, ]); @@ -181,23 +375,41 @@ export async function deleteSessionCascade( .all(); const runIds = sessionRuns.map((row) => row.id); const associatedDriverInstanceRows = await db - .select({ id: driverInstancesTable.id }) + .select({ generation: driverInstancesTable.generation, id: driverInstancesTable.id }) .from(driverInstancesTable) .where(driverInstancesForSessionCondition(db, sessionId, runIds)) .all(); return { - associatedDriverInstanceIds: associatedDriverInstanceRows.map((row) => row.id), - liveDriverInstanceIds, + associatedDriverInstances: associatedDriverInstanceRows, + liveDriverInstances, sandboxId: sandboxSession?.sandbox_id ?? null, - sessionId, }; } async function executeStep(step: SessionDeleteCleanupStep): Promise { + if (step !== "archive_session_row") { + const owned = await db + .update(sessionsTable) + .set({ updatedAt: sql`MAX(${sessionsTable.updatedAt}, ${currentTimestampMs()})` }) + .where( + and( + eq(sessionsTable.id, sessionId), + eq(sessionsTable.cleanupOperationKind, "delete"), + eq(sessionsTable.statusOperationId, operationId), + inArray(sessionsTable.status, ["IDLE", "RESCHEDULING"]), + ), + ) + .returning({ id: sessionsTable.id }) + .get(); + if (owned === undefined) { + throw new Error("Session delete cleanup lost its operation ownership."); + } + } + switch (step) { case "archive_session_row": { - await admitSessionDeleteCleanup(bindings.DB, { + cleanupTimestampMs = await admitSessionDeleteCleanup(bindings.DB, { operationId, sessionId, timestampMs, @@ -209,23 +421,29 @@ export async function deleteSessionCascade( return; } case "stop_live_drivers": { - await Promise.all( - requireCleanupTargets(targets).liveDriverInstanceIds.map((driverInstanceId) => + const results = await Promise.allSettled( + requireCleanupTargets(targets).liveDriverInstances.map((driver) => stopDriverSession(bindings, { - driverInstanceId, + driverInstanceId: driver.id, + expectedDriverGeneration: driver.generation, reason: "session.deleted", - terminalRun: { - error: { - code: "session.deleted", - details: {}, - message: "Session was deleted before the run completed.", - retryable: false, - }, - status: "cancelled", - }, }), ), ); + const failure = results.find( + (result): result is PromiseRejectedResult => result.status === "rejected", + ); + if (failure !== undefined) { + throw failure.reason; + } + return; + } + case "normalize_runtime_lifecycle": { + await normalizeSessionDeleteRuntimeLifecycle(bindings, { + operationId, + sessionId, + timestampMs: cleanupTimestampMs, + }); return; } case "close_sandbox_session": { @@ -241,11 +459,22 @@ export async function deleteSessionCascade( return; } case "destroy_driver_objects": { - await Promise.all( - requireCleanupTargets(targets).liveDriverInstanceIds.map((driverInstanceId) => - destroyDriverInstanceDurableObject(bindings, driverInstanceId, "session.deleted"), + const results = await Promise.allSettled( + requireCleanupTargets(targets).associatedDriverInstances.map((driver) => + destroyDriverInstanceDurableObject( + bindings, + driver.id, + driver.generation, + "session.deleted", + ), ), ); + const failure = results.find( + (result): result is PromiseRejectedResult => result.status === "rejected", + ); + if (failure !== undefined) { + throw failure.reason; + } return; } case "destroy_session_object": { @@ -253,7 +482,16 @@ export async function deleteSessionCascade( return; } case "delete_session_backups": { - await deleteSandboxBackupsForDir(bindings, { dir: sessionCwd }); + const sandboxId = requireCleanupTargets(targets).sandboxId; + if (sandboxId === null) { + return; + } + await deleteSandboxBackupsForSession(bindings, { + cwd: sessionCwd, + operationId, + sandboxId, + sessionId, + }); return; } case "delete_session_files": { @@ -264,8 +502,9 @@ export async function deleteSessionCascade( return; } case "delete_driver_rows": { - const associatedDriverInstanceIds = - requireCleanupTargets(targets).associatedDriverInstanceIds; + const associatedDriverInstanceIds = requireCleanupTargets( + targets, + ).associatedDriverInstances.map((driver) => driver.id); if (associatedDriverInstanceIds.length === 0) { return; } @@ -286,20 +525,26 @@ export async function deleteSessionCascade( } } + function shouldSkipStep(step: SessionDeleteCleanupStep): boolean { + if (targets === null) { + return false; + } + return ( + (step === "close_sandbox_session" && targets.sandboxId === null) || + (step === "stop_live_drivers" && targets.liveDriverInstances.length === 0) || + ((step === "delete_driver_rows" || step === "destroy_driver_objects") && + targets.associatedDriverInstances.length === 0) + ); + } + for (const step of SESSION_DELETE_CLEANUP_STEPS) { - if ( - targets !== null && - shouldSkipSessionDeleteCleanupStep({ - step, - targets, - }) - ) { - outcomes.push(skipSessionDeleteCleanupStep(step)); + if (shouldSkipStep(step)) { + outcomes.push({ status: "skipped", step }); continue; } await executeStep(step); - outcomes.push(completeSessionDeleteCleanupStep(step)); + outcomes.push({ status: "completed", step }); } return outcomes; @@ -312,15 +557,59 @@ export async function repairStaleSessionDeleteCleanups( readonly staleUpdatedAtLte: number; }, ): Promise { - const candidates = await listSessionDeleteCleanupRepairCandidates(bindings.DB, input); + const observed = await listSessionDeleteCleanupRepairCandidates(bindings.DB, input); + const candidates: SessionDeleteCleanupRepairCandidate[] = []; + for (const candidate of observed) { + const attemptAt = Math.max(currentTimestampMs(), candidate.updatedAt + 1); + const claimed = await getAppDatabase(bindings.DB) + .update(sessionsTable) + .set({ + cleanupOperationKind: "delete", + status: candidate.status === "TERMINATED" ? "RESCHEDULING" : candidate.status, + statusSeq: + candidate.status === "TERMINATED" + ? sql`${sessionsTable.statusSeq} + 1` + : sessionsTable.statusSeq, + updatedAt: attemptAt, + }) + .where( + and( + eq(sessionsTable.id, candidate.sessionId), + eq(sessionsTable.archivedAt, candidate.archivedAt), + candidate.cleanupOperationKind === null + ? isNull(sessionsTable.cleanupOperationKind) + : eq(sessionsTable.cleanupOperationKind, candidate.cleanupOperationKind), + eq(sessionsTable.status, candidate.status), + eq(sessionsTable.statusOperationId, candidate.operationId), + eq(sessionsTable.statusSeq, candidate.statusSeq), + eq(sessionsTable.updatedAt, candidate.updatedAt), + ), + ) + .returning({ id: sessionsTable.id }) + .get(); + if (claimed !== undefined) { + candidates.push(candidate); + } + } - await Promise.all( + await Promise.allSettled( candidates.map(async (candidate) => { try { await deleteSessionCascade(bindings, candidate.sessionId, { operationId: candidate.operationId, }); } catch (error) { + await getAppDatabase(bindings.DB) + .update(sessionsTable) + .set({ updatedAt: sql`MAX(${sessionsTable.updatedAt}, ${currentTimestampMs()})` }) + .where( + and( + eq(sessionsTable.id, candidate.sessionId), + eq(sessionsTable.cleanupOperationKind, "delete"), + eq(sessionsTable.statusOperationId, candidate.operationId), + ), + ) + .run(); logWarn("session.delete_cleanup.repair_failed", { ...createErrorLogContext(error), operationId: candidate.operationId, diff --git a/apps/api/src/modules/sessions/application/session-event-write.service.ts b/apps/api/src/modules/sessions/application/session-event-write.service.ts index d84ff2c8..6bfee501 100644 --- a/apps/api/src/modules/sessions/application/session-event-write.service.ts +++ b/apps/api/src/modules/sessions/application/session-event-write.service.ts @@ -1,6 +1,5 @@ -import type { AgUiSessionEvent } from "@mosoo/ag-ui-session"; import { createPlatformId } from "@mosoo/id"; -import type { RuntimeEventId, SessionId, SessionRunId } from "@mosoo/id"; +import type { DriverCommandId, RuntimeEventId, SessionId, SessionRunId } from "@mosoo/id"; import { createRuntimeEvent } from "@mosoo/runtime-events"; import type { RuntimeEventActor, @@ -19,8 +18,7 @@ import { persistSessionRuntimeEvents, } from "../infrastructure/session-runtime-event-store.repository"; import type { PersistSessionRuntimeEventsResult } from "../infrastructure/session-runtime-event-store.repository"; -import { appRuntimeEventsToSessionDeliveryEvents } from "./session-live-state.service"; -import { publishSessionViewerEvents } from "./session-viewer-events.service"; +import { syncSessionViewerState } from "./session-viewer-events.service"; export interface AppendOneSessionEventPerSessionResult { readonly persistedCount: number; @@ -42,17 +40,15 @@ export interface CreateSessionRuntimeEventInput { visibility?: RuntimeEventVisibility; } -async function publishSessionViewerEventsSafely( +async function syncSessionViewerStateSafely( bindings: ApiBindings, sessionId: SessionId, - events: AgUiSessionEvent[], ): Promise { try { - await publishSessionViewerEvents(bindings, sessionId, events); + await syncSessionViewerState(bindings, sessionId); } catch (error) { - logWarn("session.runtime_event.live_delivery_failed", { + logWarn("session.runtime_event.live_sync_failed", { ...createErrorLogContext(error), - eventCount: events.length, sessionId, }); } @@ -63,10 +59,8 @@ export async function publishPersistedSessionRuntimeEvents(input: { events: readonly RuntimeEventEnvelope[]; sessionId: SessionId; }): Promise { - const deliveryEvents = appRuntimeEventsToSessionDeliveryEvents(input.events); - - if (deliveryEvents.length > 0) { - await publishSessionViewerEventsSafely(input.bindings, input.sessionId, deliveryEvents); + if (input.events.length > 0) { + await syncSessionViewerStateSafely(input.bindings, input.sessionId); } } @@ -74,6 +68,7 @@ export interface AppendSessionRuntimeEventsInput { bindings: ApiBindings; deliver?: boolean; events: RuntimeEventEnvelope[]; + provenMcpCommandId?: DriverCommandId | null; sessionId: SessionId; sourceEventId?: string | null; } @@ -122,11 +117,15 @@ export async function appendSessionRuntimeEvents( persistedSourceEventIds: [], }; } + if (input.provenMcpCommandId != null && input.events.length !== 1) { + throw new Error("Proven MCP command provenance requires exactly one runtime event."); + } const result = await persistSessionRuntimeEvents(input.bindings.DB, { records: input.events.map((event, index) => ({ event, occurredAt: toRuntimeEventOccurredAtMs(event), + provenMcpCommandId: index === 0 ? (input.provenMcpCommandId ?? null) : null, sourceEventId: event.sourceEventId ?? (index === 0 ? (input.sourceEventId ?? null) : null), })), sessionId: input.sessionId, @@ -135,7 +134,7 @@ export async function appendSessionRuntimeEvents( if (input.deliver !== false) { await publishPersistedSessionRuntimeEvents({ bindings: input.bindings, - events: result.persistedEvents, + events: input.events, sessionId: input.sessionId, }); } @@ -167,19 +166,10 @@ export async function appendOneSessionRuntimeEventPerSession(input: { return result; } - const skippedSessionIds = new Set(result.skippedSessionIds); await Promise.all( - input.records.flatMap((record) => { - if (skippedSessionIds.has(record.sessionId)) { - return []; - } - - const deliveryEvents = appRuntimeEventsToSessionDeliveryEvents([record.event]); - - return deliveryEvents.length === 0 - ? [] - : [publishSessionViewerEventsSafely(input.bindings, record.sessionId, deliveryEvents)]; - }), + [...new Set(input.records.map((record) => record.sessionId))].map((sessionId) => + syncSessionViewerStateSafely(input.bindings, sessionId), + ), ); return result; diff --git a/apps/api/src/modules/sessions/application/session-lifecycle-mutation.service.ts b/apps/api/src/modules/sessions/application/session-lifecycle-mutation.service.ts index 8fc681f1..5fe95f04 100644 --- a/apps/api/src/modules/sessions/application/session-lifecycle-mutation.service.ts +++ b/apps/api/src/modules/sessions/application/session-lifecycle-mutation.service.ts @@ -1,22 +1,30 @@ import type { AgentSessionActionCapabilityName } from "@mosoo/contracts/session"; import { sandboxSessionsTable, sessionRunsTable, sessionsTable } from "@mosoo/db"; -import type { AppId, SessionId, SessionRunId } from "@mosoo/id"; +import { createPlatformId } from "@mosoo/id"; +import type { AppId, RuntimeOperationId, SessionId, SessionRunId } from "@mosoo/id"; import { getAvailableAgentSessionActionCapability } from "@mosoo/session-policy"; -import { and, eq, inArray } from "drizzle-orm"; +import { and, asc, eq, inArray, isNotNull, isNull, lte, ne, or, sql } from "drizzle-orm"; +import { createErrorLogContext, logWarn } from "../../../platform/cloudflare/logger"; import type { ApiBindings } from "../../../platform/cloudflare/worker-types"; import { getAppDatabase } from "../../../platform/db/drizzle"; import { forbiddenError } from "../../../platform/errors"; import { currentTimestampMs } from "../../../time"; import type { AuthenticatedViewer } from "../../auth/application/viewer-auth.service"; +import { recordCanonicalSessionRunTerminal } from "../../runtime/application/session-runs/session-run-terminal-failure.service"; +import { assertCanonicalTerminalSessionRunProjection } from "../../runtime/application/session-runs/terminal-run-reconciliation.service"; import { - createSessionStatusTransitionPatch, - setSystemSessionRunStatus, -} from "../../runtime/application/session-lifecycle-transition.service"; -import { ACTIVE_SESSION_RUN_STATUSES } from "../../runtime/domain/session-run-lifecycle.machine"; -import { listLiveDriverInstanceIdsForSandboxSessions } from "../../runtime/infrastructure/driver-instance/live-driver-instance.repository"; + ACTIVE_SESSION_RUN_STATUSES, + isTerminalSessionRunStatus, +} from "../../runtime/domain/session-run-lifecycle.machine"; +import { listLiveDriverInstanceRefsForSandboxSessions } from "../../runtime/infrastructure/driver-instance/live-driver-instance.repository"; import { stopDriverSession } from "../../runtime/infrastructure/driver-session-stop.service"; import { closeSandboxConversationSession } from "../../runtime/infrastructure/sandbox-session.service"; +import { + cattleTerminalCheckpointReadyPredicate, + isCattleTerminalCheckpointReadyForNextRun, +} from "../../runtime/infrastructure/session-runs/session-run-admission.repository"; +import { getSessionRunSummary } from "../../runtime/infrastructure/session-runs/session-run-store.repository"; import type { SessionActionAuthorization, SessionParticipantCapabilityAccessRow, @@ -26,12 +34,7 @@ import { lookupAppSessionParticipantCapabilityAccess, resolveSessionActionCreatorFlag, } from "../domain/session-access.policy"; -import { - SESSION_ARCHIVE_CLEANUP_STEPS, - completeSessionArchiveCleanupStep, - shouldSkipSessionArchiveCleanupStep, - skipSessionArchiveCleanupStep, -} from "../domain/session-cleanup-plan"; +import { SESSION_ARCHIVE_CLEANUP_STEPS } from "../domain/session-cleanup-plan"; import type { SessionArchiveCleanupStep, SessionArchiveCleanupStepOutcome, @@ -71,6 +74,130 @@ const ARCHIVED_RUN_ERROR = { retryable: false, } as const; +interface SessionArchiveCleanupClaim { + readonly operationId: RuntimeOperationId; + readonly timestampMs: number; +} + +async function readCurrentSessionArchiveCleanupClaim( + database: D1Database, + sessionId: SessionId, +): Promise { + const row = await getAppDatabase(database) + .select({ + cleanupOperationKind: sessionsTable.cleanupOperationKind, + operationId: sessionsTable.statusOperationId, + timestampMs: sessionsTable.archivedAt, + }) + .from(sessionsTable) + .where(eq(sessionsTable.id, sessionId)) + .limit(1) + .get(); + + return row?.cleanupOperationKind !== "archive" || + row.operationId === null || + row.timestampMs === null + ? null + : { operationId: row.operationId, timestampMs: row.timestampMs }; +} + +async function claimSessionArchiveCleanup( + bindings: ApiBindings, + input: { + readonly appId: AppId; + readonly sessionId: SessionId; + }, +): Promise { + const db = getAppDatabase(bindings.DB); + const current = await db + .select({ + archivedAt: sessionsTable.archivedAt, + cleanupOperationKind: sessionsTable.cleanupOperationKind, + operationId: sessionsTable.statusOperationId, + provisioningOperationId: sessionsTable.runtimeProvisioningOperationId, + status: sessionsTable.status, + statusSeq: sessionsTable.statusSeq, + updatedAt: sessionsTable.updatedAt, + }) + .from(sessionsTable) + .where(and(eq(sessionsTable.id, input.sessionId), eq(sessionsTable.appId, input.appId))) + .limit(1) + .get(); + if (current === undefined) { + throw new Error("Session was not found while claiming archive cleanup."); + } + if (current.provisioningOperationId !== null) { + throw new Error("Session runtime provisioning is still in progress."); + } + if (current.cleanupOperationKind === "archive") { + if (current.archivedAt === null) { + throw new Error("Session archive cleanup has an incomplete durable claim."); + } + if (current.operationId === null) { + return null; + } + return { operationId: current.operationId, timestampMs: current.archivedAt }; + } + const isLegacyArchive = + current.archivedAt !== null && + current.cleanupOperationKind === null && + (current.operationId === null || current.status !== "TERMINATED"); + if (current.cleanupOperationKind !== null || (current.operationId !== null && !isLegacyArchive)) { + throw new Error("Session is owned by another lifecycle operation."); + } + if (!(await isCattleTerminalCheckpointReadyForNextRun(bindings.DB, input.sessionId))) { + throw new Error("Session archive is waiting for its completed Run checkpoint."); + } + + const operationId = createPlatformId(); + const timestampMs = current.archivedAt ?? currentTimestampMs(); + const updatedAt = Math.max(currentTimestampMs(), current.updatedAt + 1); + const claimed = await db + .update(sessionsTable) + .set({ + archivedAt: timestampMs, + cleanupOperationKind: "archive", + status: "RESCHEDULING", + statusOperationId: operationId, + statusSeq: sql`${sessionsTable.statusSeq} + 1`, + updatedAt, + }) + .where( + and( + eq(sessionsTable.id, input.sessionId), + eq(sessionsTable.appId, input.appId), + current.archivedAt === null + ? isNull(sessionsTable.archivedAt) + : eq(sessionsTable.archivedAt, current.archivedAt), + isNull(sessionsTable.cleanupOperationKind), + current.operationId === null + ? isNull(sessionsTable.statusOperationId) + : eq(sessionsTable.statusOperationId, current.operationId), + isNull(sessionsTable.runtimeProvisioningOperationId), + eq(sessionsTable.status, current.status), + eq(sessionsTable.statusSeq, current.statusSeq), + eq(sessionsTable.updatedAt, current.updatedAt), + cattleTerminalCheckpointReadyPredicate(db), + ), + ) + .returning({ id: sessionsTable.id }) + .get(); + if (claimed !== undefined) { + return { operationId, timestampMs }; + } + + const winner = await readCurrentSessionArchiveCleanupClaim(bindings.DB, input.sessionId); + if (winner !== null) { + return winner; + } + + if (!(await isCattleTerminalCheckpointReadyForNextRun(bindings.DB, input.sessionId))) { + throw new Error("Session archive is waiting for its completed Run checkpoint."); + } + + throw new Error("Session archive cleanup lost its admission race."); +} + function ensureLifecycleActionCapability(input: { action: AgentSessionActionCapabilityName; authorization?: SessionActionAuthorization | undefined; @@ -106,46 +233,94 @@ async function listActiveSessionRunIds( return rows.map((row) => row.id); } -async function cancelActiveSessionRunsForLifecycle( - database: D1Database, +async function terminalizeActiveSessionRunsForLifecycle( + bindings: ApiBindings, sessionId: SessionId, + timestampMs: number, + operationId: RuntimeOperationId, ): Promise { - const activeRunIds = await listActiveSessionRunIds(database, sessionId); + const activeRunIds = await listActiveSessionRunIds(bindings.DB, sessionId); + + if (activeRunIds.length > 1) { + throw new Error("Session archive found more than one active Session Run."); + } for (const runId of activeRunIds) { - const outcome = await setSystemSessionRunStatus(database, { + const outcome = await recordCanonicalSessionRunTerminal(bindings, { + assistantMessage: null, + deliver: false, error: ARCHIVED_RUN_ERROR, + expectedSessionOperationId: operationId, + lifecycle: "IDLE", runId, + sessionId, + source: "runtime_operation", status: "cancelled", + timestampMs, }); - if (outcome.kind === "repair_needed") { - throw new Error("Session archive left the session lifecycle projection stale."); + if ( + outcome.kind === "stale" && + !["cancelled", "completed", "expired", "failed"].includes(outcome.run.status) + ) { + throw new Error("Session archive lost ownership of its active Session Run."); } } } async function normalizeSessionRuntimeLifecycle( - database: D1Database, + bindings: ApiBindings, sessionId: SessionId, + timestampMs: number, + operationId: RuntimeOperationId, ): Promise { - await cancelActiveSessionRunsForLifecycle(database, sessionId); + await terminalizeActiveSessionRunsForLifecycle(bindings, sessionId, timestampMs, operationId); - await getAppDatabase(database) + await getAppDatabase(bindings.DB) .update(sessionsTable) - .set( - createSessionStatusTransitionPatch({ - status: "IDLE", - timestampMs: currentTimestampMs(), - }), - ) + .set({ + status: "IDLE", + statusSeq: sql`${sessionsTable.statusSeq} + 1`, + updatedAt: sql`MAX(${sessionsTable.updatedAt}, ${timestampMs})`, + }) .where( and( eq(sessionsTable.id, sessionId), - inArray(sessionsTable.status, ["RUNNING", "RESCHEDULING"]), + eq(sessionsTable.status, "RESCHEDULING"), + eq(sessionsTable.statusOperationId, operationId), ), ) .run(); + + const projection = await getAppDatabase(bindings.DB) + .select({ + lastRunId: sessionsTable.lastRunId, + operationId: sessionsTable.statusOperationId, + status: sessionsTable.status, + }) + .from(sessionsTable) + .where(eq(sessionsTable.id, sessionId)) + .limit(1) + .get(); + if ( + projection === undefined || + projection.operationId !== operationId || + projection.status !== "IDLE" + ) { + throw new Error("Session archive cleanup lost its lifecycle ownership."); + } + if (projection.lastRunId === null) { + return; + } + const lastRun = await getSessionRunSummary(bindings.DB, projection.lastRunId); + if (lastRun === null || !isTerminalSessionRunStatus(lastRun.status)) { + throw new Error("Session archive cleanup did not reach a terminal current Run."); + } + await assertCanonicalTerminalSessionRunProjection(bindings, { + runId: lastRun.id, + sessionId, + status: lastRun.status, + }); } export async function archiveAgentSession({ @@ -159,12 +334,31 @@ export async function archiveAgentSession({ appId, sessionId, }); - ensureLifecycleActionCapability({ - action: "archive_session", - authorization, - session, - }); - const timestampMs = currentTimestampMs(); + const existingClaim = await readCurrentSessionArchiveCleanupClaim(bindings.DB, sessionId); + if (existingClaim === null && session.archived_at === null) { + ensureLifecycleActionCapability({ + action: "archive_session", + authorization, + session, + }); + } + const claim = existingClaim ?? (await claimSessionArchiveCleanup(bindings, { appId, sessionId })); + if (claim === null) { + return []; + } + return executeSessionArchiveCleanup(bindings, { appId, claim, sessionId }); +} + +async function executeSessionArchiveCleanup( + bindings: ApiBindings, + input: { + readonly appId: AppId; + readonly claim: SessionArchiveCleanupClaim; + readonly sessionId: SessionId; + }, +): Promise { + const { appId, sessionId } = input; + const { operationId, timestampMs } = input.claim; const outcomes: SessionArchiveCleanupStepOutcome[] = []; let targets: SessionArchiveCleanupTargets | null = null; @@ -177,28 +371,38 @@ export async function archiveAgentSession({ .limit(1) .get()) ?? null; - const liveDriverInstanceIds = await listLiveDriverInstanceIdsForSandboxSessions(bindings.DB, [ + const liveDriverInstances = await listLiveDriverInstanceRefsForSandboxSessions(bindings.DB, [ sessionId, ]); return { - liveDriverInstanceIds, + liveDriverInstances, sandboxId: sandboxSession?.sandbox_id ?? null, - sessionId, }; } async function executeStep(step: SessionArchiveCleanupStep): Promise { + const owned = await getAppDatabase(bindings.DB) + .update(sessionsTable) + .set({ updatedAt: sql`MAX(${sessionsTable.updatedAt}, ${currentTimestampMs()})` }) + .where( + and( + eq(sessionsTable.id, sessionId), + eq(sessionsTable.appId, appId), + eq(sessionsTable.archivedAt, timestampMs), + eq(sessionsTable.cleanupOperationKind, "archive"), + eq(sessionsTable.statusOperationId, operationId), + inArray(sessionsTable.status, ["IDLE", "RESCHEDULING"]), + ), + ) + .returning({ id: sessionsTable.id }) + .get(); + if (owned === undefined) { + throw new Error("Session archive cleanup lost its operation ownership."); + } + switch (step) { case "archive_session_row": { - await getAppDatabase(bindings.DB) - .update(sessionsTable) - .set({ - archivedAt: timestampMs, - updatedAt: timestampMs, - }) - .where(and(eq(sessionsTable.id, sessionId), eq(sessionsTable.appId, appId))) - .run(); return; } case "close_viewer_sockets": { @@ -210,22 +414,25 @@ export async function archiveAgentSession({ return; } case "stop_live_drivers": { - await Promise.all( - requireArchiveCleanupTargets(targets).liveDriverInstanceIds.map((driverInstanceId) => + const stopOutcomes = await Promise.allSettled( + requireArchiveCleanupTargets(targets).liveDriverInstances.map((driver) => stopDriverSession(bindings, { - driverInstanceId, + driverInstanceId: driver.id, + expectedDriverGeneration: driver.generation, reason: "session.archived", - terminalRun: { - error: ARCHIVED_RUN_ERROR, - status: "cancelled", - }, }), ), ); + const failure = stopOutcomes.find( + (outcome): outcome is PromiseRejectedResult => outcome.status === "rejected", + ); + if (failure !== undefined) { + throw failure.reason; + } return; } case "normalize_runtime_lifecycle": { - await normalizeSessionRuntimeLifecycle(bindings.DB, sessionId); + await normalizeSessionRuntimeLifecycle(bindings, sessionId, timestampMs, operationId); return; } case "close_sandbox_session": { @@ -240,26 +447,58 @@ export async function archiveAgentSession({ }); return; } + case "complete_archive_session": { + const completed = await getAppDatabase(bindings.DB) + .update(sessionsTable) + .set({ + archivedAt: timestampMs, + cleanupOperationKind: "archive", + status: "IDLE", + statusOperationId: null, + statusSeq: sql`${sessionsTable.statusSeq} + 1`, + updatedAt: sql`MAX(${sessionsTable.updatedAt}, ${timestampMs})`, + }) + .where( + and( + eq(sessionsTable.id, sessionId), + eq(sessionsTable.appId, appId), + eq(sessionsTable.archivedAt, timestampMs), + eq(sessionsTable.cleanupOperationKind, "archive"), + eq(sessionsTable.status, "IDLE"), + eq(sessionsTable.statusOperationId, operationId), + ), + ) + .returning({ id: sessionsTable.id }) + .get(); + if (completed === undefined) { + throw new Error("Session archive cleanup lost its operation ownership."); + } + return; + } default: { throw new Error("Unknown session archive cleanup step."); } } } + function shouldSkipStep(step: SessionArchiveCleanupStep): boolean { + if (targets === null) { + return false; + } + return ( + (step === "close_sandbox_session" && targets.sandboxId === null) || + (step === "stop_live_drivers" && targets.liveDriverInstances.length === 0) + ); + } + for (const step of SESSION_ARCHIVE_CLEANUP_STEPS) { - if ( - targets !== null && - shouldSkipSessionArchiveCleanupStep({ - step, - targets, - }) - ) { - outcomes.push(skipSessionArchiveCleanupStep(step)); + if (shouldSkipStep(step)) { + outcomes.push({ status: "skipped", step }); continue; } await executeStep(step); - outcomes.push(completeSessionArchiveCleanupStep(step)); + outcomes.push({ status: "completed", step }); } return outcomes; @@ -275,6 +514,126 @@ function requireArchiveCleanupTargets( return targets; } +export async function repairStaleSessionArchiveCleanups( + bindings: ApiBindings, + input: { + readonly limit: number; + readonly staleUpdatedAtLte: number; + }, +): Promise { + if (!Number.isSafeInteger(input.limit) || input.limit <= 0) { + throw new Error("Session archive cleanup repair limit must be a positive integer."); + } + + const rows = await getAppDatabase(bindings.DB) + .select({ + appId: sessionsTable.appId, + cleanupOperationKind: sessionsTable.cleanupOperationKind, + operationId: sessionsTable.statusOperationId, + sessionId: sessionsTable.id, + status: sessionsTable.status, + statusSeq: sessionsTable.statusSeq, + timestampMs: sessionsTable.archivedAt, + updatedAt: sessionsTable.updatedAt, + }) + .from(sessionsTable) + .where( + and( + isNotNull(sessionsTable.archivedAt), + or( + and( + eq(sessionsTable.cleanupOperationKind, "archive"), + inArray(sessionsTable.status, ["IDLE", "RESCHEDULING"]), + isNotNull(sessionsTable.statusOperationId), + ), + and( + isNull(sessionsTable.cleanupOperationKind), + or(isNull(sessionsTable.statusOperationId), ne(sessionsTable.status, "TERMINATED")), + ), + ), + lte(sessionsTable.updatedAt, input.staleUpdatedAtLte), + ), + ) + .orderBy(asc(sessionsTable.updatedAt), asc(sessionsTable.id)) + .limit(input.limit) + .all(); + const candidates: Array<{ + appId: AppId; + claim: SessionArchiveCleanupClaim; + sessionId: SessionId; + }> = []; + for (const row of rows) { + if (row.timestampMs === null) { + continue; + } + if (row.cleanupOperationKind === null) { + const claim = await claimSessionArchiveCleanup(bindings, { + appId: row.appId, + sessionId: row.sessionId, + }); + if (claim !== null) { + candidates.push({ appId: row.appId, claim, sessionId: row.sessionId }); + } + continue; + } + if (row.operationId === null) { + continue; + } + + const attemptAt = Math.max(currentTimestampMs(), row.updatedAt + 1); + const claimed = await getAppDatabase(bindings.DB) + .update(sessionsTable) + .set({ updatedAt: attemptAt }) + .where( + and( + eq(sessionsTable.id, row.sessionId), + eq(sessionsTable.archivedAt, row.timestampMs), + eq(sessionsTable.cleanupOperationKind, "archive"), + eq(sessionsTable.status, row.status), + eq(sessionsTable.statusOperationId, row.operationId), + eq(sessionsTable.statusSeq, row.statusSeq), + eq(sessionsTable.updatedAt, row.updatedAt), + ), + ) + .returning({ id: sessionsTable.id }) + .get(); + if (claimed !== undefined) { + candidates.push({ + appId: row.appId, + claim: { operationId: row.operationId, timestampMs: row.timestampMs }, + sessionId: row.sessionId, + }); + } + } + + await Promise.allSettled( + candidates.map(async (candidate) => { + try { + await executeSessionArchiveCleanup(bindings, candidate); + } catch (error) { + await getAppDatabase(bindings.DB) + .update(sessionsTable) + .set({ updatedAt: sql`MAX(${sessionsTable.updatedAt}, ${currentTimestampMs()})` }) + .where( + and( + eq(sessionsTable.id, candidate.sessionId), + eq(sessionsTable.cleanupOperationKind, "archive"), + eq(sessionsTable.statusOperationId, candidate.claim.operationId), + ), + ) + .run(); + logWarn("session.archive_cleanup.repair_failed", { + ...createErrorLogContext(error), + operationId: candidate.claim.operationId, + sessionId: candidate.sessionId, + }); + } + }), + ); + + return candidates.length; +} + export async function unarchiveAgentSession({ authorization, database, @@ -292,16 +651,31 @@ export async function unarchiveAgentSession({ session, }); - await normalizeSessionRuntimeLifecycle(database, sessionId); - - await getAppDatabase(database) + if (session.archived_at === null) { + throw new Error("Session is not archived."); + } + const unarchived = await getAppDatabase(database) .update(sessionsTable) .set({ archivedAt: null, + cleanupOperationKind: null, updatedAt: currentTimestampMs(), }) - .where(and(eq(sessionsTable.id, sessionId), eq(sessionsTable.appId, appId))) - .run(); + .where( + and( + eq(sessionsTable.id, sessionId), + eq(sessionsTable.appId, appId), + eq(sessionsTable.archivedAt, session.archived_at), + eq(sessionsTable.cleanupOperationKind, "archive"), + eq(sessionsTable.status, "IDLE"), + isNull(sessionsTable.statusOperationId), + ), + ) + .returning({ id: sessionsTable.id }) + .get(); + if (unarchived === undefined) { + throw new Error("Session cleanup is still in progress."); + } } export async function deleteAgentSession({ diff --git a/apps/api/src/modules/sessions/application/session-live-state.service.ts b/apps/api/src/modules/sessions/application/session-live-state.service.ts index fcfc69e6..ab951205 100644 --- a/apps/api/src/modules/sessions/application/session-live-state.service.ts +++ b/apps/api/src/modules/sessions/application/session-live-state.service.ts @@ -27,12 +27,6 @@ export function appRuntimeEventToSessionDeliveryEvents( return appRuntimeEventToAgUiSessionEvents(event); } -export function appRuntimeEventsToSessionDeliveryEvents( - events: readonly RuntimeEventEnvelope[], -): SessionDeliveryEvent[] { - return events.flatMap((event) => appRuntimeEventToSessionDeliveryEvents(event)); -} - export function applyRuntimeEventToSessionLiveState( state: SessionLiveState, event: RuntimeEventEnvelope, @@ -44,5 +38,11 @@ function applyRuntimeEventsToSessionLiveState( state: SessionLiveState, events: readonly RuntimeEventEnvelope[], ): SessionLiveState { - return applyAgUiEventsToSessionLiveState(state, appRuntimeEventsToSessionDeliveryEvents(events)); + let next = state; + + for (const event of events) { + next = applyAgUiEventsToSessionLiveState(next, appRuntimeEventToSessionDeliveryEvents(event)); + } + + return next; } diff --git a/apps/api/src/modules/sessions/application/session-message-query.service.ts b/apps/api/src/modules/sessions/application/session-message-query.service.ts index e9150aa8..ca18b719 100644 --- a/apps/api/src/modules/sessions/application/session-message-query.service.ts +++ b/apps/api/src/modules/sessions/application/session-message-query.service.ts @@ -6,6 +6,7 @@ import { asc, eq } from "drizzle-orm"; import { getAppDatabase } from "../../../platform/db/drizzle"; import type { AuthenticatedViewer } from "../../auth/application/viewer-auth.service"; import { ensureAppSessionParticipantAccess } from "../domain/session-access.policy"; +import { resolveStoredSessionMessageReferences } from "../infrastructure/session-message-reference.repository"; import { toSessionMessage } from "./session-message-mappers"; import { getSessionReadAccess } from "./session-read-access.service"; @@ -46,13 +47,17 @@ async function listSessionMessages( created_by_account_id: sessionMessagesTable.createdByAccountId, id: sessionMessagesTable.id, plan_json: sessionMessagesTable.planJson, + projection_format: sessionMessagesTable.projectionFormat, role: sessionMessagesTable.role, segments_json: sessionMessagesTable.segmentsJson, + session_run_id: sessionMessagesTable.sessionRunId, }) .from(sessionMessagesTable) .where(eq(sessionMessagesTable.sessionId, sessionId)) .orderBy(asc(sessionMessagesTable.seq)) .all(); - return results.map((row) => toSessionMessage(row)); + const resolved = await resolveStoredSessionMessageReferences(database, sessionId, results); + + return resolved.map((row) => toSessionMessage(row)); } diff --git a/apps/api/src/modules/sessions/application/session-message-write.service.ts b/apps/api/src/modules/sessions/application/session-message-write.service.ts deleted file mode 100644 index 30e01572..00000000 --- a/apps/api/src/modules/sessions/application/session-message-write.service.ts +++ /dev/null @@ -1,6 +0,0 @@ -export { - insertSessionMessageRecord, - insertSessionMessage, - type InsertedSessionMessage, - type InsertSessionMessageInput, -} from "../infrastructure/session-message-store.repository"; diff --git a/apps/api/src/modules/sessions/application/session-model-call.service.ts b/apps/api/src/modules/sessions/application/session-model-call.service.ts deleted file mode 100644 index 2dda2922..00000000 --- a/apps/api/src/modules/sessions/application/session-model-call.service.ts +++ /dev/null @@ -1,4 +0,0 @@ -export { - upsertSessionModelCallUsage, - type SessionModelCallStatus, -} from "../infrastructure/session-model-call.repository"; diff --git a/apps/api/src/modules/sessions/application/session-process-events.service.ts b/apps/api/src/modules/sessions/application/session-process-events.service.ts index 90544d14..0d2b4f43 100644 --- a/apps/api/src/modules/sessions/application/session-process-events.service.ts +++ b/apps/api/src/modules/sessions/application/session-process-events.service.ts @@ -5,14 +5,21 @@ import { import type { SessionProcessEvent } from "@mosoo/contracts/session"; import { sessionEventsTable } from "@mosoo/db"; import type { AccountId, AppId, RuntimeEventId, SessionId, SessionRunId } from "@mosoo/id"; -import { and, desc, eq } from "drizzle-orm"; +import { and, desc, eq, lt } from "drizzle-orm"; import { getAppDatabase } from "../../../platform/db/drizzle"; import { validationError } from "../../../platform/errors"; import { toIsoString } from "../../../time"; import type { AuthenticatedViewer } from "../../auth/application/viewer-auth.service"; import { getAppSessionParticipantTimelineAccess } from "../domain/session-access.policy"; -import { foldStreamedSessionEventRows } from "../domain/session-event-stream-fold"; +import { + excludeSessionEventStreams, + findLeftIncompleteSessionEventStreamKeys, + foldStreamedSessionEventRows, + getSessionEventStreamKey, +} from "../domain/session-event-stream-fold"; +import { formatStoredSessionEventContent } from "../domain/session-event-tool-content"; +import type { StoredToolStatus } from "../domain/session-event-tool-content"; export interface SessionEventProcessRow { content_text: string; @@ -24,11 +31,16 @@ export interface SessionEventProcessRow { process_type: SessionProcessEvent["type"]; run_id: SessionRunId | null; seq: number; + stream_id: string | null; + tool_name?: string | null; + tool_status?: StoredToolStatus | null; tokens: number | null; } const DEFAULT_PROCESS_EVENT_LIMIT = 500; const MAX_PROCESS_EVENT_LIMIT = 1000; +const PROCESS_EVENT_ROW_PAGE_SIZE = MAX_PROCESS_EVENT_LIMIT + 1; +const PROCESS_EVENT_RAW_ROW_SCAN_LIMIT = MAX_PROCESS_EVENT_LIMIT * 20; // Longer gaps may be permission or user waits, not continuous execution. const MAX_INFERRED_PROCESS_EVENT_DURATION_MS = 5 * 60 * 1000; @@ -45,6 +57,12 @@ interface SessionProcessEventAccess { updatedAt: string; } +interface SessionProcessEventWindow { + events: SessionProcessEvent[]; + recorded: boolean; + truncated: boolean; +} + function normalizeProcessEventLimit(limit: number | null | undefined): number { if (limit === null || limit === undefined) { return DEFAULT_PROCESS_EVENT_LIMIT; @@ -60,9 +78,7 @@ function normalizeProcessEventLimit(limit: number | null | undefined): number { function finalizeProcessEventDurations( projections: ProcessEventProjection[], ): SessionProcessEvent[] { - const sortedProjections = projections.toSorted( - (a, b) => a.startMs - b.startMs || a.order - b.order, - ); + const sortedProjections = projections.toSorted((a, b) => a.order - b.order); return sortedProjections.map((projection, index) => { const next = sortedProjections[index + 1] ?? null; @@ -91,13 +107,19 @@ function finalizeProcessEventDurations( }); } -function toProcessEventProjectionFromSessionEventRow( - row: SessionEventProcessRow, -): ProcessEventProjection { - return { +export function createSessionProcessEventsFromSessionEventRows( + rows: SessionEventProcessRow[], +): SessionProcessEvent[] { + const foldedRows = foldStreamedSessionEventRows(rows); + const projections = foldedRows.map((row) => ({ endMs: row.ended_at, event: { - content: row.content_text, + content: formatStoredSessionEventContent({ + contentText: row.content_text, + eventType: row.event_type, + toolName: row.tool_name ?? null, + toolStatus: row.tool_status ?? null, + }), durationMs: 0, id: row.id, occurredAt: toIsoString(row.occurred_at), @@ -108,18 +130,7 @@ function toProcessEventProjectionFromSessionEventRow( order: row.seq, runId: row.run_id, startMs: row.occurred_at, - }; -} - -export function createSessionProcessEventsFromSessionEventRows( - rows: SessionEventProcessRow[], - options: { foldStreamedRows?: boolean } = {}, -): SessionProcessEvent[] { - const foldedRows = - options.foldStreamedRows === false - ? rows - : foldStreamedSessionEventRows(rows, { flushOpenStreams: true }).rows; - const projections = foldedRows.map(toProcessEventProjectionFromSessionEventRow); + })); return finalizeProcessEventDurations(projections); } @@ -170,6 +181,107 @@ async function getThreadSessionProcessEventAccess( }; } +async function readSessionProcessEventWindow(input: { + database: D1Database; + limit: number; + sessionId: SessionId; +}): Promise { + const scannedRows: SessionEventProcessRow[] = []; + let beforeSeq: number | null = null; + let reachedStart = false; + + while (scannedRows.length < PROCESS_EVENT_RAW_ROW_SCAN_LIMIT) { + const rowCapacity = Math.min( + PROCESS_EVENT_ROW_PAGE_SIZE, + PROCESS_EVENT_RAW_ROW_SCAN_LIMIT - scannedRows.length, + ); + const querySize = rowCapacity + 1; + const filters = [ + eq(sessionEventsTable.sessionId, input.sessionId), + eq(sessionEventsTable.visibility, "all_consumers"), + ]; + + if (beforeSeq !== null) { + filters.push(lt(sessionEventsTable.seq, beforeSeq)); + } + + const page = await getAppDatabase(input.database) + .select({ + content_text: sessionEventsTable.contentText, + ended_at: sessionEventsTable.endedAt, + event_type: sessionEventsTable.eventType, + id: sessionEventsTable.id, + occurred_at: sessionEventsTable.occurredAt, + process_status: sessionEventsTable.processStatus, + process_type: sessionEventsTable.processType, + run_id: sessionEventsTable.runId, + seq: sessionEventsTable.seq, + stream_id: sessionEventsTable.streamId, + tool_name: sessionEventsTable.toolName, + tool_status: sessionEventsTable.toolStatus, + tokens: sessionEventsTable.tokens, + }) + .from(sessionEventsTable) + .where(and(...filters)) + .orderBy(desc(sessionEventsTable.seq)) + .limit(querySize) + .all(); + + if (page.length === 0) { + reachedStart = true; + break; + } + + const scannedPage = page.slice(0, rowCapacity); + scannedRows.push(...scannedPage); + beforeSeq = scannedPage[scannedPage.length - 1]?.seq ?? beforeSeq; + const chronologicalRows = scannedRows.toReversed(); + const foldedRows = foldStreamedSessionEventRows(chronologicalRows); + const events = createSessionProcessEventsFromSessionEventRows(chronologicalRows); + + if (events.length > input.limit) { + const reachedDatabaseStart = page.length <= rowCapacity; + const incompleteStreams = reachedDatabaseStart + ? new Set() + : findLeftIncompleteSessionEventStreamKeys(chronologicalRows); + const rowsByEventId = new Map(foldedRows.map((row) => [row.id, row])); + const retainedStreamsAreComplete = events.slice(-input.limit).every((event) => { + const row = rowsByEventId.get(event.id); + const key = row === undefined ? null : getSessionEventStreamKey(row); + return key === null || !incompleteStreams.has(key); + }); + + if (!retainedStreamsAreComplete) { + continue; + } + + return { + events: events.slice(-input.limit), + recorded: true, + truncated: true, + }; + } + + if (page.length <= rowCapacity) { + reachedStart = true; + break; + } + } + + const chronologicalRows = scannedRows.toReversed(); + const incompleteStreams = reachedStart + ? new Set() + : findLeftIncompleteSessionEventStreamKeys(chronologicalRows); + const completeRows = excludeSessionEventStreams(chronologicalRows, incompleteStreams); + const events = createSessionProcessEventsFromSessionEventRows(completeRows); + + return { + events: events.slice(-input.limit), + recorded: scannedRows.length > 0, + truncated: !reachedStart || events.length > input.limit, + }; +} + async function listSessionProcessEvents( database: D1Database, viewer: AuthenticatedViewer, @@ -184,56 +296,29 @@ async function listSessionProcessEvents( appId: input.appId, sessionId: input.sessionId, }); - const rows = await getAppDatabase(database) - .select({ - content_text: sessionEventsTable.contentText, - ended_at: sessionEventsTable.endedAt, - event_type: sessionEventsTable.eventType, - id: sessionEventsTable.id, - occurred_at: sessionEventsTable.occurredAt, - process_status: sessionEventsTable.processStatus, - process_type: sessionEventsTable.processType, - run_id: sessionEventsTable.runId, - seq: sessionEventsTable.seq, - tokens: sessionEventsTable.tokens, - }) - .from(sessionEventsTable) - .where( - and( - eq(sessionEventsTable.sessionId, input.sessionId), - eq(sessionEventsTable.visibility, "all_consumers"), - ), - ) - .orderBy(desc(sessionEventsTable.seq)) - .limit(limit + 1) - .all(); - - if (rows.length === 0) { + const window = await readSessionProcessEventWindow({ + database, + limit, + sessionId: input.sessionId, + }); + + if (!window.recorded) { return [createNoRuntimeEventsRecordedEvent(session)]; } - const hasMore = rows.length > limit; - const processEvents = createSessionProcessEventsFromSessionEventRows( - rows.slice(0, limit).toReversed(), - ); - - if (!hasMore) { - return processEvents; + if (!window.truncated) { + return window.events; } - const firstEvent = processEvents[0] ?? null; - - if (firstEvent === null) { - return processEvents; - } + const firstEvent = window.events[0] ?? null; return [ createProcessEventsTruncatedEvent({ limit, - occurredAt: firstEvent.occurredAt, + occurredAt: firstEvent?.occurredAt ?? session.updatedAt, sessionId: session.id, }), - ...processEvents, + ...window.events, ]; } diff --git a/apps/api/src/modules/sessions/application/session-runtime-recovery-query.service.ts b/apps/api/src/modules/sessions/application/session-runtime-recovery-query.service.ts index f33cebb5..76280aa0 100644 --- a/apps/api/src/modules/sessions/application/session-runtime-recovery-query.service.ts +++ b/apps/api/src/modules/sessions/application/session-runtime-recovery-query.service.ts @@ -5,6 +5,7 @@ import { and, desc, eq, isNull, ne, or } from "drizzle-orm"; import { getAppDatabase } from "../../../platform/db/drizzle"; import { sanitizeProviderPrivateMarkup } from "../domain/provider-private-markup"; +import { resolveStoredSessionMessageContentReferences } from "../infrastructure/session-message-reference.repository"; const MAX_RUNTIME_RECOVERY_MESSAGES = 100; // Replayed history rides the driver boot payload and, for prompt-replay @@ -22,8 +23,11 @@ export async function getSessionRuntimeRecoveryMessages( ): Promise { const rows = await getAppDatabase(database) .select({ - content: sessionMessagesTable.contentText, + content_text: sessionMessagesTable.contentText, + id: sessionMessagesTable.id, + projection_format: sessionMessagesTable.projectionFormat, role: sessionMessagesTable.role, + session_run_id: sessionMessagesTable.sessionRunId, }) .from(sessionMessagesTable) .where( @@ -40,31 +44,42 @@ export async function getSessionRuntimeRecoveryMessages( .orderBy(desc(sessionMessagesTable.seq)) .limit(MAX_RUNTIME_RECOVERY_MESSAGES) .all(); - const newestFirst: DriverRecoveryMessage[] = []; let remainingChars = MAX_RUNTIME_RECOVERY_CONTENT_CHARS; for (const row of rows) { + const resolvedRows = await resolveStoredSessionMessageContentReferences( + database, + input.sessionId, + [row], + remainingChars + 1, + ); + + const [resolved] = resolvedRows; + if (resolved === undefined) { + continue; + } const content = - row.role === "assistant" ? sanitizeProviderPrivateMarkup(row.content).text : row.content; + resolved.role === "assistant" + ? sanitizeProviderPrivateMarkup(resolved.content_text).text + : resolved.content_text; if (content.trim().length === 0) { continue; } if (content.length > remainingChars) { - // Keep the window contiguous: stop at the first message that no longer - // fits instead of skipping past it. The newest message alone is kept - // truncated so an oversized latest exchange cannot erase all context. if (newestFirst.length === 0) { - newestFirst.push({ content: content.slice(0, remainingChars), role: row.role }); + newestFirst.push({ content: content.slice(0, remainingChars), role: resolved.role }); } - - break; + return newestFirst.toReversed(); } - newestFirst.push({ content, role: row.role }); + newestFirst.push({ content, role: resolved.role }); remainingChars -= content.length; + if (remainingChars === 0) { + return newestFirst.toReversed(); + } } return newestFirst.toReversed(); diff --git a/apps/api/src/modules/sessions/application/session-title.service.ts b/apps/api/src/modules/sessions/application/session-title.service.ts index 163546fe..834ea743 100644 --- a/apps/api/src/modules/sessions/application/session-title.service.ts +++ b/apps/api/src/modules/sessions/application/session-title.service.ts @@ -1,6 +1,7 @@ import type { RenameSessionInput, SessionSummary } from "@mosoo/contracts/session"; import { sessionsTable } from "@mosoo/db"; -import { and, eq, isNull } from "drizzle-orm"; +import type { PlatformId, RuntimeEventId, SessionId } from "@mosoo/id"; +import { and, eq, isNotNull, isNull, lt, or } from "drizzle-orm"; import { getAppDatabase } from "../../../platform/db/drizzle"; import { currentTimestampMs } from "../../../time"; @@ -20,6 +21,91 @@ export interface RenameSessionRequest { viewer: AuthenticatedViewer; } +export interface DurableSessionAutoTitleInput { + creatorAccountId: PlatformId; + eventSeq: number; + sessionId: SessionId; + title: string; +} + +export function prepareDurableSessionAutoTitleProjection( + database: D1Database, + input: { + createdAt: number; + eventId: RuntimeEventId; + eventSeq: number; + semanticHash: string; + sessionId: SessionId; + title: string; + }, +): D1PreparedStatement[] { + if (!Number.isSafeInteger(input.eventSeq) || input.eventSeq < 0) { + throw new Error("Session auto-title event seq must be a non-negative safe integer."); + } + + const title = normalizeSessionTitle(input.title); + const receiptFence = `EXISTS ( + SELECT 1 + FROM session_event AS receipt + WHERE receipt.id = ? + AND receipt.session_id = ? + AND receipt.event_type = 'session.info.updated' + AND receipt.semantic_hash = ? + AND receipt.seq = ? + )`; + const update = database + .prepare( + `UPDATE session + SET auto_title_event_seq = ?, title = ?, updated_at = ? + WHERE id = ? + AND renamed = 0 + AND (title IS NULL OR auto_title_event_seq IS NOT NULL) + AND (auto_title_event_seq IS NULL OR auto_title_event_seq < ?) + AND ${receiptFence}`, + ) + .bind( + input.eventSeq, + title, + input.createdAt, + input.sessionId, + input.eventSeq, + input.eventId, + input.sessionId, + input.semanticHash, + input.eventSeq, + ); + const guard = database + .prepare( + `INSERT INTO session_event (id) + SELECT ? + WHERE ${receiptFence} + AND NOT EXISTS ( + SELECT 1 + FROM session + WHERE id = ? + AND ( + renamed = 1 + OR (auto_title_event_seq IS NULL AND title IS NOT NULL) + OR auto_title_event_seq > ? + OR (auto_title_event_seq = ? AND title = ?) + ) + )`, + ) + .bind( + input.eventId, + input.eventId, + input.sessionId, + input.semanticHash, + input.eventSeq, + input.sessionId, + input.eventSeq, + input.eventSeq, + title, + ); + + return [update, guard]; +} + async function hydrateUpdatedSessionSummary( database: D1Database, row: SessionSummaryRow, @@ -102,3 +188,69 @@ export async function autoTitleSession( return hydrateUpdatedSessionSummary(database, updated); } + +export async function applyDurableSessionAutoTitle( + database: D1Database, + input: DurableSessionAutoTitleInput, +): Promise { + if (!Number.isSafeInteger(input.eventSeq) || input.eventSeq < 0) { + throw new Error("Session auto-title event seq must be a non-negative safe integer."); + } + + const title = normalizeSessionTitle(input.title); + const appDatabase = getAppDatabase(database); + + await appDatabase + .update(sessionsTable) + .set({ + autoTitleEventSeq: input.eventSeq, + title, + updatedAt: currentTimestampMs(), + }) + .where( + and( + eq(sessionsTable.id, input.sessionId), + eq(sessionsTable.creatorAccountId, input.creatorAccountId), + eq(sessionsTable.renamed, false), + or(isNull(sessionsTable.title), isNotNull(sessionsTable.autoTitleEventSeq)), + or( + isNull(sessionsTable.autoTitleEventSeq), + lt(sessionsTable.autoTitleEventSeq, input.eventSeq), + ), + ), + ) + .run(); + + const stored = + (await appDatabase + .select({ + autoTitleEventSeq: sessionsTable.autoTitleEventSeq, + renamed: sessionsTable.renamed, + title: sessionsTable.title, + }) + .from(sessionsTable) + .where( + and( + eq(sessionsTable.id, input.sessionId), + eq(sessionsTable.creatorAccountId, input.creatorAccountId), + ), + ) + .limit(1) + .get()) ?? null; + + if (stored === null) { + throw new Error("Session not found for durable auto-title."); + } + + if (stored.renamed || (stored.autoTitleEventSeq === null && stored.title !== null)) { + return; + } + + if (stored.autoTitleEventSeq === null || stored.autoTitleEventSeq < input.eventSeq) { + throw new Error("Session auto-title CAS did not persist the durable event."); + } + + if (stored.autoTitleEventSeq === input.eventSeq && stored.title !== title) { + throw new Error("Session auto-title event seq was replayed with conflicting content."); + } +} diff --git a/apps/api/src/modules/sessions/application/session-viewer-events.service.ts b/apps/api/src/modules/sessions/application/session-viewer-events.service.ts index 1a89b17f..447dd447 100644 --- a/apps/api/src/modules/sessions/application/session-viewer-events.service.ts +++ b/apps/api/src/modules/sessions/application/session-viewer-events.service.ts @@ -1,4 +1 @@ -export { - publishSessionViewerEvents, - syncSessionViewerState, -} from "../infrastructure/session/client"; +export { syncSessionViewerState } from "../infrastructure/session/client"; diff --git a/apps/api/src/modules/sessions/domain/provider-private-markup.ts b/apps/api/src/modules/sessions/domain/provider-private-markup.ts index 6a3aee07..33a4a6db 100644 --- a/apps/api/src/modules/sessions/domain/provider-private-markup.ts +++ b/apps/api/src/modules/sessions/domain/provider-private-markup.ts @@ -1,7 +1,11 @@ -import { filterOpenAiPrivateCitations } from "@mosoo/agent-driver/provider-output"; +import { + filterOpenAiPrivateCitations, + OpenAiPrivateCitationStreamFilter, +} from "@mosoo/agent-driver/provider-output"; import type { SessionMessageSegment } from "@mosoo/contracts/session"; export const sanitizeProviderPrivateMarkup = filterOpenAiPrivateCitations; +export { OpenAiPrivateCitationStreamFilter as ProviderPrivateMarkupStreamFilter }; export function sanitizeAssistantMessageSegments( segments: SessionMessageSegment[], diff --git a/apps/api/src/modules/sessions/domain/session-cleanup-plan.ts b/apps/api/src/modules/sessions/domain/session-cleanup-plan.ts index 78a42d63..3401fd3e 100644 --- a/apps/api/src/modules/sessions/domain/session-cleanup-plan.ts +++ b/apps/api/src/modules/sessions/domain/session-cleanup-plan.ts @@ -1,4 +1,4 @@ -import type { DriverInstanceId, SandboxId, SessionId } from "@mosoo/id"; +import type { DriverInstanceId, SandboxId } from "@mosoo/id"; export const SESSION_ARCHIVE_CLEANUP_STEPS = [ "archive_session_row", @@ -7,12 +7,14 @@ export const SESSION_ARCHIVE_CLEANUP_STEPS = [ "stop_live_drivers", "normalize_runtime_lifecycle", "close_sandbox_session", + "complete_archive_session", ] as const; export const SESSION_DELETE_CLEANUP_STEPS = [ "archive_session_row", "load_cleanup_targets", "stop_live_drivers", + "normalize_runtime_lifecycle", "close_sandbox_session", "destroy_driver_objects", "destroy_session_object", @@ -24,104 +26,24 @@ export const SESSION_DELETE_CLEANUP_STEPS = [ export type SessionArchiveCleanupStep = (typeof SESSION_ARCHIVE_CLEANUP_STEPS)[number]; export type SessionDeleteCleanupStep = (typeof SESSION_DELETE_CLEANUP_STEPS)[number]; -export type SessionDeleteCleanupStepStatus = "completed" | "skipped"; -export type SessionArchiveCleanupStepStatus = "completed" | "skipped"; -export interface SessionArchiveCleanupStepOutcome { +export type SessionArchiveCleanupStepOutcome = { step: SessionArchiveCleanupStep; - status: SessionArchiveCleanupStepStatus; -} + status: "completed" | "skipped"; +}; -export interface SessionDeleteCleanupStepOutcome { +export type SessionDeleteCleanupStepOutcome = { step: SessionDeleteCleanupStep; - status: SessionDeleteCleanupStepStatus; -} + status: "completed" | "skipped"; +}; export interface SessionArchiveCleanupTargets { - liveDriverInstanceIds: readonly DriverInstanceId[]; + liveDriverInstances: readonly { generation: number; id: DriverInstanceId }[]; sandboxId: SandboxId | null; - sessionId: SessionId; } export interface SessionDeleteCleanupTargets { - associatedDriverInstanceIds: readonly DriverInstanceId[]; - liveDriverInstanceIds: readonly DriverInstanceId[]; + associatedDriverInstances: readonly { generation: number; id: DriverInstanceId }[]; + liveDriverInstances: readonly { generation: number; id: DriverInstanceId }[]; sandboxId: SandboxId | null; - sessionId: SessionId; -} - -export function completeSessionArchiveCleanupStep( - step: SessionArchiveCleanupStep, -): SessionArchiveCleanupStepOutcome { - return { status: "completed", step }; -} - -export function completeSessionDeleteCleanupStep( - step: SessionDeleteCleanupStep, -): SessionDeleteCleanupStepOutcome { - return { status: "completed", step }; -} - -export function skipSessionArchiveCleanupStep( - step: SessionArchiveCleanupStep, -): SessionArchiveCleanupStepOutcome { - return { status: "skipped", step }; -} - -export function skipSessionDeleteCleanupStep( - step: SessionDeleteCleanupStep, -): SessionDeleteCleanupStepOutcome { - return { status: "skipped", step }; -} - -export function shouldSkipSessionArchiveCleanupStep(input: { - step: SessionArchiveCleanupStep; - targets: SessionArchiveCleanupTargets; -}): boolean { - switch (input.step) { - case "close_sandbox_session": { - return input.targets.sandboxId === null; - } - case "stop_live_drivers": { - return input.targets.liveDriverInstanceIds.length === 0; - } - case "archive_session_row": - case "close_viewer_sockets": - case "load_runtime_targets": - case "normalize_runtime_lifecycle": { - return false; - } - default: { - throw new Error("Unknown session archive cleanup step."); - } - } -} - -export function shouldSkipSessionDeleteCleanupStep(input: { - step: SessionDeleteCleanupStep; - targets: SessionDeleteCleanupTargets; -}): boolean { - switch (input.step) { - case "close_sandbox_session": { - return input.targets.sandboxId === null; - } - case "destroy_driver_objects": - case "stop_live_drivers": { - return input.targets.liveDriverInstanceIds.length === 0; - } - case "delete_driver_rows": { - return input.targets.associatedDriverInstanceIds.length === 0; - } - case "archive_session_row": - case "delete_session_backups": - case "delete_session_files": - case "delete_session_row": - case "destroy_session_object": - case "load_cleanup_targets": { - return false; - } - default: { - throw new Error("Unknown session delete cleanup step."); - } - } } diff --git a/apps/api/src/modules/sessions/domain/session-event-stream-fold.ts b/apps/api/src/modules/sessions/domain/session-event-stream-fold.ts index 1ef897df..6f467607 100644 --- a/apps/api/src/modules/sessions/domain/session-event-stream-fold.ts +++ b/apps/api/src/modules/sessions/domain/session-event-stream-fold.ts @@ -3,18 +3,9 @@ import type { RuntimeEventId, SessionRunId } from "@mosoo/id"; // Streamed text events (message.delta / thought.delta) are persisted one row // per fragment so every accepted source identity stays durable (#274). Reading // them back verbatim renders each fragment as its own timeline entry, so read -// paths fold a fragment stream into a single row before projecting process -// events. The merge rules mirror the pre-persistence compactor -// (runtime-event-compaction.ts): deltas append, snapshots prefer the longer -// prefix-matching text. -// -// Rows carry no message identity, so a stream whose driver-side identity -// fractured (a dropped message_start splits one reply across several -// started/delta groups, YEF-884) still folds into several fragment rows, and -// the trailing message.added snapshot lands after its stream already closed. -// To heal both shapes, closed fragment rows are kept as supersede candidates: -// a snapshot whose text prefix-matches the concatenated fragments replaces -// them with a single row instead of rendering a duplicate. +// paths fold each identified stream into one row before projecting process +// events. message.added starts an authoritative snapshot, and later deltas +// append to it. export interface StreamFoldableSessionEventRow { content_text: string; @@ -25,21 +16,33 @@ export interface StreamFoldableSessionEventRow { process_type: string; run_id: SessionRunId | null; seq: number; + stream_id: string | null; tokens: number | null; } -export interface FoldedStreamedSessionEventRows { - /** - * Raw rows of streams that have not seen their closing event yet, in seq - * order. Only populated when `flushOpenStreams` is false; callers carry them - * into the next fold so a stream spanning reads still emits exactly once. - */ - openStreamRows: R[]; - rows: R[]; -} - type StreamRowPhase = "added" | "completed" | "delta" | "started"; +const MESSAGE_STREAM_AUTHORITY_RESET_EVENT_TYPES = [ + "message.cancelled", + "message.failed", + "message.started", +] as const; + +export const MESSAGE_STREAM_AUTHORITY_BOUNDARY_EVENT_TYPES = [ + "message.added", + ...MESSAGE_STREAM_AUTHORITY_RESET_EVENT_TYPES, +] as const; + +export const MESSAGE_STREAM_EVENT_TYPES = [ + ...MESSAGE_STREAM_AUTHORITY_BOUNDARY_EVENT_TYPES, + "message.completed", + "message.delta", +] as const; + +const messageStreamAuthorityResetEventTypes: ReadonlySet = new Set( + MESSAGE_STREAM_AUTHORITY_RESET_EVENT_TYPES, +); + interface StreamRowClassification { phase: StreamRowPhase; placeholder: string; @@ -50,9 +53,12 @@ const THOUGHT_PLACEHOLDER = "Agent thinking updated."; const streamRowClassifications: Readonly> = { "message.added": { phase: "added", placeholder: MESSAGE_PLACEHOLDER }, + "message.cancelled": { phase: "completed", placeholder: MESSAGE_PLACEHOLDER }, "message.completed": { phase: "completed", placeholder: MESSAGE_PLACEHOLDER }, "message.delta": { phase: "delta", placeholder: MESSAGE_PLACEHOLDER }, + "message.failed": { phase: "completed", placeholder: MESSAGE_PLACEHOLDER }, "message.started": { phase: "started", placeholder: MESSAGE_PLACEHOLDER }, + "thought.cancelled": { phase: "completed", placeholder: THOUGHT_PLACEHOLDER }, "thought.completed": { phase: "completed", placeholder: THOUGHT_PLACEHOLDER }, "thought.delta": { phase: "delta", placeholder: THOUGHT_PLACEHOLDER }, "thought.started": { phase: "started", placeholder: THOUGHT_PLACEHOLDER }, @@ -60,163 +66,304 @@ const streamRowClassifications: Readonly const terminalRunEventTypes = new Set(["run.cancelled", "run.completed", "run.failed"]); -interface OpenStreamGroup { +export interface MessageStreamTextFragment { + kind: "append" | "reset"; + text: string; +} + +export function getMessageStreamTextFragment( + row: StreamFoldableSessionEventRow, +): MessageStreamTextFragment | null { + const classification = streamRowClassifications[row.event_type]; + + if ( + classification === undefined || + !row.event_type.startsWith("message.") || + (classification.phase !== "added" && classification.phase !== "delta") + ) { + return null; + } + + return { + kind: classification.phase === "added" ? "reset" : "append", + text: row.content_text, + }; +} + +interface StreamGroup { contentText: string; + firstRow: R; + latestRow: R; + outputIndex: number | null; placeholder: string; - rows: R[]; + representative: R | null; runId: SessionRunId | null; + terminal: boolean; } -interface ClosedFragmentSegment { - content: string; - outputIndex: number; +interface ScannedStreamState { + leftBoundarySeen: boolean; + runId: SessionRunId | null; } -function appendStreamText(current: string, next: string): string { - if (next.length === 0) { - return current; - } - - return current.length === 0 ? next : `${current}${next}`; +function createStreamGroup( + row: R, + classification: StreamRowClassification, +): StreamGroup { + return { + contentText: "", + firstRow: row, + latestRow: row, + outputIndex: null, + placeholder: classification.placeholder, + representative: null, + runId: row.run_id, + terminal: false, + }; } -function mergeSnapshotText(current: string, next: string): string { - if (next.length === 0) { - return current; +export function getSessionEventStreamKey(row: StreamFoldableSessionEventRow): string | null { + if (streamRowClassifications[row.event_type] === undefined) { + return null; } - if (current.length === 0) { - return next; + if (row.stream_id === null) { + throw new Error(`Streamed session event ${row.id} has no stream identity.`); } - if (next.length > current.length && next.startsWith(current)) { - return next; - } + return JSON.stringify([row.run_id, row.process_type, row.stream_id]); +} - if (current.length >= next.length && current.startsWith(next)) { - return current; - } +// Reverse window scans must cross a stream's left boundary before treating +// its folded content and first sequence as complete. Migration identities +// that equal their row ID are deliberately row-scoped and already complete. +export function findLeftIncompleteSessionEventStreamKeys( + rows: readonly StreamFoldableSessionEventRow[], +): Set { + const runStarts = new Set(); + const streams = new Map(); - return `${current}${next}`; -} + for (const row of rows) { + if (row.event_type === "run.started" && row.run_id !== null) { + runStarts.add(row.run_id); + } + + const key = getSessionEventStreamKey(row); -function isSnapshotOfFragments(fragments: string, snapshot: string): boolean { - if (fragments.length === 0 || snapshot.length === 0) { - return false; + if (key === null) { + continue; + } + + const stream = streams.get(key) ?? { + leftBoundarySeen: false, + runId: row.run_id, + }; + stream.leftBoundarySeen ||= + row.event_type === "message.added" || + row.event_type === "message.started" || + row.event_type === "thought.started" || + row.stream_id === row.id; + streams.set(key, stream); } - return snapshot.startsWith(fragments) || fragments.startsWith(snapshot); + return new Set( + [...streams] + .filter( + ([, stream]) => + !stream.leftBoundarySeen && (stream.runId === null || !runStarts.has(stream.runId)), + ) + .map(([key]) => key), + ); } -function createOpenStreamGroup( - row: R, - classification: StreamRowClassification, -): OpenStreamGroup { - return { - contentText: "", - placeholder: classification.placeholder, - rows: [], - runId: row.run_id, - }; +export function excludeSessionEventStreams( + rows: readonly R[], + excludedKeys: ReadonlySet, +): R[] { + return rows.filter((row) => { + const key = getSessionEventStreamKey(row); + return key === null || !excludedKeys.has(key); + }); } function mergeStreamRow( - group: OpenStreamGroup, + group: StreamGroup, row: R, classification: StreamRowClassification, ): void { - group.rows.push(row); + group.latestRow = row; + group.contentText = mergeStreamText( + group.contentText, + row.content_text, + classification, + group.placeholder, + ); + + if (classification.phase === "added") { + if (!group.terminal) { + group.representative = row; + } + } else if (classification.phase === "completed") { + if (group.outputIndex === null) { + group.representative = row; + } + group.terminal = true; + } +} - // Fragments that carried no text are persisted with the process-draft - // placeholder; they mark stream boundaries and must not leak into the text. - if (row.content_text === group.placeholder) { - return; +function mergeStreamText( + currentText: string, + rowText: string, + classification: StreamRowClassification, + placeholder: string, +): string { + const text = + classification.phase === "added" || classification.phase === "delta" + ? rowText + : rowText === placeholder + ? "" + : rowText; + + if (classification.phase === "added") { + return text; } + if (classification.phase === "delta") { + return currentText + text; + } + if (classification.phase === "completed") { + return currentText || text; + } + return currentText; +} - group.contentText = - classification.phase === "delta" - ? appendStreamText(group.contentText, row.content_text) - : mergeSnapshotText(group.contentText, row.content_text); +/** + * Resolves one already-ordered assistant message stream only when its latest + * lifecycle is sealed by message.completed. A later start, delta, or + * authoritative snapshot opens the stream again, so an old terminal receipt + * cannot make an incomplete replacement look final. + */ +interface AuthoritativeMessageStream { + sealed: boolean; + text: string; } -function createFoldedStreamRow( - group: OpenStreamGroup, -): R | null { - const firstRow = group.rows[0]; - const lastRow = group.rows[group.rows.length - 1]; +export interface MessageStreamLifecycle { + authoritative: boolean; + sealed: boolean; +} - if (firstRow === undefined || lastRow === undefined || group.contentText.length === 0) { - return null; +export interface MessageStreamReducerState + extends AuthoritativeMessageStream, MessageStreamLifecycle {} + +export function createMessageStreamLifecycle(): MessageStreamLifecycle { + return { authoritative: false, sealed: false }; +} + +export function reduceMessageStreamLifecycle( + state: MessageStreamLifecycle, + eventType: string, +): MessageStreamLifecycle { + if (eventType === "message.added") { + return { authoritative: true, sealed: false }; + } + if (messageStreamAuthorityResetEventTypes.has(eventType)) { + return { authoritative: false, sealed: false }; } + if (eventType === "message.completed") { + return { authoritative: state.authoritative, sealed: state.authoritative }; + } + if (eventType === "message.delta") { + return { authoritative: state.authoritative, sealed: false }; + } + return state; +} - return { - ...lastRow, - content_text: group.contentText, - occurred_at: firstRow.occurred_at, - seq: firstRow.seq, - }; +export function createMessageStreamReducerState(): MessageStreamReducerState { + return { ...createMessageStreamLifecycle(), text: "" }; } -export function foldStreamedSessionEventRows( - rows: readonly R[], - options: { flushOpenStreams: boolean }, -): FoldedStreamedSessionEventRows { - const output: (R | null)[] = []; - const openGroups = new Map>(); - const fragmentSegments = new Map(); +export function reduceMessageStreamRow( + state: MessageStreamReducerState, + row: StreamFoldableSessionEventRow, + options?: { maxTextLength: number }, +): void { + const classification = streamRowClassifications[row.event_type]; + + if (classification === undefined || !row.event_type.startsWith("message.")) { + return; + } - function closeGroupAsFragment(key: string, group: OpenStreamGroup): void { - const folded = createFoldedStreamRow(group); + state.text = mergeStreamText(state.text, row.content_text, classification, MESSAGE_PLACEHOLDER); + if (options !== undefined && state.text.length > options.maxTextLength) { + state.text = state.text.slice(0, options.maxTextLength); + } + const lifecycle = reduceMessageStreamLifecycle(state, row.event_type); + state.authoritative = lifecycle.authoritative; + state.sealed = lifecycle.sealed; +} - if (folded === null) { - return; - } +function resolveAuthoritativeMessageStream( + rows: readonly StreamFoldableSessionEventRow[], +): AuthoritativeMessageStream | null { + const state = createMessageStreamReducerState(); - output.push(folded); - const segments = fragmentSegments.get(key) ?? []; - segments.push({ content: folded.content_text, outputIndex: output.length - 1 }); - fragmentSegments.set(key, segments); + for (const row of rows) { + reduceMessageStreamRow(state, row); } - // A snapshot row is the authoritative text of the message it closes. When - // its text extends (or repeats) the fragment rows already emitted for the - // same stream key, those fragments were partial views of this snapshot: - // collapse them into one row at the first fragment's timeline position. - function closeGroupWithSnapshot(key: string, folded: R | null): void { - if (folded === null) { - return; - } + return state.authoritative ? { sealed: state.sealed, text: state.text } : null; +} - const segments = fragmentSegments.get(key) ?? []; - fragmentSegments.delete(key); - const fragments = segments.map((segment) => segment.content).join(""); +export function resolveSealedMessageStream( + rows: readonly StreamFoldableSessionEventRow[], +): { text: string } | null { + const stream = resolveAuthoritativeMessageStream(rows); + return stream?.sealed === true ? { text: stream.text } : null; +} - if (!isSnapshotOfFragments(fragments, folded.content_text)) { - output.push(folded); - return; - } +function createFoldedStreamRow( + group: StreamGroup, + keepEmpty: boolean, +): R | null { + if (!keepEmpty && group.contentText.length === 0) { + return null; + } - const firstSegment = segments[0]; - const anchorRow = firstSegment === undefined ? null : output[firstSegment.outputIndex]; + const lastRow = group.representative ?? group.latestRow; - if (firstSegment === undefined || anchorRow === null || anchorRow === undefined) { - output.push(folded); - return; - } + return { + ...lastRow, + content_text: group.contentText, + occurred_at: group.firstRow.occurred_at, + seq: group.firstRow.seq, + }; +} - for (const segment of segments) { - output[segment.outputIndex] = null; - } +function emitStreamGroup( + output: R[], + group: StreamGroup, + keepEmpty: boolean, +): void { + const folded = createFoldedStreamRow(group, keepEmpty); - output[firstSegment.outputIndex] = { - ...folded, - content_text: - folded.content_text.length >= fragments.length ? folded.content_text : fragments, - occurred_at: anchorRow.occurred_at, - seq: anchorRow.seq, - }; + if (folded === null) { + return; } + if (group.outputIndex === null) { + group.outputIndex = output.length; + output.push(folded); + } else { + output[group.outputIndex] = folded; + } +} + +export function foldStreamedSessionEventRows( + rows: readonly R[], +): R[] { + const output: R[] = []; + const groups = new Map>(); + for (const row of rows) { const classification = streamRowClassifications[row.event_type]; @@ -225,10 +372,9 @@ export function foldStreamedSessionEventRows row !== null) }; } - return { - openStreamRows: [...openGroups.values()] - .flatMap((group) => group.rows) - .toSorted((a, b) => a.seq - b.seq), - rows: output.filter((row): row is R => row !== null), - }; + return output.toSorted((a, b) => a.seq - b.seq); } diff --git a/apps/api/src/modules/sessions/domain/session-event-tool-content.ts b/apps/api/src/modules/sessions/domain/session-event-tool-content.ts new file mode 100644 index 00000000..f626ec2e --- /dev/null +++ b/apps/api/src/modules/sessions/domain/session-event-tool-content.ts @@ -0,0 +1,35 @@ +export type StoredToolStatus = "cancelled" | "completed" | "failed" | "running"; + +function normalizeToolResult(value: string): string { + const normalized = value.replaceAll(/\s+/g, " ").trim(); + return normalized.length > 0 ? normalized : value; +} + +export function formatStoredSessionEventContent(input: { + contentText: string; + eventType: string; + toolName?: string | null; + toolStatus?: StoredToolStatus | null; +}): string { + if (input.eventType === "session.commands.updated") { + return "Session commands updated."; + } + if (input.eventType === "usage.updated") { + return "Usage updated."; + } + if (input.eventType !== "tool.call.updated" || input.toolStatus == null) { + return input.contentText; + } + + const name = input.toolName ?? "Tool"; + + if (input.toolStatus === "running" || input.toolStatus === "cancelled") { + return name; + } + + if (input.contentText.length > 0) { + return `${name} result: ${normalizeToolResult(input.contentText)}`; + } + + return input.toolStatus === "failed" ? `${name} failed.` : `${name} completed.`; +} diff --git a/apps/api/src/modules/sessions/domain/session-message-projection-parser.ts b/apps/api/src/modules/sessions/domain/session-message-projection-parser.ts index 1281c6e5..36e82e03 100644 --- a/apps/api/src/modules/sessions/domain/session-message-projection-parser.ts +++ b/apps/api/src/modules/sessions/domain/session-message-projection-parser.ts @@ -1,4 +1,6 @@ import type { SessionMessagePlanEntry, SessionMessageSegment } from "@mosoo/contracts/session"; +import { parsePlatformId } from "@mosoo/id"; +import type { SessionRunId } from "@mosoo/id"; export interface StoredSessionMessageProjectionInput { planJson: string | null; @@ -50,6 +52,10 @@ function readNullableString(value: unknown, fieldName: string): string | null { return value; } +function readOptionalNullableString(value: unknown, fieldName: string): string | null | undefined { + return value === undefined ? undefined : readNullableString(value, fieldName); +} + function readPlanPriority(value: unknown): SessionMessagePlanEntry["priority"] { if (value === "high" || value === "medium" || value === "low") { return value; @@ -91,12 +97,18 @@ function parseMessageSegment(raw: unknown): SessionMessageSegment { const tool = readString(raw["tool"], "tool segment tool"); const toolCallId = readString(raw["toolCallId"], "tool segment toolCallId"); + const rawRunId = readOptionalNullableString(raw["runId"], "tool segment runId"); + const runId = + rawRunId === undefined || rawRunId === null + ? rawRunId + : parsePlatformId(rawRunId, "tool segment runId"); if (kind === "tool_use") { return { argsText: readString(raw["argsText"], "tool_use argsText"), kind: "tool_use", path: readNullableString(raw["path"], "tool_use path"), + ...(runId === undefined ? {} : { runId }), tool, toolCallId, }; @@ -106,6 +118,7 @@ function parseMessageSegment(raw: unknown): SessionMessageSegment { return { kind: "tool_result", output: readString(raw["output"], "tool_result output"), + ...(runId === undefined ? {} : { runId }), tool, toolCallId, }; diff --git a/apps/api/src/modules/sessions/domain/session-runtime-event-authority.ts b/apps/api/src/modules/sessions/domain/session-runtime-event-authority.ts new file mode 100644 index 00000000..25d25f79 --- /dev/null +++ b/apps/api/src/modules/sessions/domain/session-runtime-event-authority.ts @@ -0,0 +1,33 @@ +import { + createRuntimeEventSemanticHash, + parseRuntimeEventEnvelope, + stringifyRuntimeEventSemanticValue, +} from "@mosoo/runtime-events"; +import type { RuntimeEventEnvelope } from "@mosoo/runtime-events"; + +export async function readSessionRuntimeEventSemanticAuthority(input: { + readonly eventJson: string | null; + readonly invalidMessage: string; + readonly missingMessage: string; + readonly semanticHash: string | null; +}): Promise { + if (input.eventJson === null || input.semanticHash === null) { + throw new Error(input.missingMessage); + } + + let event: RuntimeEventEnvelope; + try { + event = parseRuntimeEventEnvelope(JSON.parse(input.eventJson)); + } catch { + throw new Error(input.invalidMessage); + } + + if ( + stringifyRuntimeEventSemanticValue(event) !== input.eventJson || + (await createRuntimeEventSemanticHash(event)) !== input.semanticHash + ) { + throw new Error(input.invalidMessage); + } + + return event; +} diff --git a/apps/api/src/modules/sessions/domain/session-runtime-event-projection.ts b/apps/api/src/modules/sessions/domain/session-runtime-event-projection.ts index 4631806d..bd0afb4e 100644 --- a/apps/api/src/modules/sessions/domain/session-runtime-event-projection.ts +++ b/apps/api/src/modules/sessions/domain/session-runtime-event-projection.ts @@ -1,3 +1,4 @@ +import { createMcpExecuteFailedEventIdentity } from "@mosoo/agent-driver/events"; import type { SessionProcessEventStatus, SessionProcessEventType, @@ -5,16 +6,20 @@ import type { SessionRuntimeEventSource, SessionRuntimeEventVisibility, } from "@mosoo/contracts/session"; -import { parseJsonObject } from "@mosoo/contracts/validation"; -import type { JsonValue } from "@mosoo/contracts/validation"; -import type { SessionRunId } from "@mosoo/id"; +import { parsePlatformId } from "@mosoo/id"; +import type { DriverCommandId, SessionRunId } from "@mosoo/id"; import { + createRuntimeToolResultMessageId, createProcessDraftFromRuntimeEvent, getRuntimeEventSessionFamily, getRuntimeEventParticipantVisibility, getRuntimeEventSource, + readRuntimeEventMessageKey, + readRuntimeEventPayload, readRuntimeEventPermissionRequest, + readRuntimeEventString, readRuntimeEventToolCallUpdate, + readRuntimeEventToolOutputSnapshot, } from "@mosoo/runtime-events"; import type { RuntimeEventEnvelope } from "@mosoo/runtime-events"; @@ -22,13 +27,21 @@ export interface SessionRuntimeEventProjection { contentText: string; eventType: string; family: SessionRuntimeEventFamily; + mcpCommandId: DriverCommandId | null; processStatus: SessionProcessEventStatus; processType: SessionProcessEventType; runId: SessionRunId | null; source: SessionRuntimeEventSource; + streamId: string | null; toolCallId: string | null; + toolInputDeltaJson: string | null; toolInputJson: string | null; toolName: string | null; + toolOutputDeltaText: string | null; + toolOutputText: string | null; + toolParentMessageId: string | null; + toolResultMessageId: string | null; + toolStatus: "cancelled" | "completed" | "failed" | "running" | null; traceId: string | null; tokens: number | null; visibility: SessionRuntimeEventVisibility; @@ -40,67 +53,97 @@ function isKnownRuntimeEventSource(value: unknown): value is SessionRuntimeEvent return typeof value === "string" && knownRuntimeEventSources.has(value); } -function normalizeContentText(value: string): string { - const normalized = value.replaceAll(/\s+/g, " ").trim(); - return normalized.length > 0 ? normalized : value; -} - -function sortJsonValue(value: JsonValue): JsonValue { - if (Array.isArray(value)) { - return value.map(sortJsonValue); - } - - if (value !== null && typeof value === "object") { - return Object.fromEntries( - Object.entries(value) - .toSorted(([left], [right]) => (left < right ? -1 : left > right ? 1 : 0)) - .map(([key, entry]) => [key, sortJsonValue(entry)]), - ); - } - - return value; -} - -function toCanonicalToolInputJson(rawInput: string | null): string | null { - if (rawInput === null || rawInput.length === 0) { - return null; - } - - try { - const input = parseJsonObject(JSON.parse(rawInput), "Runtime tool input"); - - return Object.keys(input).length === 0 ? null : JSON.stringify(sortJsonValue(input)); - } catch { - return null; - } -} - -function readProjectedToolCall(event: RuntimeEventEnvelope): { +function readProjectedToolCall( + event: RuntimeEventEnvelope, + provenMcpCommandId: DriverCommandId | null, +): { + mcpCommandId: DriverCommandId | null; toolCallId: string | null; + toolInputDeltaJson: string | null; toolInputJson: string | null; toolName: string | null; + toolOutputDeltaText: string | null; + toolOutputText: string | null; + toolParentMessageId: string | null; + toolResultMessageId: string | null; + toolStatus: "cancelled" | "completed" | "failed" | "running" | null; } { if (event.kind === "tool.call.updated") { const toolCall = readRuntimeEventToolCallUpdate(event); + if (provenMcpCommandId !== null) { + const commandId = parsePlatformId( + event.correlationId, + "MCP command correlation ID", + ); + const sourceEventId = + toolCall.status === "failed" && + toolCall.kind === "mcp" && + toolCall.rawInput !== null && + toolCall.rawOutput !== null && + toolCall.title !== null + ? createMcpExecuteFailedEventIdentity({ + commandId, + rawInput: toolCall.rawInput, + rawOutput: toolCall.rawOutput, + title: toolCall.title, + toolCallId: toolCall.toolCallId, + }).sourceEventId + : `mcp.execute.${toolCall.status}:${commandId}`; + if ( + commandId !== provenMcpCommandId || + toolCall.kind !== "mcp" || + toolCall.status === "running" || + (toolCall.status === "failed" && + (toolCall.rawInput === null || toolCall.rawOutput === null || toolCall.title === null)) || + event.sourceEventId !== sourceEventId + ) { + throw new Error("Proven MCP command does not match its terminal tool event."); + } + } return { + mcpCommandId: provenMcpCommandId, toolCallId: toolCall.toolCallId, - // Running input is streamed and may be valid JSON before it is complete. - toolInputJson: - toolCall.status === "running" ? null : toCanonicalToolInputJson(toolCall.rawInput), - // Terminal titles are provider display labels and may differ from the stable start name. - toolName: toolCall.status === "running" ? toolCall.title || toolCall.kind : null, + toolInputDeltaJson: toolCall.rawInputDelta, + toolInputJson: toolCall.rawInput, + toolName: toolCall.title ?? toolCall.kind, + toolOutputDeltaText: toolCall.rawOutputDelta, + toolOutputText: readRuntimeEventToolOutputSnapshot(toolCall), + toolParentMessageId: toolCall.parentMessageId ?? toolCall.messageId, + toolResultMessageId: createRuntimeToolResultMessageId({ + runId: event.runId ?? null, + toolCallId: toolCall.toolCallId, + }), + toolStatus: toolCall.status, }; } const permission = readRuntimeEventPermissionRequest(event); return permission === null - ? { toolCallId: null, toolInputJson: null, toolName: null } + ? { + toolCallId: null, + mcpCommandId: null, + toolInputDeltaJson: null, + toolInputJson: null, + toolName: null, + toolOutputDeltaText: null, + toolOutputText: null, + toolParentMessageId: null, + toolResultMessageId: null, + toolStatus: null, + } : { toolCallId: permission.toolCallId, + mcpCommandId: null, + toolInputDeltaJson: null, toolInputJson: null, toolName: null, + toolOutputDeltaText: null, + toolOutputText: null, + toolParentMessageId: null, + toolResultMessageId: null, + toolStatus: null, }; } @@ -109,23 +152,33 @@ function readProjectedContentText( draft: ReturnType, ): string { if (event.kind !== "tool.call.updated") { - return draft.content; - } - - const toolCall = readRuntimeEventToolCallUpdate(event); + const payload = readRuntimeEventPayload(event); + if (event.kind === "plan.updated") { + return JSON.stringify(Array.isArray(payload["entries"]) ? payload["entries"] : []); + } + if (event.kind === "session.commands.updated") { + return JSON.stringify(Array.isArray(payload["commands"]) ? payload["commands"] : []); + } + if (event.kind === "session.config.updated") { + return JSON.stringify({ + configOptions: Array.isArray(payload["options"]) ? payload["options"] : [], + }); + } + if (event.kind === "session.mode.updated") { + return JSON.stringify({ + currentModeId: readRuntimeEventString(payload, "currentMode"), + visibleModes: Array.isArray(payload["availableModes"]) ? payload["availableModes"] : [], + }); + } + if (event.kind === "usage.updated") { + return JSON.stringify(payload); + } - if (toolCall.status !== "completed" && toolCall.status !== "failed") { return draft.content; } - const name = toolCall.title ?? toolCall.kind ?? "Tool"; - const result = toolCall.rawOutput ?? toolCall.content; - - if (result === null) { - return toolCall.status === "failed" ? `${name} failed.` : `${name} completed.`; - } - - return `${name} result: ${normalizeContentText(result)}`; + const toolCall = readRuntimeEventToolCallUpdate(event); + return toolCall.rawOutput ?? toolCall.rawOutputDelta ?? toolCall.content ?? ""; } function readProjectedProcessStatus( @@ -150,21 +203,29 @@ function readProjectedVisibility(event: RuntimeEventEnvelope): SessionRuntimeEve : getRuntimeEventParticipantVisibility(event); } +function readProjectedStreamId(event: RuntimeEventEnvelope): string | null { + return event.kind === "run.completed" + ? readRuntimeEventString(readRuntimeEventPayload(event), "finalMessageId") + : readRuntimeEventMessageKey(event); +} + export function createSessionRuntimeEventProjection( event: RuntimeEventEnvelope, + options?: { provenMcpCommandId: DriverCommandId | null }, ): SessionRuntimeEventProjection { const draft = createProcessDraftFromRuntimeEvent(event); const source = getRuntimeEventSource(event); - const toolCall = readProjectedToolCall(event); + const toolCall = readProjectedToolCall(event, options?.provenMcpCommandId ?? null); return { contentText: readProjectedContentText(event, draft), eventType: event.kind, - family: event.kind === "agent.tasks.replaced" ? "state" : getRuntimeEventSessionFamily(event), + family: getRuntimeEventSessionFamily(event), processStatus: readProjectedProcessStatus(event, draft), processType: draft.type, runId: event.runId ?? null, source: isKnownRuntimeEventSource(source) ? source : "system", + streamId: readProjectedStreamId(event), ...toolCall, traceId: event.traceId ?? null, tokens: draft.tokens ?? null, diff --git a/apps/api/src/modules/sessions/domain/session-terminal-event-authority.ts b/apps/api/src/modules/sessions/domain/session-terminal-event-authority.ts new file mode 100644 index 00000000..c0b18b0f --- /dev/null +++ b/apps/api/src/modules/sessions/domain/session-terminal-event-authority.ts @@ -0,0 +1,55 @@ +import { + readRuntimeEventPayload, + readRuntimeEventString, + readRuntimeRunPayload, +} from "@mosoo/runtime-events"; +import type { RuntimeEventEnvelope } from "@mosoo/runtime-events"; + +import { readSessionRuntimeEventSemanticAuthority } from "./session-runtime-event-authority"; +import { createSessionRuntimeEventProjection } from "./session-runtime-event-projection"; + +export interface TerminalEventSemanticAuthority { + readonly event: RuntimeEventEnvelope; + readonly finalMessageId: string | null; + readonly lifecycle: "IDLE" | "TERMINATED"; +} + +export async function readTerminalEventSemanticAuthority(input: { + eventJson: string | null; + eventType: string; + runId: string; + semanticHash: string | null; + sessionId: string; + sourceEventId: string; + streamId: string | null; +}): Promise { + const event: RuntimeEventEnvelope = await readSessionRuntimeEventSemanticAuthority({ + eventJson: input.eventJson, + invalidMessage: `Session run ${input.runId} has an invalid durable terminal semantic authority.`, + missingMessage: `Session run ${input.runId} has no durable terminal semantic authority.`, + semanticHash: input.semanticHash, + }); + + const projection = createSessionRuntimeEventProjection(event); + const runtimeRun = readRuntimeRunPayload(event); + const finalMessageId = + event.kind === "run.completed" + ? readRuntimeEventString(readRuntimeEventPayload(event), "finalMessageId") + : null; + if ( + event.kind !== input.eventType || + event.runId !== input.runId || + event.sessionId !== input.sessionId || + event.sourceEventId !== input.sourceEventId || + projection.eventType !== input.eventType || + projection.runId !== input.runId || + projection.streamId !== input.streamId || + runtimeRun.run?.id !== input.runId || + (runtimeRun.lifecycle !== "IDLE" && runtimeRun.lifecycle !== "TERMINATED") || + finalMessageId !== input.streamId + ) { + throw new Error(`Session run ${input.runId} terminal semantic authority is invalid.`); + } + + return { event, finalMessageId, lifecycle: runtimeRun.lifecycle }; +} diff --git a/apps/api/src/modules/sessions/infrastructure/session-agent-task-snapshot.repository.ts b/apps/api/src/modules/sessions/infrastructure/session-agent-task-snapshot.repository.ts index d9c7b874..0702f929 100644 --- a/apps/api/src/modules/sessions/infrastructure/session-agent-task-snapshot.repository.ts +++ b/apps/api/src/modules/sessions/infrastructure/session-agent-task-snapshot.repository.ts @@ -1,7 +1,8 @@ -import { AgentTaskSnapshot, AgentTasksReplacedPayload } from "@mosoo/contracts/session"; +import { AgentTaskSnapshot } from "@mosoo/contracts/session"; +import type { SessionRunStatus } from "@mosoo/contracts/session-run"; import { parseSchemaValue } from "@mosoo/contracts/validation"; import { sessionAgentTaskSnapshotsTable, sessionRunsTable, sessionsTable } from "@mosoo/db"; -import type { RuntimeEventId, SessionId } from "@mosoo/id"; +import type { RuntimeEventId, SessionId, SessionRunId } from "@mosoo/id"; import { and, eq, inArray, isNull } from "drizzle-orm"; import { createErrorLogContext, logWarn } from "../../../platform/cloudflare/logger"; @@ -66,12 +67,10 @@ export function parseStoredAgentTaskSnapshot(input: { tasksJson: string; }): AgentTaskSnapshot | null { try { - const payload = parseSchemaValue(AgentTasksReplacedPayload, JSON.parse(input.tasksJson)); - return parseSchemaValue(AgentTaskSnapshot, { + ...JSON.parse(input.tasksJson), driverInstanceId: input.driverInstanceId, runId: input.runId, - tasks: payload.tasks, }); } catch (error) { logWarn("session.agent_task_snapshot.invalid", { @@ -83,36 +82,46 @@ export function parseStoredAgentTaskSnapshot(input: { } } -export async function loadSessionAgentTaskSnapshot( +export interface SessionAgentTaskState { + driverInstanceId: string | null; + runId: SessionRunId; + runStatus: SessionRunStatus; + snapshot: AgentTaskSnapshot | null; +} + +export async function loadSessionAgentTaskState( database: D1Database, sessionId: SessionId, -): Promise { +): Promise { const row = (await getAppDatabase(database) .select({ - driverInstanceId: sessionAgentTaskSnapshotsTable.driverInstanceId, - runId: sessionAgentTaskSnapshotsTable.runId, + runDriverInstanceId: sessionRunsTable.driverInstanceId, + runId: sessionRunsTable.id, + runStatus: sessionRunsTable.status, + taskDriverInstanceId: sessionAgentTaskSnapshotsTable.driverInstanceId, + taskRunId: sessionAgentTaskSnapshotsTable.runId, tasksJson: sessionAgentTaskSnapshotsTable.tasksJson, }) - .from(sessionAgentTaskSnapshotsTable) + .from(sessionsTable) .innerJoin( - sessionsTable, + sessionRunsTable, and( - eq(sessionsTable.id, sessionAgentTaskSnapshotsTable.sessionId), - eq(sessionsTable.lastRunId, sessionAgentTaskSnapshotsTable.runId), + eq(sessionRunsTable.id, sessionsTable.lastRunId), + eq(sessionRunsTable.sessionId, sessionsTable.id), ), ) - .innerJoin( - sessionRunsTable, + .leftJoin( + sessionAgentTaskSnapshotsTable, and( - eq(sessionRunsTable.id, sessionAgentTaskSnapshotsTable.runId), - eq(sessionRunsTable.sessionId, sessionAgentTaskSnapshotsTable.sessionId), - eq(sessionRunsTable.driverInstanceId, sessionAgentTaskSnapshotsTable.driverInstanceId), + eq(sessionAgentTaskSnapshotsTable.sessionId, sessionsTable.id), + eq(sessionAgentTaskSnapshotsTable.runId, sessionRunsTable.id), + eq(sessionAgentTaskSnapshotsTable.driverInstanceId, sessionRunsTable.driverInstanceId), ), ) .where( and( - eq(sessionAgentTaskSnapshotsTable.sessionId, sessionId), + eq(sessionsTable.id, sessionId), isNull(sessionsTable.archivedAt), eq(sessionsTable.status, "RUNNING"), inArray(sessionRunsTable.status, ACTIVE_SESSION_RUN_STATUSES), @@ -125,10 +134,27 @@ export async function loadSessionAgentTaskSnapshot( return null; } - return parseStoredAgentTaskSnapshot({ - driverInstanceId: row.driverInstanceId, + const snapshot = + row.taskDriverInstanceId === null || row.taskRunId === null || row.tasksJson === null + ? null + : parseStoredAgentTaskSnapshot({ + driverInstanceId: row.taskDriverInstanceId, + runId: row.taskRunId, + sessionId, + tasksJson: row.tasksJson, + }); + + return { + driverInstanceId: row.runDriverInstanceId, runId: row.runId, - sessionId, - tasksJson: row.tasksJson, - }); + runStatus: row.runStatus, + snapshot, + }; +} + +export async function loadSessionAgentTaskSnapshot( + database: D1Database, + sessionId: SessionId, +): Promise { + return (await loadSessionAgentTaskState(database, sessionId))?.snapshot ?? null; } diff --git a/apps/api/src/modules/sessions/infrastructure/session-message-event-stream.repository.ts b/apps/api/src/modules/sessions/infrastructure/session-message-event-stream.repository.ts new file mode 100644 index 00000000..afbcec01 --- /dev/null +++ b/apps/api/src/modules/sessions/infrastructure/session-message-event-stream.repository.ts @@ -0,0 +1,301 @@ +import type { SessionProcessEvent } from "@mosoo/contracts/session"; +import { sessionEventsTable } from "@mosoo/db"; +import type { SessionId, SessionRunId } from "@mosoo/id"; +import { + and, + asc, + desc, + eq, + gt, + gte, + inArray, + isNotNull, + isNull, + lte, + ne, + notInArray, + or, +} from "drizzle-orm"; + +import { getAppDatabase } from "../../../platform/db/drizzle"; +import { + createMessageStreamLifecycle, + createMessageStreamReducerState, + MESSAGE_STREAM_EVENT_TYPES, + reduceMessageStreamLifecycle, + reduceMessageStreamRow, +} from "../domain/session-event-stream-fold"; +import type { StreamFoldableSessionEventRow } from "../domain/session-event-stream-fold"; + +const SESSION_MESSAGE_EVENT_PAGE_SIZE = 1_000; +const SESSION_MESSAGE_EVENT_TYPE_SET: ReadonlySet = new Set(MESSAGE_STREAM_EVENT_TYPES); + +export interface SessionMessageEventStreamCursor { + endSeq: number; + finish: boolean; + outputOffset: number; + startSeq: number; +} + +export interface SessionMessageEventStreamIdentity { + processType: SessionProcessEvent["type"]; + runId: SessionRunId | null; + sessionId: SessionId; + streamId: string; +} + +export interface SessionMessageEventRow extends StreamFoldableSessionEventRow { + process_status: SessionProcessEvent["status"]; + process_type: SessionProcessEvent["type"]; +} + +async function assertExactMessageStreamIdentity( + database: D1Database, + input: SessionMessageEventStreamIdentity & { endSeq?: number }, +): Promise { + const conflictingRun = + input.runId === null + ? isNotNull(sessionEventsTable.runId) + : or(isNull(sessionEventsTable.runId), ne(sessionEventsTable.runId, input.runId)); + const collision = await getAppDatabase(database) + .select({ id: sessionEventsTable.id }) + .from(sessionEventsTable) + .where( + and( + eq(sessionEventsTable.sessionId, input.sessionId), + eq(sessionEventsTable.streamId, input.streamId), + inArray(sessionEventsTable.eventType, [...MESSAGE_STREAM_EVENT_TYPES]), + input.endSeq === undefined ? undefined : lte(sessionEventsTable.seq, input.endSeq), + or(ne(sessionEventsTable.processType, input.processType), conflictingRun), + ), + ) + .limit(1) + .get(); + + if (collision !== undefined) { + throw new Error(`Session message stream ${input.streamId} has conflicting identity rows.`); + } +} + +export async function* iteratePublicSessionMessageEventRows( + database: D1Database, + input: SessionMessageEventStreamIdentity & { cursor: SessionMessageEventStreamCursor }, +): AsyncGenerator { + let afterSeq: number | null = null; + await assertExactMessageStreamIdentity(database, { ...input, endSeq: input.cursor.endSeq }); + const streamScope = [ + eq(sessionEventsTable.sessionId, input.sessionId), + eq(sessionEventsTable.streamId, input.streamId), + eq(sessionEventsTable.processType, input.processType), + lte(sessionEventsTable.seq, input.cursor.endSeq), + input.runId === null + ? isNull(sessionEventsTable.runId) + : eq(sessionEventsTable.runId, input.runId), + ]; + const invalidRow = await getAppDatabase(database) + .select({ + eventType: sessionEventsTable.eventType, + visibility: sessionEventsTable.visibility, + }) + .from(sessionEventsTable) + .where( + and( + ...streamScope, + or( + ne(sessionEventsTable.visibility, "all_consumers"), + notInArray(sessionEventsTable.eventType, [...MESSAGE_STREAM_EVENT_TYPES]), + ), + ), + ) + .limit(1) + .get(); + if (invalidRow?.visibility !== undefined) { + throw new Error( + invalidRow.visibility === "all_consumers" + ? `Session message stream ${input.streamId} has unsupported event ${invalidRow.eventType}.` + : `Session message stream ${input.streamId} has mixed visibility.`, + ); + } + + for (;;) { + const filters = [ + ...streamScope, + eq(sessionEventsTable.visibility, "all_consumers"), + gte(sessionEventsTable.seq, input.cursor.startSeq), + ]; + + if (afterSeq !== null) { + filters.push(gt(sessionEventsTable.seq, afterSeq)); + } + + const page = await getAppDatabase(database) + .select({ + content_text: sessionEventsTable.contentText, + ended_at: sessionEventsTable.endedAt, + event_type: sessionEventsTable.eventType, + id: sessionEventsTable.id, + occurred_at: sessionEventsTable.occurredAt, + process_status: sessionEventsTable.processStatus, + process_type: sessionEventsTable.processType, + run_id: sessionEventsTable.runId, + seq: sessionEventsTable.seq, + stream_id: sessionEventsTable.streamId, + tokens: sessionEventsTable.tokens, + }) + .from(sessionEventsTable) + .where(and(...filters)) + .orderBy(asc(sessionEventsTable.seq)) + .limit(SESSION_MESSAGE_EVENT_PAGE_SIZE) + .all(); + + for (const row of page) { + if (!SESSION_MESSAGE_EVENT_TYPE_SET.has(row.event_type)) { + throw new Error( + `Session message stream ${input.streamId} has unsupported event ${row.event_type}.`, + ); + } + yield row; + } + + if (page.length < SESSION_MESSAGE_EVENT_PAGE_SIZE) { + return; + } + + afterSeq = page[page.length - 1]?.seq ?? afterSeq; + } +} + +export async function readSealedPublicSessionMessage( + database: D1Database, + input: SessionMessageEventStreamIdentity & { endSeq?: number }, +): Promise<{ text: string } | null> { + const scope = [ + eq(sessionEventsTable.sessionId, input.sessionId), + eq(sessionEventsTable.streamId, input.streamId), + eq(sessionEventsTable.processType, input.processType), + input.runId === null + ? isNull(sessionEventsTable.runId) + : eq(sessionEventsTable.runId, input.runId), + ]; + const endSeq = + input.endSeq ?? + ( + await getAppDatabase(database) + .select({ seq: sessionEventsTable.seq }) + .from(sessionEventsTable) + .where(and(...scope)) + .orderBy(desc(sessionEventsTable.seq)) + .limit(1) + .get() + )?.seq; + if (endSeq === undefined) { + return null; + } + const state = createMessageStreamReducerState(); + + for await (const row of iteratePublicSessionMessageEventRows(database, { + ...input, + cursor: { + endSeq, + finish: true, + outputOffset: 0, + startSeq: 0, + }, + })) { + reduceMessageStreamRow(state, row); + } + + return state.authoritative && state.sealed ? { text: state.text } : null; +} + +export interface PublicSessionMessageStreamSealState { + authoritative: boolean; + sealed: boolean; +} + +export async function readPublicSessionMessageStreamSealState( + database: D1Database, + input: SessionMessageEventStreamIdentity, +): Promise { + await assertExactMessageStreamIdentity(database, input); + const scope = [ + eq(sessionEventsTable.sessionId, input.sessionId), + eq(sessionEventsTable.streamId, input.streamId), + eq(sessionEventsTable.processType, input.processType), + input.runId === null + ? isNull(sessionEventsTable.runId) + : eq(sessionEventsTable.runId, input.runId), + ]; + const endSeq = ( + await getAppDatabase(database) + .select({ seq: sessionEventsTable.seq }) + .from(sessionEventsTable) + .where(and(...scope)) + .orderBy(desc(sessionEventsTable.seq)) + .limit(1) + .get() + )?.seq; + if (endSeq === undefined) { + return { authoritative: false, sealed: false }; + } + + const invalidRow = await getAppDatabase(database) + .select({ + eventType: sessionEventsTable.eventType, + visibility: sessionEventsTable.visibility, + }) + .from(sessionEventsTable) + .where( + and( + ...scope, + lte(sessionEventsTable.seq, endSeq), + or( + ne(sessionEventsTable.visibility, "all_consumers"), + notInArray(sessionEventsTable.eventType, [...MESSAGE_STREAM_EVENT_TYPES]), + ), + ), + ) + .limit(1) + .get(); + if (invalidRow?.visibility !== undefined) { + throw new Error( + invalidRow.visibility === "all_consumers" + ? `Session message stream ${input.streamId} has unsupported event ${invalidRow.eventType}.` + : `Session message stream ${input.streamId} has mixed visibility.`, + ); + } + + let afterSeq: number | null = null; + let state = createMessageStreamLifecycle(); + for (;;) { + const page = await getAppDatabase(database) + .select({ eventType: sessionEventsTable.eventType, seq: sessionEventsTable.seq }) + .from(sessionEventsTable) + .where( + and( + ...scope, + eq(sessionEventsTable.visibility, "all_consumers"), + lte(sessionEventsTable.seq, endSeq), + afterSeq === null ? undefined : gt(sessionEventsTable.seq, afterSeq), + ), + ) + .orderBy(asc(sessionEventsTable.seq)) + .limit(SESSION_MESSAGE_EVENT_PAGE_SIZE) + .all(); + + for (const row of page) { + state = reduceMessageStreamLifecycle(state, row.eventType); + } + if (page.length < SESSION_MESSAGE_EVENT_PAGE_SIZE) { + return state; + } + afterSeq = page[page.length - 1]?.seq ?? afterSeq; + } +} + +export async function isSealedPublicSessionMessageStream( + database: D1Database, + input: SessionMessageEventStreamIdentity, +): Promise { + return (await readPublicSessionMessageStreamSealState(database, input)).sealed; +} diff --git a/apps/api/src/modules/sessions/infrastructure/session-message-reference.repository.ts b/apps/api/src/modules/sessions/infrastructure/session-message-reference.repository.ts new file mode 100644 index 00000000..19e0a87c --- /dev/null +++ b/apps/api/src/modules/sessions/infrastructure/session-message-reference.repository.ts @@ -0,0 +1,1310 @@ +import { + applyAgUiEventToSessionLiveState, + applyToolCallUpdateToSessionLiveState, + createInitialSessionLiveState, + createServerCustomEvent, + EventType, + MOSOO_CUSTOM_EVENT, + parseAgUiSessionEvent, + parseNullableSessionUsageSummary, +} from "@mosoo/ag-ui-session"; +import type { + AgUiSessionEvent, + SessionLiveState, + SessionViewPlanEntry, +} from "@mosoo/ag-ui-session"; +import { sessionEventsTable } from "@mosoo/db"; +import type { SessionId, SessionMessageId, SessionRunId } from "@mosoo/id"; +import { createSessionRunTerminalSourceId } from "@mosoo/runtime-events"; +import { and, asc, desc, eq, gt, gte, inArray, lte, ne, notInArray, or, sql } from "drizzle-orm"; +import type { SQL } from "drizzle-orm"; + +import { getAppDatabase } from "../../../platform/db/drizzle"; +import { + ProviderPrivateMarkupStreamFilter, + sanitizeProviderPrivateMarkup, +} from "../domain/provider-private-markup"; +import { + createMessageStreamReducerState, + MESSAGE_STREAM_AUTHORITY_BOUNDARY_EVENT_TYPES, + MESSAGE_STREAM_EVENT_TYPES, + reduceMessageStreamRow, +} from "../domain/session-event-stream-fold"; +import type { + MessageStreamReducerState, + StreamFoldableSessionEventRow, +} from "../domain/session-event-stream-fold"; +import { readTerminalEventSemanticAuthority } from "../domain/session-terminal-event-authority"; + +const REFERENCE_EVENT_PAGE_SIZE = 1_000; +const BOUNDED_REFERENCE_EVENT_PAGE_SIZE = 1_000; +const BOUNDED_REFERENCE_EVENT_PAGE_CHARS = 512 * 1_024; +const RUN_TERMINAL_EVENT_TYPES = ["run.cancelled", "run.completed", "run.failed"]; +const MESSAGE_EVENT_TYPE_SET: ReadonlySet = new Set(MESSAGE_STREAM_EVENT_TYPES); +const THOUGHT_EVENT_TYPES = new Set([ + "thought.cancelled", + "thought.completed", + "thought.delta", + "thought.started", +]); +const THOUGHT_PROCESS_TYPE = "agent.thinking.delta"; +const USER_MESSAGE_PROCESS_TYPE = "user.message"; +const ACTIVE_SESSION_STATE_EVENT_TYPES = [ + "session.commands.updated", + "session.config.updated", + "session.mode.updated", + "usage.updated", +] as const; + +export interface StoredSessionMessageReferenceRow { + content_text: string; + id: SessionMessageId; + plan_json: string | null; + projection_format: "event_stream_v3" | "materialized"; + role: "assistant" | "user"; + segments_json: string | null; + session_run_id: SessionRunId | null; +} + +interface ReferenceEventRow extends StreamFoldableSessionEventRow { + tool_call_id: string | null; + tool_input_delta_json: string | null; + tool_input_json: string | null; + tool_name: string | null; + tool_output_delta_text: string | null; + tool_output_text: string | null; + tool_parent_message_id: string | null; + tool_result_message_id: string | null; + tool_status: "cancelled" | "completed" | "failed" | "running" | null; + visibility: string; +} + +interface ReferenceState { + isCarrier: boolean; + isFinal: boolean; + live: SessionLiveState; + message: MessageStreamReducerState; + planJson: string | null; +} + +interface ContentReferenceState { + filter: ProviderPrivateMarkupStreamFilter; + hasStreamText: boolean; + message: MessageStreamReducerState; +} + +interface SealedReferenceCursor { + endSeq: number; + startSeq: number; +} + +interface LightweightReference { + messageId: SessionMessageId; + runId: SessionRunId; +} + +interface CanonicalReference extends LightweightReference { + isCarrier: boolean; + isFinal: boolean; +} + +interface TerminalReference { + messageId: string | null; + seq: number; +} + +interface SessionStateEventRow { + content_text: string; + event_type: string; + seq: number; + visibility: string; +} + +interface ToolRoute { + authority: "carrier" | "final" | null; + firstOutputSeq: number | null; + firstParentSeq: number | null; + parentMessageId: string | null; + referenceKeys: Set; + resultMessageId: string | null; + runId: SessionRunId; + toolCallId: string; +} + +export interface StoredSessionMessageContentReferenceRow { + content_text: string; + id: SessionMessageId; + projection_format: "event_stream_v3" | "materialized"; + role: "assistant" | "user"; + session_run_id: SessionRunId | null; +} + +function referenceKey(runId: SessionRunId, messageId: string): string { + return JSON.stringify([runId, messageId]); +} + +function potentialReference( + row: StoredSessionMessageContentReferenceRow, +): LightweightReference | null { + return row.projection_format === "event_stream_v3" && + row.role === "assistant" && + row.session_run_id !== null + ? { messageId: row.id, runId: row.session_run_id } + : null; +} + +function requireTerminalReferences( + candidates: readonly LightweightReference[], + terminalReferences: ReadonlyMap, +): CanonicalReference[] { + const references: CanonicalReference[] = []; + for (const reference of candidates) { + const terminal = terminalReferences.get(reference.runId); + const isCarrier = String(reference.messageId) === String(reference.runId); + const isFinal = terminal?.messageId === reference.messageId; + if (terminal === undefined || (!isCarrier && !isFinal)) { + throw new Error( + `Stored event-stream assistant ${reference.messageId} has no exact terminal authority.`, + ); + } + references.push({ ...reference, isCarrier, isFinal }); + } + return references; +} + +function createReferenceState( + sessionId: SessionId, + reference: Pick = { + isCarrier: false, + isFinal: false, + }, +): ReferenceState { + return { + ...reference, + live: createInitialSessionLiveState({ sessionId, title: null, viewerId: "reference" }), + message: createMessageStreamReducerState(), + planJson: null, + }; +} + +function createContentReferenceState(): ContentReferenceState { + return { + filter: new ProviderPrivateMarkupStreamFilter(), + hasStreamText: false, + message: createMessageStreamReducerState(), + }; +} + +function reduceSanitizedMessageStreamRow( + state: ContentReferenceState, + row: StreamFoldableSessionEventRow, + maxTextLength: number, +): void { + if (row.event_type === "message.started" || row.event_type === "message.added") { + state.filter = new ProviderPrivateMarkupStreamFilter(); + state.hasStreamText = false; + } + + if (row.event_type === "message.added" || row.event_type === "message.delta") { + state.hasStreamText ||= row.content_text.length > 0; + reduceMessageStreamRow( + state.message, + { ...row, content_text: state.filter.push(row.content_text).text }, + { maxTextLength }, + ); + return; + } + + if (row.event_type === "message.completed") { + const trailingText = state.filter.finish().text; + if (trailingText.length > 0) { + reduceMessageStreamRow( + state.message, + { ...row, content_text: trailingText, event_type: "message.delta" }, + { maxTextLength }, + ); + } + reduceMessageStreamRow( + state.message, + { + ...row, + content_text: state.hasStreamText + ? "" + : sanitizeProviderPrivateMarkup(row.content_text).text, + }, + { maxTextLength }, + ); + return; + } + + reduceMessageStreamRow(state.message, row, { maxTextLength }); +} + +function toolKey(runId: SessionRunId, toolCallId: string): string { + return JSON.stringify([runId, toolCallId]); +} + +function mergeToolRouteIdentity( + route: ToolRoute, + row: { + tool_parent_message_id: string | null; + tool_result_message_id: string | null; + }, +): void { + if ( + (route.parentMessageId !== null && + row.tool_parent_message_id !== null && + route.parentMessageId !== row.tool_parent_message_id) || + (route.resultMessageId !== null && + row.tool_result_message_id !== null && + route.resultMessageId !== row.tool_result_message_id) + ) { + throw new Error(`Stored tool call ${route.toolCallId} changed its message identity.`); + } + route.parentMessageId ??= row.tool_parent_message_id; + route.resultMessageId ??= row.tool_result_message_id; +} + +function applyAgUiEvents(state: ReferenceState, events: readonly AgUiSessionEvent[]): void { + for (const event of events) { + state.live = applyAgUiEventToSessionLiveState(state.live, event); + } +} + +function messageAgUiEvent(row: ReferenceEventRow): AgUiSessionEvent { + const messageId = row.stream_id; + if (messageId === null) { + throw new Error(`Stored ${row.event_type} event is missing its stream identity.`); + } + + if (row.process_type === THOUGHT_PROCESS_TYPE) { + switch (row.event_type) { + case "thought.started": + return { messageId, role: "reasoning", type: EventType.REASONING_MESSAGE_START }; + case "thought.delta": + return { + delta: row.content_text, + messageId, + type: EventType.REASONING_MESSAGE_CONTENT, + }; + default: + return { messageId, type: EventType.REASONING_MESSAGE_END }; + } + } + + switch (row.event_type) { + case "message.added": + return { + delta: row.content_text, + messageId, + role: row.process_type === USER_MESSAGE_PROCESS_TYPE ? "user" : "assistant", + type: EventType.TEXT_MESSAGE_CHUNK, + }; + case "message.started": + return { + messageId, + role: row.process_type === USER_MESSAGE_PROCESS_TYPE ? "user" : "assistant", + type: EventType.TEXT_MESSAGE_START, + }; + case "message.delta": + return { delta: row.content_text, messageId, type: EventType.TEXT_MESSAGE_CONTENT }; + default: + return { messageId, type: EventType.TEXT_MESSAGE_END }; + } +} + +function applyReferenceEvent( + states: ReadonlyMap, + statesByRun: ReadonlyMap, + ceilings: ReadonlyMap, + toolRoutes: ReadonlyMap, + row: ReferenceEventRow, + options?: { activeArtifacts?: boolean }, +): void { + if (row.run_id === null || row.seq > (ceilings.get(row.run_id) ?? -1)) { + return; + } + + if (row.event_type === "plan.updated") { + const runStates = statesByRun.get(row.run_id) ?? []; + if (runStates.length > 0 && row.visibility !== "all_consumers") { + throw new Error(`Session run ${row.run_id} has a mixed-visibility plan stream.`); + } + const plan: unknown = JSON.parse(row.content_text); + if (!Array.isArray(plan)) { + throw new Error(`Session run ${row.run_id} has an invalid plan projection.`); + } + for (const state of runStates) { + state.planJson = row.content_text; + state.live = applyAgUiEventToSessionLiveState( + state.live, + createServerCustomEvent(MOSOO_CUSTOM_EVENT.sessionPlanUpdated.name, { + plan: plan as SessionViewPlanEntry[], + }), + ); + } + return; + } + + if ( + options?.activeArtifacts === true && + (row.process_type === THOUGHT_PROCESS_TYPE || row.process_type === USER_MESSAGE_PROCESS_TYPE) + ) { + if (row.visibility !== "all_consumers") { + throw new Error(`Session run ${row.run_id} has a mixed-visibility message stream.`); + } + const supported = + row.process_type === THOUGHT_PROCESS_TYPE + ? THOUGHT_EVENT_TYPES.has(row.event_type) + : MESSAGE_EVENT_TYPE_SET.has(row.event_type); + if (!supported) { + throw new Error(`Stored active message stream has unsupported event ${row.event_type}.`); + } + const event = messageAgUiEvent(row); + for (const state of new Set(statesByRun.get(row.run_id) ?? [])) { + state.live = applyAgUiEventToSessionLiveState(state.live, event); + } + return; + } + + if (row.process_type === "agent.message.delta") { + if (row.stream_id === null) { + return; + } + const state = states.get(referenceKey(row.run_id, row.stream_id)); + if (state === undefined || !state.isFinal) { + return; + } + if (row.visibility !== "all_consumers") { + throw new Error("Stored assistant reference has a mixed-visibility message stream."); + } + if (!MESSAGE_EVENT_TYPE_SET.has(row.event_type)) { + throw new Error(`Stored assistant reference has unsupported event ${row.event_type}.`); + } + reduceMessageStreamRow(state.message, row); + applyAgUiEvents(state, [messageAgUiEvent(row)]); + return; + } + + if (row.event_type !== "tool.call.updated") { + return; + } + if (row.visibility !== "all_consumers") { + throw new Error("Stored assistant reference has a mixed-visibility tool stream."); + } + if (row.tool_call_id === null) { + return; + } + const route = toolRoutes.get(toolKey(row.run_id, row.tool_call_id)); + if (route === undefined) { + return; + } + mergeToolRouteIdentity(route, row); + const resultMessageId = route.resultMessageId ?? route.parentMessageId; + if (resultMessageId === null) { + throw new Error(`Stored tool call ${row.tool_call_id} has no message identity.`); + } + const routedStates = new Set(); + for (const key of route.referenceKeys) { + const state = states.get(key); + if (state === undefined) { + continue; + } + routedStates.add(state); + } + for (const state of routedStates) { + const carrierAuthority = route.authority === "carrier"; + state.live = applyToolCallUpdateToSessionLiveState(state.live, { + inputDelta: row.tool_input_delta_json, + inputSnapshot: row.tool_input_json, + outputDelta: row.tool_output_delta_text, + outputSnapshot: row.tool_output_text, + parentMessageId: + row.tool_parent_message_id === null + ? null + : carrierAuthority + ? route.runId + : row.tool_parent_message_id, + resultMessageId: carrierAuthority ? route.runId : resultMessageId, + runId: row.run_id, + toolCallId: row.tool_call_id, + toolName: row.tool_name ?? "Tool", + }); + } +} + +function sessionStateAgUiEvent(row: SessionStateEventRow): AgUiSessionEvent { + if (row.visibility !== "all_consumers") { + throw new Error(`Session state event ${row.event_type} has mixed visibility.`); + } + const content: unknown = JSON.parse(row.content_text); + if (row.event_type === "plan.updated") { + if (!Array.isArray(content)) { + throw new Error("Session plan event has an invalid projection."); + } + return createServerCustomEvent(MOSOO_CUSTOM_EVENT.sessionPlanUpdated.name, { + plan: content as SessionViewPlanEntry[], + }); + } + const names = { + "session.commands.updated": MOSOO_CUSTOM_EVENT.sessionCommandsUpdated.name, + "session.config.updated": MOSOO_CUSTOM_EVENT.sessionConfigUpdated.name, + "session.mode.updated": MOSOO_CUSTOM_EVENT.sessionModeUpdated.name, + "usage.updated": MOSOO_CUSTOM_EVENT.sessionUsageUpdated.name, + } as const; + const name = names[row.event_type as keyof typeof names]; + if (name === undefined) { + throw new Error(`Unsupported Session state event ${row.event_type}.`); + } + return parseAgUiSessionEvent({ + name, + type: EventType.CUSTOM, + value: + row.event_type === "session.commands.updated" + ? { commands: content } + : row.event_type === "usage.updated" + ? { usage: parseNullableSessionUsageSummary(content) } + : content, + }); +} + +async function readTerminalReferences( + database: D1Database, + sessionId: SessionId, + runIdsInput: readonly SessionRunId[], +): Promise> { + const runIds = [...new Set(runIdsInput)]; + const terminalRows = await getAppDatabase(database) + .select({ + eventType: sessionEventsTable.eventType, + runId: sessionEventsTable.runId, + semanticHash: sessionEventsTable.semanticHash, + seq: sessionEventsTable.seq, + sourceEventId: sessionEventsTable.sourceEventId, + streamId: sessionEventsTable.streamId, + terminalEventJson: sessionEventsTable.terminalEventJson, + }) + .from(sessionEventsTable) + .where( + and( + eq(sessionEventsTable.sessionId, sessionId), + sql`${sessionEventsTable.runId} IN (SELECT value FROM json_each(${JSON.stringify(runIds)}))`, + inArray(sessionEventsTable.eventType, RUN_TERMINAL_EVENT_TYPES), + ), + ) + .all(); + + const referencesByRun = new Map(); + const seenRuns = new Set(); + for (const row of terminalRows) { + if (row.runId === null) { + continue; + } + if (seenRuns.has(row.runId)) { + throw new Error( + `Stored assistant reference has multiple terminal events for run ${row.runId}.`, + ); + } + seenRuns.add(row.runId); + if ( + (row.eventType !== "run.cancelled" && + row.eventType !== "run.completed" && + row.eventType !== "run.failed") || + row.sourceEventId !== createSessionRunTerminalSourceId(row.runId, row.eventType) + ) { + throw new Error(`Stored assistant reference has a non-canonical terminal source.`); + } + if (row.semanticHash === null) { + continue; + } + const semanticAuthority = await readTerminalEventSemanticAuthority({ + eventJson: row.terminalEventJson, + eventType: row.eventType, + runId: row.runId, + semanticHash: row.semanticHash, + sessionId, + sourceEventId: row.sourceEventId, + streamId: row.streamId, + }); + referencesByRun.set(row.runId, { + messageId: semanticAuthority.finalMessageId, + seq: row.seq, + }); + } + return referencesByRun; +} + +async function scanReferenceEvents( + database: D1Database, + input: { + ceilings: ReadonlyMap; + includePlan?: boolean; + messageIds?: readonly string[]; + references: readonly LightweightReference[]; + runIds?: readonly SessionRunId[]; + sessionId: SessionId; + toolRoutes: ReadonlyMap; + }, + apply: (row: ReferenceEventRow) => void, +): Promise { + const runIds = [...new Set(input.runIds ?? input.references.map((reference) => reference.runId))]; + const messageIds = [ + ...new Set(input.messageIds ?? input.references.map((reference) => reference.messageId)), + ]; + const toolCallIds = [...new Set([...input.toolRoutes.values()].map((route) => route.toolCallId))]; + const endSeq = Math.max(...runIds.map((runId) => input.ceilings.get(runId) ?? -1)); + let afterSeq: number | null = null; + + for (;;) { + const page = await getAppDatabase(database) + .select({ + content_text: sessionEventsTable.contentText, + ended_at: sessionEventsTable.endedAt, + event_type: sessionEventsTable.eventType, + id: sessionEventsTable.id, + occurred_at: sessionEventsTable.occurredAt, + process_type: sessionEventsTable.processType, + run_id: sessionEventsTable.runId, + seq: sessionEventsTable.seq, + stream_id: sessionEventsTable.streamId, + tokens: sessionEventsTable.tokens, + tool_call_id: sessionEventsTable.toolCallId, + tool_input_delta_json: sessionEventsTable.toolInputDeltaJson, + tool_input_json: sessionEventsTable.toolInputJson, + tool_name: sessionEventsTable.toolName, + tool_output_delta_text: sessionEventsTable.toolOutputDeltaText, + tool_output_text: sessionEventsTable.toolOutputText, + tool_parent_message_id: sessionEventsTable.toolParentMessageId, + tool_result_message_id: sessionEventsTable.toolResultMessageId, + tool_status: sessionEventsTable.toolStatus, + visibility: sessionEventsTable.visibility, + }) + .from(sessionEventsTable) + .where( + and( + eq(sessionEventsTable.sessionId, input.sessionId), + sql`${sessionEventsTable.runId} IN (SELECT value FROM json_each(${JSON.stringify(runIds)}))`, + lte(sessionEventsTable.seq, endSeq), + or( + and( + inArray(sessionEventsTable.processType, [ + "agent.message.delta", + THOUGHT_PROCESS_TYPE, + USER_MESSAGE_PROCESS_TYPE, + ]), + sql`${sessionEventsTable.streamId} IN (SELECT value FROM json_each(${JSON.stringify(messageIds)}))`, + ), + and( + eq(sessionEventsTable.eventType, "tool.call.updated"), + eq(sessionEventsTable.visibility, "all_consumers"), + toolCallIds.length === 0 + ? sql`0` + : sql`${sessionEventsTable.toolCallId} IN (SELECT value FROM json_each(${JSON.stringify(toolCallIds)}))`, + ), + input.includePlan === false ? sql`0` : eq(sessionEventsTable.eventType, "plan.updated"), + ), + afterSeq === null ? undefined : gt(sessionEventsTable.seq, afterSeq), + ), + ) + .orderBy(asc(sessionEventsTable.seq)) + .limit(REFERENCE_EVENT_PAGE_SIZE) + .all(); + + for (const row of page) { + apply(row); + } + if (page.length < REFERENCE_EVENT_PAGE_SIZE) { + return; + } + afterSeq = page[page.length - 1]?.seq ?? afterSeq; + } +} + +async function discoverToolRoutes( + database: D1Database, + input: { + ceilings: ReadonlyMap; + references: readonly CanonicalReference[]; + sessionId: SessionId; + }, +): Promise> { + const runIds = [...new Set(input.references.map((reference) => reference.runId))]; + const scopes = runIds.map((runId) => ({ + endSeq: input.ceilings.get(runId) ?? -1, + runId, + })); + const targetKeys = new Set( + input.references.map((reference) => referenceKey(reference.runId, reference.messageId)), + ); + const finalKeys = new Set( + input.references + .filter((reference) => reference.isFinal) + .map((reference) => referenceKey(reference.runId, reference.messageId)), + ); + const endSeq = Math.max(...runIds.map((runId) => input.ceilings.get(runId) ?? -1)); + const routes = new Map(); + let afterSeq: number | null = null; + + for (;;) { + const page = await getAppDatabase(database) + .select({ + run_id: sessionEventsTable.runId, + seq: sessionEventsTable.seq, + tool_call_id: sessionEventsTable.toolCallId, + tool_output_delta_text: sessionEventsTable.toolOutputDeltaText, + tool_output_text: sessionEventsTable.toolOutputText, + tool_parent_message_id: sessionEventsTable.toolParentMessageId, + tool_result_message_id: sessionEventsTable.toolResultMessageId, + }) + .from(sessionEventsTable) + .where( + and( + eq(sessionEventsTable.sessionId, input.sessionId), + sql`${sessionEventsTable.runId} IN (SELECT value FROM json_each(${JSON.stringify(runIds)}))`, + eq(sessionEventsTable.eventType, "tool.call.updated"), + eq(sessionEventsTable.visibility, "all_consumers"), + lte(sessionEventsTable.seq, endSeq), + sql`EXISTS ( + SELECT 1 + FROM json_each(${JSON.stringify(scopes)}) AS scope + WHERE json_extract(scope.value, '$.runId') = ${sessionEventsTable.runId} + AND ${sessionEventsTable.seq} <= json_extract(scope.value, '$.endSeq') + )`, + afterSeq === null ? undefined : gt(sessionEventsTable.seq, afterSeq), + ), + ) + .orderBy(asc(sessionEventsTable.seq)) + .limit(REFERENCE_EVENT_PAGE_SIZE) + .all(); + + for (const row of page) { + if ( + row.run_id === null || + row.tool_call_id === null || + row.seq > (input.ceilings.get(row.run_id) ?? -1) + ) { + continue; + } + const key = toolKey(row.run_id, row.tool_call_id); + const route = routes.get(key) ?? { + authority: null, + firstOutputSeq: null, + firstParentSeq: null, + parentMessageId: null, + referenceKeys: new Set(), + resultMessageId: null, + runId: row.run_id, + toolCallId: row.tool_call_id, + }; + mergeToolRouteIdentity(route, row); + if (row.tool_parent_message_id !== null) { + route.firstParentSeq ??= row.seq; + } + if (row.tool_output_delta_text !== null || row.tool_output_text !== null) { + route.firstOutputSeq ??= row.seq; + } + routes.set(key, route); + } + if (page.length < REFERENCE_EVENT_PAGE_SIZE) { + break; + } + afterSeq = page[page.length - 1]?.seq ?? afterSeq; + } + + for (const [key, route] of routes) { + const parentOwnsRoute = + route.firstParentSeq !== null && + (route.firstOutputSeq === null || route.firstParentSeq <= route.firstOutputSeq); + const parentKey = + route.parentMessageId === null ? null : referenceKey(route.runId, route.parentMessageId); + const parentIsFinal = parentOwnsRoute && parentKey !== null && finalKeys.has(parentKey); + const authorityMessageId = parentIsFinal ? route.parentMessageId : route.runId; + const hasEffectiveSegment = parentOwnsRoute || route.firstOutputSeq !== null; + if (hasEffectiveSegment && authorityMessageId !== null) { + const matchedKey = referenceKey(route.runId, authorityMessageId); + route.authority = parentIsFinal ? "final" : "carrier"; + if (!targetKeys.has(matchedKey)) { + throw new Error(`Stored tool call ${route.toolCallId} has no canonical carrier.`); + } + route.referenceKeys.add(matchedKey); + } + if (route.referenceKeys.size === 0) { + routes.delete(key); + } + } + + return routes; +} + +async function discoverActiveRunArtifactIdentities( + database: D1Database, + input: { + endSeq: number; + runId: SessionRunId; + sessionId: SessionId; + }, +): Promise<{ messageIds: string[]; toolRoutes: Map }> { + const messageIds = new Set(); + const toolRoutes = new Map(); + let afterSeq: number | null = null; + + for (;;) { + const page = await getAppDatabase(database) + .select({ + event_type: sessionEventsTable.eventType, + process_type: sessionEventsTable.processType, + seq: sessionEventsTable.seq, + stream_id: sessionEventsTable.streamId, + tool_call_id: sessionEventsTable.toolCallId, + tool_parent_message_id: sessionEventsTable.toolParentMessageId, + tool_result_message_id: sessionEventsTable.toolResultMessageId, + }) + .from(sessionEventsTable) + .where( + and( + eq(sessionEventsTable.sessionId, input.sessionId), + eq(sessionEventsTable.runId, input.runId), + lte(sessionEventsTable.seq, input.endSeq), + or( + and( + inArray(sessionEventsTable.processType, [ + "agent.message.delta", + USER_MESSAGE_PROCESS_TYPE, + ]), + inArray(sessionEventsTable.eventType, [...MESSAGE_STREAM_EVENT_TYPES]), + ), + and( + eq(sessionEventsTable.processType, THOUGHT_PROCESS_TYPE), + inArray(sessionEventsTable.eventType, [...THOUGHT_EVENT_TYPES]), + ), + eq(sessionEventsTable.eventType, "tool.call.updated"), + ), + afterSeq === null ? undefined : gt(sessionEventsTable.seq, afterSeq), + ), + ) + .orderBy(asc(sessionEventsTable.seq)) + .limit(REFERENCE_EVENT_PAGE_SIZE) + .all(); + + for (const row of page) { + if ( + row.process_type === "agent.message.delta" || + row.process_type === THOUGHT_PROCESS_TYPE || + row.process_type === USER_MESSAGE_PROCESS_TYPE + ) { + if (row.stream_id === null) { + throw new Error(`Stored ${row.event_type} event has no message stream identity.`); + } + messageIds.add(row.stream_id); + } + if (row.event_type !== "tool.call.updated" || row.tool_call_id === null) { + continue; + } + const key = toolKey(input.runId, row.tool_call_id); + const route = toolRoutes.get(key) ?? { + authority: null, + firstOutputSeq: null, + firstParentSeq: null, + parentMessageId: null, + referenceKeys: new Set(), + resultMessageId: null, + runId: input.runId, + toolCallId: row.tool_call_id, + }; + mergeToolRouteIdentity(route, row); + toolRoutes.set(key, route); + if (row.tool_parent_message_id !== null) { + messageIds.add(row.tool_parent_message_id); + } + if (row.tool_result_message_id !== null) { + messageIds.add(row.tool_result_message_id); + } + } + if (page.length < REFERENCE_EVENT_PAGE_SIZE) { + return { + messageIds: [...messageIds], + toolRoutes: new Map( + [...toolRoutes].filter( + ([, route]) => route.parentMessageId !== null || route.resultMessageId !== null, + ), + ), + }; + } + afterSeq = page[page.length - 1]?.seq ?? afterSeq; + } +} + +async function applyStoredSessionStateArtifacts( + database: D1Database, + input: { + endSeq: number; + sessionId: SessionId; + state: SessionLiveState; + }, +): Promise { + let state = input.state; + let afterSeq: number | null = null; + + for (;;) { + const page = await getAppDatabase(database) + .select({ + content_text: sessionEventsTable.contentText, + event_type: sessionEventsTable.eventType, + seq: sessionEventsTable.seq, + visibility: sessionEventsTable.visibility, + }) + .from(sessionEventsTable) + .where( + and( + eq(sessionEventsTable.sessionId, input.sessionId), + inArray(sessionEventsTable.eventType, [ + ...ACTIVE_SESSION_STATE_EVENT_TYPES, + "plan.updated", + ]), + lte(sessionEventsTable.seq, input.endSeq), + afterSeq === null ? undefined : gt(sessionEventsTable.seq, afterSeq), + ), + ) + .orderBy(asc(sessionEventsTable.seq)) + .limit(REFERENCE_EVENT_PAGE_SIZE) + .all(); + for (const row of page) { + state = applyAgUiEventToSessionLiveState(state, sessionStateAgUiEvent(row)); + } + if (page.length < REFERENCE_EVENT_PAGE_SIZE) { + return state; + } + afterSeq = page[page.length - 1]?.seq ?? afterSeq; + } +} + +export async function applyStoredSessionArtifacts( + database: D1Database, + input: { + endSeq: number; + includeActiveRunArtifacts: boolean; + runId: SessionRunId | null; + sessionId: SessionId; + state: SessionLiveState; + }, +): Promise { + const metadataState = await applyStoredSessionStateArtifacts(database, input); + if (input.runId === null || !input.includeActiveRunArtifacts) { + return metadataState; + } + + const { messageIds, toolRoutes } = await discoverActiveRunArtifactIdentities(database, { + endSeq: input.endSeq, + runId: input.runId, + sessionId: input.sessionId, + }); + const state: ReferenceState = { + isCarrier: false, + isFinal: true, + live: metadataState, + message: createMessageStreamReducerState(), + planJson: null, + }; + const activeKey = referenceKey(input.runId, "$active"); + const states = new Map([[activeKey, state]]); + for (const messageId of messageIds) { + states.set(referenceKey(input.runId, messageId), state); + } + for (const route of toolRoutes.values()) { + route.referenceKeys.add(activeKey); + } + const ceilings = new Map([[input.runId, input.endSeq]]); + const statesByRun = new Map([[input.runId, [state]]]); + + await scanReferenceEvents( + database, + { + ceilings, + includePlan: false, + messageIds, + references: [], + runIds: [input.runId], + sessionId: input.sessionId, + toolRoutes, + }, + (row) => { + applyReferenceEvent(states, statesByRun, ceilings, toolRoutes, row, { + activeArtifacts: true, + }); + }, + ); + + return state.live; +} + +async function readSealedReferenceCursor( + database: D1Database, + input: { + endSeq: number; + reference: LightweightReference; + sessionId: SessionId; + }, +): Promise { + const databaseClient = getAppDatabase(database); + const scope = [ + eq(sessionEventsTable.sessionId, input.sessionId), + eq(sessionEventsTable.runId, input.reference.runId), + eq(sessionEventsTable.streamId, input.reference.messageId), + eq(sessionEventsTable.processType, "agent.message.delta"), + lte(sessionEventsTable.seq, input.endSeq), + ]; + const [invalid, latest, boundary] = await Promise.all([ + databaseClient + .select({ id: sessionEventsTable.id }) + .from(sessionEventsTable) + .where( + and( + ...scope, + or( + ne(sessionEventsTable.visibility, "all_consumers"), + notInArray(sessionEventsTable.eventType, [...MESSAGE_STREAM_EVENT_TYPES]), + ), + ), + ) + .limit(1) + .get(), + databaseClient + .select({ eventType: sessionEventsTable.eventType, seq: sessionEventsTable.seq }) + .from(sessionEventsTable) + .where(and(...scope, inArray(sessionEventsTable.eventType, [...MESSAGE_STREAM_EVENT_TYPES]))) + .orderBy(desc(sessionEventsTable.seq)) + .limit(1) + .get(), + databaseClient + .select({ eventType: sessionEventsTable.eventType, seq: sessionEventsTable.seq }) + .from(sessionEventsTable) + .where( + and( + ...scope, + inArray(sessionEventsTable.eventType, [...MESSAGE_STREAM_AUTHORITY_BOUNDARY_EVENT_TYPES]), + ), + ) + .orderBy(desc(sessionEventsTable.seq)) + .limit(1) + .get(), + ]); + if (invalid !== undefined) { + throw new Error(`Stored assistant reference ${input.reference.messageId} is not public.`); + } + return latest?.eventType === "message.completed" && boundary?.eventType === "message.added" + ? { endSeq: latest.seq, startSeq: boundary.seq } + : null; +} + +async function readBoundedReferenceContent( + database: D1Database, + input: { + cursor: SealedReferenceCursor; + maxTextLength: number; + reference: LightweightReference; + sessionId: SessionId; + }, +): Promise { + const state = createContentReferenceState(); + if (input.maxTextLength === 0) { + return ""; + } + let afterSeq: number | null = null; + + for (;;) { + const filters: (SQL | undefined)[] = [ + eq(sessionEventsTable.sessionId, input.sessionId), + eq(sessionEventsTable.runId, input.reference.runId), + eq(sessionEventsTable.processType, "agent.message.delta"), + eq(sessionEventsTable.streamId, input.reference.messageId), + gte(sessionEventsTable.seq, input.cursor.startSeq), + lte(sessionEventsTable.seq, input.cursor.endSeq), + afterSeq === null ? undefined : gt(sessionEventsTable.seq, afterSeq), + ]; + const metadata: { contentLength: number; seq: number }[] = await getAppDatabase(database) + .select({ + contentLength: sql`length(${sessionEventsTable.contentText})`, + seq: sessionEventsTable.seq, + }) + .from(sessionEventsTable) + .where(and(...filters)) + .orderBy(asc(sessionEventsTable.seq)) + .limit(BOUNDED_REFERENCE_EVENT_PAGE_SIZE) + .all(); + if (metadata.length === 0) { + throw new Error(`Stored assistant reference ${input.reference.messageId} has no content.`); + } + let pageChars = 0; + let pageRowCount = 0; + for (const row of metadata) { + if (pageRowCount > 0 && pageChars + row.contentLength > BOUNDED_REFERENCE_EVENT_PAGE_CHARS) { + break; + } + pageChars += row.contentLength; + pageRowCount += 1; + } + const pageEndSeq: number | undefined = metadata[pageRowCount - 1]?.seq; + if (pageEndSeq === undefined) { + throw new Error(`Stored assistant reference ${input.reference.messageId} has no content.`); + } + const page = await getAppDatabase(database) + .select({ + content_text: sessionEventsTable.contentText, + ended_at: sessionEventsTable.endedAt, + event_type: sessionEventsTable.eventType, + id: sessionEventsTable.id, + occurred_at: sessionEventsTable.occurredAt, + process_type: sessionEventsTable.processType, + run_id: sessionEventsTable.runId, + seq: sessionEventsTable.seq, + stream_id: sessionEventsTable.streamId, + tokens: sessionEventsTable.tokens, + visibility: sessionEventsTable.visibility, + }) + .from(sessionEventsTable) + .where(and(...filters, lte(sessionEventsTable.seq, pageEndSeq))) + .orderBy(asc(sessionEventsTable.seq)) + .all(); + + for (const row of page) { + reduceSanitizedMessageStreamRow(state, row, input.maxTextLength); + if (state.message.text.length >= input.maxTextLength) { + return state.message.text; + } + } + if ( + pageEndSeq === input.cursor.endSeq || + (pageRowCount === metadata.length && metadata.length < BOUNDED_REFERENCE_EVENT_PAGE_SIZE) + ) { + if (!state.message.authoritative || !state.message.sealed) { + throw new Error( + `Stored assistant reference ${input.reference.messageId} is not sealed and authoritative.`, + ); + } + return state.message.text; + } + afterSeq = pageEndSeq; + } +} + +async function assertReferenceMessageIdentities( + database: D1Database, + input: { + ceilings: ReadonlyMap; + references: readonly LightweightReference[]; + sessionId: SessionId; + }, +): Promise { + const scopes = input.references.map((reference) => ({ + endSeq: input.ceilings.get(reference.runId) ?? -1, + messageId: reference.messageId, + runId: reference.runId, + })); + const collision = await getAppDatabase(database) + .select({ id: sessionEventsTable.id }) + .from(sessionEventsTable) + .where( + and( + eq(sessionEventsTable.sessionId, input.sessionId), + inArray(sessionEventsTable.eventType, [...MESSAGE_STREAM_EVENT_TYPES]), + sql`EXISTS ( + SELECT 1 + FROM json_each(${JSON.stringify(scopes)}) AS scope + WHERE json_extract(scope.value, '$.messageId') = ${sessionEventsTable.streamId} + AND ${sessionEventsTable.seq} <= json_extract(scope.value, '$.endSeq') + AND ( + ${sessionEventsTable.runId} IS NOT json_extract(scope.value, '$.runId') + OR ${sessionEventsTable.processType} <> 'agent.message.delta' + ) + )`, + ), + ) + .limit(1) + .get(); + + if (collision !== undefined) { + throw new Error("Stored assistant reference has conflicting message-stream identity rows."); + } +} + +export async function resolveStoredSessionMessageContentReferences< + Row extends StoredSessionMessageContentReferenceRow, +>( + database: D1Database, + sessionId: SessionId, + rows: readonly Row[], + maxTextLength: number, +): Promise { + for (const row of rows) { + if ( + row.projection_format === "event_stream_v3" && + (row.role !== "assistant" || row.session_run_id === null || row.content_text !== "") + ) { + throw new Error(`Stored event-stream assistant ${row.id} is not lightweight.`); + } + } + const candidates = rows.flatMap((row): LightweightReference[] => { + const reference = potentialReference(row); + return reference === null ? [] : [reference]; + }); + if (candidates.length === 0) { + return [...rows]; + } + const terminalReferences = await readTerminalReferences( + database, + sessionId, + candidates.map((candidate) => candidate.runId), + ); + const references = requireTerminalReferences(candidates, terminalReferences); + const ceilings = new Map( + references.map((reference) => [reference.runId, terminalReferences.get(reference.runId)!.seq]), + ); + const finalReferences = references.filter((reference) => reference.isFinal); + await assertReferenceMessageIdentities(database, { + ceilings, + references: finalReferences, + sessionId, + }); + const resolvedText = new Map(); + for (const reference of finalReferences) { + const endSeq = ceilings.get(reference.runId); + if (endSeq === undefined) { + throw new Error( + `Stored event-stream assistant ${reference.messageId} has no terminal ceiling.`, + ); + } + const cursor = await readSealedReferenceCursor(database, { + endSeq, + reference, + sessionId, + }); + if (cursor === null) { + throw new Error( + `Stored assistant reference ${reference.messageId} is not sealed and authoritative.`, + ); + } + resolvedText.set( + referenceKey(reference.runId, reference.messageId), + await readBoundedReferenceContent(database, { + cursor, + maxTextLength, + reference, + sessionId, + }), + ); + } + + return rows.map((row) => { + if (row.session_run_id === null) { + return row; + } + const contentText = resolvedText.get(referenceKey(row.session_run_id, row.id)); + if (contentText === undefined) { + return row; + } + return { ...row, content_text: contentText }; + }); +} + +async function resolveStoredSessionMessageSnapshot( + database: D1Database, + sessionId: SessionId, + rows: readonly Row[], +): Promise { + for (const row of rows) { + if ( + row.projection_format === "event_stream_v3" && + (row.role !== "assistant" || + row.session_run_id === null || + row.content_text !== "" || + row.plan_json !== null || + row.segments_json !== null) + ) { + throw new Error(`Stored event-stream assistant ${row.id} is not lightweight.`); + } + } + const candidates = rows.flatMap((row): LightweightReference[] => { + const reference = potentialReference(row); + return reference === null ? [] : [reference]; + }); + if (candidates.length === 0) { + return [...rows]; + } + const terminalReferences = await readTerminalReferences( + database, + sessionId, + candidates.map((candidate) => candidate.runId), + ); + const references = requireTerminalReferences(candidates, terminalReferences); + const ceilings = new Map( + references.map((reference) => [reference.runId, terminalReferences.get(reference.runId)!.seq]), + ); + await assertReferenceMessageIdentities(database, { + ceilings, + references: references.filter((reference) => reference.isFinal), + sessionId, + }); + const states = new Map(); + const statesByRun = new Map(); + for (const reference of references) { + const state = createReferenceState(sessionId, reference); + states.set(referenceKey(reference.runId, reference.messageId), state); + if (reference.isFinal) { + const runStates = statesByRun.get(reference.runId) ?? []; + runStates.push(state); + statesByRun.set(reference.runId, runStates); + } + } + + const toolRoutes = await discoverToolRoutes(database, { ceilings, references, sessionId }); + await scanReferenceEvents(database, { ceilings, references, sessionId, toolRoutes }, (row) => + applyReferenceEvent(states, statesByRun, ceilings, toolRoutes, row), + ); + + return rows.map((row) => { + if (row.session_run_id === null) { + return row; + } + const state = states.get(referenceKey(row.session_run_id, row.id)); + if (state === undefined) { + return row; + } + if (state.isFinal && (!state.message.authoritative || !state.message.sealed)) { + throw new Error(`Stored assistant reference ${row.id} is not sealed and authoritative.`); + } + const message = state.live.messages.find((candidate) => candidate.id === row.id); + if ( + message === undefined || + (state.isFinal && message.content !== state.message.text) || + (!state.isFinal && message.content !== "") || + (!state.isFinal && + !message.segments.some( + (segment) => segment.kind === "tool_result" || segment.kind === "tool_use", + )) + ) { + throw new Error(`Stored assistant reference ${row.id} diverges from the live reducer.`); + } + if (state.live.messages.some((candidate) => candidate.id !== row.id)) { + throw new Error(`Stored assistant reference ${row.id} produced a detached message.`); + } + + return { + ...row, + content_text: state.isFinal ? state.message.text : "", + plan_json: state.isFinal ? state.planJson : null, + segments_json: JSON.stringify(message.segments), + }; + }); +} + +export async function resolveStoredSessionMessageReferences< + Row extends StoredSessionMessageReferenceRow, +>(database: D1Database, sessionId: SessionId, rows: readonly Row[]): Promise { + return resolveStoredSessionMessageSnapshot(database, sessionId, rows); +} + +export async function resolveStoredSessionMessageSnapshotReferences< + Row extends StoredSessionMessageReferenceRow, +>(database: D1Database, sessionId: SessionId, rows: readonly Row[]): Promise { + return resolveStoredSessionMessageSnapshot(database, sessionId, rows); +} diff --git a/apps/api/src/modules/sessions/infrastructure/session-message-snapshot.repository.ts b/apps/api/src/modules/sessions/infrastructure/session-message-snapshot.repository.ts index 564b9a3c..cd1e94ee 100644 --- a/apps/api/src/modules/sessions/infrastructure/session-message-snapshot.repository.ts +++ b/apps/api/src/modules/sessions/infrastructure/session-message-snapshot.repository.ts @@ -1,5 +1,5 @@ import { sessionMessagesTable } from "@mosoo/db"; -import type { SessionId, SessionMessageId } from "@mosoo/id"; +import type { SessionId, SessionMessageId, SessionRunId } from "@mosoo/id"; import { asc, eq } from "drizzle-orm"; import { getAppDatabase } from "../../../platform/db/drizzle"; @@ -10,6 +10,7 @@ import { } from "../domain/provider-private-markup"; import { parseStoredSessionMessageProjection } from "../domain/session-message-projection-parser"; import type { SessionLiveStateMessage } from "./session-live-state.types"; +import { resolveStoredSessionMessageSnapshotReferences } from "./session-message-reference.repository"; export interface StoredSessionMessageRow { content_text: string; @@ -19,6 +20,7 @@ export interface StoredSessionMessageRow { role: "assistant" | "user"; segments_json: string | null; seq: number; + session_run_id: SessionRunId | null; } function compareStoredSessionMessageRows( @@ -50,7 +52,7 @@ function toLiveStateMessage(row: StoredSessionMessageRow): SessionLiveStateMessa function storedSessionMessageRowsToLiveMessages( rows: StoredSessionMessageRow[], ): SessionLiveStateMessage[] { - return [...rows].toSorted(compareStoredSessionMessageRows).map((row) => toLiveStateMessage(row)); + return [...rows].toSorted(compareStoredSessionMessageRows).map(toLiveStateMessage); } export async function loadStoredSessionMessages( @@ -63,14 +65,22 @@ export async function loadStoredSessionMessages( created_at: sessionMessagesTable.createdAt, id: sessionMessagesTable.id, plan_json: sessionMessagesTable.planJson, + projection_format: sessionMessagesTable.projectionFormat, role: sessionMessagesTable.role, segments_json: sessionMessagesTable.segmentsJson, seq: sessionMessagesTable.seq, + session_run_id: sessionMessagesTable.sessionRunId, }) .from(sessionMessagesTable) .where(eq(sessionMessagesTable.sessionId, sessionId)) .orderBy(asc(sessionMessagesTable.seq)) .all(); - return storedSessionMessageRowsToLiveMessages(results); + const resolved = await resolveStoredSessionMessageSnapshotReferences( + database, + sessionId, + results, + ); + + return storedSessionMessageRowsToLiveMessages(resolved); } diff --git a/apps/api/src/modules/sessions/infrastructure/session-model-call.repository.ts b/apps/api/src/modules/sessions/infrastructure/session-model-call.repository.ts index 8de48d19..6c5cea09 100644 --- a/apps/api/src/modules/sessions/infrastructure/session-model-call.repository.ts +++ b/apps/api/src/modules/sessions/infrastructure/session-model-call.repository.ts @@ -1,6 +1,8 @@ +import type { SessionRunStatus } from "@mosoo/contracts/session-run"; import { agentsTable, appsTable, + sessionEventsTable, sessionModelCallsTable, sessionRunsTable, sessionsTable, @@ -13,16 +15,23 @@ import type { DriverInstanceId, OrganizationId, AppId, + RuntimeEventId, SessionId, SessionModelCallId, SessionRunId, } from "@mosoo/id"; -import { and, eq, sql } from "drizzle-orm"; +import { and, eq, exists, sql } from "drizzle-orm"; +import type { SQL } from "drizzle-orm"; import { getAppDatabase, runAppDatabaseBatch } from "../../../platform/db/drizzle"; import { isTruthy } from "../../../shared/truthiness"; import { currentTimestampMs } from "../../../time"; -import { createRuntimeUsageEventUpsert } from "../../cost/application/cost-usage-event.service"; +import { + createRuntimeUsageEventConvergencePredicate, + createRuntimeUsageEventUnrolledPredicate, + createRuntimeUsageEventUpsert, + hasRuntimeUsageEventRollupReceipt, +} from "../../cost/application/cost-usage-event.service"; import type { SessionUsageSummary } from "./session-live-state.types"; interface SessionModelCallRunRow { agent_id: AgentId; @@ -31,6 +40,7 @@ interface SessionModelCallRunRow { agent_status: "draft" | "published"; actor_user_id: AccountId; completed_at: number | null; + created_at: number; model: string | null; app_organization_id: OrganizationId; app_id: AppId; @@ -41,20 +51,52 @@ interface SessionModelCallRunRow { session_provider: string; session_runtime_id: string; started_at: number | null; + status: SessionRunStatus; trigger: "resume" | "retry" | "system" | "user_prompt"; } export type SessionModelCallStatus = "completed" | "failed" | "started"; export interface UpsertSessionModelCallUsageInput { + createdAtMs: number; driverInstanceId: DriverInstanceId; sessionId: SessionId; sessionRunId: SessionRunId; - status: SessionModelCallStatus; + sourceEventSeq: number; traceId: string; usage: SessionUsageSummary | null; } +export interface DurableSessionModelCallUsageProjectionInput extends UpsertSessionModelCallUsageInput { + eventId: RuntimeEventId; + semanticHash: string; +} + +type SessionModelCallInsert = typeof sessionModelCallsTable.$inferInsert; + +interface StoredSessionModelCall { + cacheCreationTokens: number | null; + cacheReadTokens: number | null; + costCurrency: string | null; + driverInstanceId: string | null; + inputTokens: number | null; + metadataJson: string | null; + model: string; + nativeCallId: string | null; + outputTokens: number | null; + provider: string; + sessionId: string; + sessionRunId: string; + sourceEventSeq: number; + startedAt: number | null; + totalCostUsdMicros: number | null; + traceId: string; +} + +function selectedValue(value: Value, alias: string) { + return sql`${value}`.as(alias); +} + function toTokenCount(value: number | null | undefined): number | null { if (typeof value !== "number" || !Number.isFinite(value) || value < 0) { return null; @@ -73,9 +115,14 @@ function toUsdMicros(value: number | null | undefined): number | null { function buildUsageMetadata(usage: SessionUsageSummary): string { return JSON.stringify({ + cachedReadTokens: usage.cachedReadTokens ?? null, cachedWriteTokens: usage.cachedWriteTokens ?? null, callId: usage.callId ?? null, + costAmount: usage.costAmount ?? null, + costCurrency: usage.costCurrency ?? null, + inputTokens: usage.inputTokens ?? null, model: usage.model ?? null, + outputTokens: usage.outputTokens ?? null, provider: usage.provider ?? null, size: usage.size ?? null, source: usage.source, @@ -99,6 +146,7 @@ async function getSessionModelCallRunRow( agent_revision_id: sessionRunsTable.deploymentVersionId, agent_status: sql<"draft" | "published">`${agentsTable.status}`, completed_at: sessionRunsTable.completedAt, + created_at: sessionRunsTable.createdAt, model: sql`${sessionRunsTable.model}`.mapWith(sessionRunsTable.model).as("model"), app_organization_id: appsTable.organizationId, app_id: sessionsTable.appId, @@ -117,6 +165,7 @@ async function getSessionModelCallRunRow( .mapWith(sessionsTable.runtimeId) .as("session_runtime_id"), started_at: sessionRunsTable.startedAt, + status: sessionRunsTable.status, trigger: sessionRunsTable.trigger, }) .from(sessionRunsTable) @@ -135,31 +184,217 @@ async function getSessionModelCallRunRow( ); } -export async function upsertSessionModelCallUsage( +function toSessionModelCallStatus(status: SessionRunStatus): SessionModelCallStatus { + if (status === "completed") { + return "completed"; + } + + if (status === "failed" || status === "cancelled" || status === "expired") { + return "failed"; + } + + return "started"; +} + +function createSessionModelCallInsertSelect( + database: ReturnType, + values: SessionModelCallInsert, + writeFence: SQL, +) { + return database + .select({ + cacheCreationTokens: selectedValue(values.cacheCreationTokens, "cache_creation_tokens"), + cacheReadTokens: selectedValue(values.cacheReadTokens, "cache_read_tokens"), + callKey: selectedValue(values.callKey, "call_key"), + completedAt: selectedValue(values.completedAt, "completed_at"), + costCurrency: selectedValue(values.costCurrency, "cost_currency"), + createdAt: selectedValue(values.createdAt, "created_at"), + driverInstanceId: selectedValue(values.driverInstanceId, "driver_instance_id"), + errorCode: selectedValue(values.errorCode, "error_code"), + errorMessage: selectedValue(values.errorMessage, "error_message"), + id: selectedValue(values.id, "id"), + inputTokens: selectedValue(values.inputTokens, "input_tokens"), + metadataJson: selectedValue(values.metadataJson, "metadata_json"), + model: selectedValue(values.model, "model"), + nativeCallId: selectedValue(values.nativeCallId, "native_call_id"), + outputTokens: selectedValue(values.outputTokens, "output_tokens"), + provider: selectedValue(values.provider, "provider"), + sourceEventSeq: selectedValue(values.sourceEventSeq, "source_event_seq"), + sessionId: selectedValue(values.sessionId, "session_id"), + sessionRunId: selectedValue(values.sessionRunId, "session_run_id"), + startedAt: selectedValue(values.startedAt, "started_at"), + status: selectedValue(values.status, "status"), + totalCostUsdMicros: selectedValue(values.totalCostUsdMicros, "total_cost_usd_micros"), + traceId: selectedValue(values.traceId, "trace_id"), + updatedAt: selectedValue(values.updatedAt, "updated_at"), + }) + .from(sql`(SELECT 1)`) + .where(writeFence); +} + +function createSessionModelCallUsageUpsert( + database: ReturnType, + values: SessionModelCallInsert, + writeFence: SQL, +) { + return database + .insert(sessionModelCallsTable) + .select(createSessionModelCallInsertSelect(database, values, writeFence)) + .onConflictDoUpdate({ + set: { + cacheCreationTokens: sql`COALESCE(excluded.cache_creation_tokens, ${sessionModelCallsTable.cacheCreationTokens})`, + cacheReadTokens: sql`COALESCE(excluded.cache_read_tokens, ${sessionModelCallsTable.cacheReadTokens})`, + completedAt: sql`COALESCE(excluded.completed_at, ${sessionModelCallsTable.completedAt})`, + costCurrency: sql`COALESCE(excluded.cost_currency, ${sessionModelCallsTable.costCurrency})`, + driverInstanceId: sql`excluded.driver_instance_id`, + inputTokens: sql`COALESCE(excluded.input_tokens, ${sessionModelCallsTable.inputTokens})`, + metadataJson: sql`excluded.metadata_json`, + model: sql`excluded.model`, + outputTokens: sql`COALESCE(excluded.output_tokens, ${sessionModelCallsTable.outputTokens})`, + provider: sql`excluded.provider`, + sourceEventSeq: sql`excluded.source_event_seq`, + startedAt: sql`COALESCE(${sessionModelCallsTable.startedAt}, excluded.started_at)`, + status: sql`excluded.status`, + totalCostUsdMicros: sql`COALESCE(excluded.total_cost_usd_micros, ${sessionModelCallsTable.totalCostUsdMicros})`, + traceId: sql`excluded.trace_id`, + updatedAt: sql`excluded.updated_at`, + }, + setWhere: sql`${sessionModelCallsTable.sourceEventSeq} < excluded.source_event_seq AND ${writeFence}`, + target: [sessionModelCallsTable.sessionRunId, sessionModelCallsTable.callKey], + }); +} + +function createSessionModelCallUsageConvergenceGuard( + database: ReturnType, + modelCallValues: SessionModelCallInsert, + usageEventInput: Parameters[1], +) { + return database.insert(sessionModelCallsTable).select( + createSessionModelCallInsertSelect( + database, + modelCallValues, + sql`${createRuntimeUsageEventUnrolledPredicate(database, usageEventInput)} + AND NOT (${createRuntimeUsageEventConvergencePredicate(database, usageEventInput)})`, + ), + ); +} + +async function getStoredSessionModelCall( + database: D1Database, + input: { callKey: string; sessionRunId: SessionRunId }, +): Promise { + return ( + (await getAppDatabase(database) + .select({ + cacheCreationTokens: sessionModelCallsTable.cacheCreationTokens, + cacheReadTokens: sessionModelCallsTable.cacheReadTokens, + costCurrency: sessionModelCallsTable.costCurrency, + driverInstanceId: sessionModelCallsTable.driverInstanceId, + inputTokens: sessionModelCallsTable.inputTokens, + metadataJson: sessionModelCallsTable.metadataJson, + model: sessionModelCallsTable.model, + nativeCallId: sessionModelCallsTable.nativeCallId, + outputTokens: sessionModelCallsTable.outputTokens, + provider: sessionModelCallsTable.provider, + sessionId: sessionModelCallsTable.sessionId, + sessionRunId: sessionModelCallsTable.sessionRunId, + sourceEventSeq: sessionModelCallsTable.sourceEventSeq, + startedAt: sessionModelCallsTable.startedAt, + totalCostUsdMicros: sessionModelCallsTable.totalCostUsdMicros, + traceId: sessionModelCallsTable.traceId, + }) + .from(sessionModelCallsTable) + .where( + and( + eq(sessionModelCallsTable.sessionRunId, input.sessionRunId), + eq(sessionModelCallsTable.callKey, input.callKey), + ), + ) + .limit(1) + .get()) ?? null + ); +} + +function assertSessionModelCallConverged( + stored: StoredSessionModelCall | null, + expected: SessionModelCallInsert, +): void { + const sourceEventSeq = expected.sourceEventSeq ?? 0; + + if (stored === null || stored.sourceEventSeq < sourceEventSeq) { + throw new Error("Session model call CAS did not persist the durable event."); + } + + if (stored.sourceEventSeq > sourceEventSeq) { + return; + } + + const requiredValuesMatch = + stored.driverInstanceId === (expected.driverInstanceId ?? null) && + stored.metadataJson === (expected.metadataJson ?? null) && + stored.model === expected.model && + stored.nativeCallId === (expected.nativeCallId ?? null) && + stored.provider === expected.provider && + stored.sessionId === expected.sessionId && + stored.sessionRunId === expected.sessionRunId && + stored.startedAt === (expected.startedAt ?? null) && + stored.traceId === expected.traceId; + const optionalValuesMatch = + (expected.cacheCreationTokens == null || + stored.cacheCreationTokens === expected.cacheCreationTokens) && + (expected.cacheReadTokens == null || stored.cacheReadTokens === expected.cacheReadTokens) && + (expected.costCurrency == null || stored.costCurrency === expected.costCurrency) && + (expected.inputTokens == null || stored.inputTokens === expected.inputTokens) && + (expected.outputTokens == null || stored.outputTokens === expected.outputTokens) && + (expected.totalCostUsdMicros == null || + stored.totalCostUsdMicros === expected.totalCostUsdMicros); + + if (!requiredValuesMatch || !optionalValuesMatch) { + throw new Error("Session model call event seq was replayed with conflicting content."); + } +} + +async function prepareSessionModelCallUsageValues( database: D1Database, input: UpsertSessionModelCallUsageInput, -): Promise { +): Promise<{ + modelCallValues: SessionModelCallInsert; + usageEventInput: Parameters[1]; +} | null> { if (!input.usage) { - return; + return null; + } + + if (!Number.isSafeInteger(input.sourceEventSeq) || input.sourceEventSeq < 0) { + throw new Error("Session model call source event seq must be a non-negative safe integer."); + } + if (!Number.isSafeInteger(input.createdAtMs) || input.createdAtMs < 0) { + throw new Error("Session model call createdAtMs must be a non-negative safe integer."); } - const usage = input.usage; + const usage = input.usage; const run = await getSessionModelCallRunRow(database, input.sessionRunId); if (!run) { throw new Error("Session run not found for model call usage."); } + if (run.session_id !== input.sessionId) { + throw new Error("Session model call usage does not belong to the durable Session Run."); + } + const timestampMs = currentTimestampMs(); - const completedAt = - input.status === "completed" || input.status === "failed" - ? (run.completed_at ?? timestampMs) - : null; + const status = toSessionModelCallStatus(run.status); + const completedAt = status === "started" ? null : run.completed_at; + + if (status !== "started" && completedAt === null) { + throw new Error("Terminal Session Run is missing completed_at for model call usage."); + } + const nativeCallId = normalizeUsageCallId(usage.callId); const callKey = isTruthy(nativeCallId) ? `model_call:${nativeCallId}` : "run_usage"; const provider = run.provider ?? run.session_provider; const model = run.model ?? run.session_model; - const usageEventInput = { callKey, driverInstanceId: input.driverInstanceId, @@ -170,7 +405,7 @@ export async function upsertSessionModelCallUsage( agentOwnerUserId: run.agent_owner_user_id, agentRevisionId: run.agent_revision_id, agentStatus: run.agent_status, - createdAtMs: completedAt ?? timestampMs, + createdAtMs: input.createdAtMs, model, organizationId: run.app_organization_id, appId: run.app_id, @@ -180,66 +415,235 @@ export async function upsertSessionModelCallUsage( sessionRunId: input.sessionRunId, trigger: run.trigger, }, + sourceEventSeq: input.sourceEventSeq, usage, } satisfies Parameters[1]; + const modelCallValues = { + cacheCreationTokens: toTokenCount(usage.cachedWriteTokens), + cacheReadTokens: toTokenCount(usage.cachedReadTokens), + callKey, + completedAt, + costCurrency: usage.costCurrency ?? null, + createdAt: input.createdAtMs, + driverInstanceId: input.driverInstanceId, + errorCode: null, + errorMessage: null, + id: createPlatformId(), + inputTokens: toTokenCount(usage.inputTokens), + metadataJson: buildUsageMetadata(usage), + model, + nativeCallId, + outputTokens: toTokenCount(usage.outputTokens), + provider, + sessionId: input.sessionId, + sessionRunId: input.sessionRunId, + sourceEventSeq: input.sourceEventSeq, + startedAt: run.started_at ?? run.created_at, + status, + totalCostUsdMicros: toUsdMicros(usage.costAmount), + traceId: input.traceId, + updatedAt: timestampMs, + } satisfies SessionModelCallInsert; + + return { modelCallValues, usageEventInput }; +} + +export async function prepareDurableSessionModelCallUsageProjection( + database: D1Database, + input: DurableSessionModelCallUsageProjectionInput, +): Promise { + const prepared = await prepareSessionModelCallUsageValues(database, input); + + if (prepared === null) { + return []; + } + + const { modelCallValues, usageEventInput } = prepared; + const appDatabase = getAppDatabase(database); + const receiptFence = exists( + appDatabase + .select({ id: sessionEventsTable.id }) + .from(sessionEventsTable) + .where( + and( + eq(sessionEventsTable.id, input.eventId), + eq(sessionEventsTable.sessionId, input.sessionId), + eq(sessionEventsTable.eventType, "usage.updated"), + eq(sessionEventsTable.runId, input.sessionRunId), + eq(sessionEventsTable.semanticHash, input.semanticHash), + eq(sessionEventsTable.seq, input.sourceEventSeq), + ), + ), + ); + const writeFence = sql`${createRuntimeUsageEventUnrolledPredicate( + appDatabase, + usageEventInput, + )} AND ${receiptFence}`; + const modelCallQuery = createSessionModelCallUsageUpsert( + appDatabase, + modelCallValues, + writeFence, + ).toSQL(); + const statements = [database.prepare(modelCallQuery.sql).bind(...modelCallQuery.params)]; + const usageEventUpsert = createRuntimeUsageEventUpsert( + appDatabase, + usageEventInput, + receiptFence, + ); + + if (usageEventUpsert !== null) { + const usageEventQuery = usageEventUpsert.toSQL(); + statements.push(database.prepare(usageEventQuery.sql).bind(...usageEventQuery.params)); + } + + const usageEventConvergenceQuery = + usageEventUpsert === null + ? null + : appDatabase + .select({ converged: sql`1` }) + .from(sql`(SELECT 1)`) + .where(createRuntimeUsageEventConvergencePredicate(appDatabase, usageEventInput)) + .toSQL(); + const usageEventFailureSql = + usageEventConvergenceQuery === null ? "" : ` OR NOT EXISTS (${usageEventConvergenceQuery.sql})`; + + const sourceEventSeq = modelCallValues.sourceEventSeq ?? 0; + statements.push( + database + .prepare( + `INSERT INTO session_event (id) + SELECT ? + WHERE EXISTS ( + SELECT 1 + FROM session_event AS receipt + WHERE receipt.id = ? + AND receipt.session_id = ? + AND receipt.event_type = 'usage.updated' + AND receipt.run_id = ? + AND receipt.semantic_hash = ? + AND receipt.seq = ? + ) + AND ( + NOT EXISTS ( + SELECT 1 + FROM session_model_call AS stored + WHERE stored.session_run_id = ? + AND stored.call_key = ? + AND ( + stored.source_event_seq > ? + OR ( + stored.source_event_seq = ? + AND stored.driver_instance_id IS ? + AND stored.metadata_json IS ? + AND stored.model = ? + AND stored.native_call_id IS ? + AND stored.provider = ? + AND stored.session_id = ? + AND stored.started_at IS ? + AND stored.trace_id = ? + AND (? IS NULL OR stored.cache_creation_tokens = ?) + AND (? IS NULL OR stored.cache_read_tokens = ?) + AND (? IS NULL OR stored.cost_currency = ?) + AND (? IS NULL OR stored.input_tokens = ?) + AND (? IS NULL OR stored.output_tokens = ?) + AND (? IS NULL OR stored.total_cost_usd_micros = ?) + ) + ) + )${usageEventFailureSql} + )`, + ) + .bind( + input.eventId, + input.eventId, + input.sessionId, + input.sessionRunId, + input.semanticHash, + input.sourceEventSeq, + input.sessionRunId, + modelCallValues.callKey, + sourceEventSeq, + sourceEventSeq, + modelCallValues.driverInstanceId ?? null, + modelCallValues.metadataJson ?? null, + modelCallValues.model, + modelCallValues.nativeCallId ?? null, + modelCallValues.provider, + modelCallValues.sessionId, + modelCallValues.startedAt ?? null, + modelCallValues.traceId, + modelCallValues.cacheCreationTokens ?? null, + modelCallValues.cacheCreationTokens ?? null, + modelCallValues.cacheReadTokens ?? null, + modelCallValues.cacheReadTokens ?? null, + modelCallValues.costCurrency ?? null, + modelCallValues.costCurrency ?? null, + modelCallValues.inputTokens ?? null, + modelCallValues.inputTokens ?? null, + modelCallValues.outputTokens ?? null, + modelCallValues.outputTokens ?? null, + modelCallValues.totalCostUsdMicros ?? null, + modelCallValues.totalCostUsdMicros ?? null, + ...(usageEventConvergenceQuery?.params ?? []), + ), + ); + + return statements; +} + +export async function upsertSessionModelCallUsage( + database: D1Database, + input: UpsertSessionModelCallUsageInput, +): Promise { + const prepared = await prepareSessionModelCallUsageValues(database, input); + + if (prepared === null) { + return; + } + const { modelCallValues, usageEventInput } = prepared; + const callKey = modelCallValues.callKey; + + if (await hasRuntimeUsageEventRollupReceipt(database, usageEventInput)) { + const stored = await getStoredSessionModelCall(database, { + callKey, + sessionRunId: input.sessionRunId, + }); + + if (stored !== null && stored.sourceEventSeq >= input.sourceEventSeq) { + assertSessionModelCallConverged(stored, modelCallValues); + return; + } + + throw new Error( + "Session model call usage was already rolled up and cannot be replaced safely.", + ); + } await runAppDatabaseBatch(database, (appDatabase) => { - const modelCallUpsert = appDatabase - .insert(sessionModelCallsTable) - .values({ - cacheCreationTokens: toTokenCount(usage.cachedWriteTokens), - cacheReadTokens: toTokenCount(usage.cachedReadTokens), - callKey, - completedAt, - costCurrency: usage.costCurrency ?? null, - createdAt: timestampMs, - driverInstanceId: input.driverInstanceId, - errorCode: null, - errorMessage: null, - id: createPlatformId(), - inputTokens: toTokenCount(usage.inputTokens), - metadataJson: buildUsageMetadata(usage), - model, - nativeCallId, - outputTokens: toTokenCount(usage.outputTokens), - provider, - sessionId: input.sessionId, - sessionRunId: input.sessionRunId, - startedAt: run.started_at ?? timestampMs, - status: input.status, - totalCostUsdMicros: toUsdMicros(usage.costAmount), - traceId: input.traceId, - updatedAt: timestampMs, - }) - .onConflictDoUpdate({ - set: { - cacheCreationTokens: sql`COALESCE(excluded.cache_creation_tokens, ${sessionModelCallsTable.cacheCreationTokens})`, - cacheReadTokens: sql`COALESCE(excluded.cache_read_tokens, ${sessionModelCallsTable.cacheReadTokens})`, - completedAt: sql`COALESCE(excluded.completed_at, ${sessionModelCallsTable.completedAt})`, - costCurrency: sql`COALESCE(excluded.cost_currency, ${sessionModelCallsTable.costCurrency})`, - driverInstanceId: sql`excluded.driver_instance_id`, - inputTokens: sql`COALESCE(excluded.input_tokens, ${sessionModelCallsTable.inputTokens})`, - metadataJson: sql`excluded.metadata_json`, - model: sql`excluded.model`, - outputTokens: sql`COALESCE(excluded.output_tokens, ${sessionModelCallsTable.outputTokens})`, - provider: sql`excluded.provider`, - startedAt: sql`COALESCE(${sessionModelCallsTable.startedAt}, excluded.started_at)`, - status: sql`CASE - WHEN ${sessionModelCallsTable.status} IN ('completed', 'failed') - AND excluded.status = 'started' - THEN ${sessionModelCallsTable.status} - ELSE excluded.status - END`, - totalCostUsdMicros: sql`COALESCE(excluded.total_cost_usd_micros, ${sessionModelCallsTable.totalCostUsdMicros})`, - traceId: sql`excluded.trace_id`, - updatedAt: sql`excluded.updated_at`, - }, - target: [sessionModelCallsTable.sessionRunId, sessionModelCallsTable.callKey], - }); + const writeFence = createRuntimeUsageEventUnrolledPredicate(appDatabase, usageEventInput); + const modelCallUpsert = createSessionModelCallUsageUpsert( + appDatabase, + modelCallValues, + writeFence, + ); const usageEventUpsert = createRuntimeUsageEventUpsert(appDatabase, usageEventInput); - return usageEventUpsert === null ? [modelCallUpsert] : [modelCallUpsert, usageEventUpsert]; + return usageEventUpsert === null + ? [modelCallUpsert] + : [ + modelCallUpsert, + usageEventUpsert, + createSessionModelCallUsageConvergenceGuard( + appDatabase, + modelCallValues, + usageEventInput, + ), + ]; }); + + assertSessionModelCallConverged( + await getStoredSessionModelCall(database, { callKey, sessionRunId: input.sessionRunId }), + modelCallValues, + ); } function normalizeUsageCallId(value: string | null | undefined): string | null { diff --git a/apps/api/src/modules/sessions/infrastructure/session-runtime-event-store.repository.ts b/apps/api/src/modules/sessions/infrastructure/session-runtime-event-store.repository.ts index 86dd1707..4fffb02c 100644 --- a/apps/api/src/modules/sessions/infrastructure/session-runtime-event-store.repository.ts +++ b/apps/api/src/modules/sessions/infrastructure/session-runtime-event-store.repository.ts @@ -1,15 +1,34 @@ -import { sessionEventsTable, sessionRunsTable, sessionsTable } from "@mosoo/db"; +import { parseNullableSessionUsageSummary } from "@mosoo/ag-ui-session"; +import { + driverInstancesTable, + sessionEventsTable, + sessionRunsTable, + sessionsTable, +} from "@mosoo/db"; import { createPlatformId } from "@mosoo/id"; import type { RuntimeEventId, SessionId, SessionRunId } from "@mosoo/id"; -import { readRuntimeAgentTaskSnapshot } from "@mosoo/runtime-events"; -import { and, eq, inArray, isNull, sql } from "drizzle-orm"; +import { + createRuntimeEventSemanticHash, + readRuntimeAgentTaskSnapshot, + readRuntimeEventPayload, + readRuntimeEventString, +} from "@mosoo/runtime-events"; +import { and, eq, exists, inArray, isNull, sql } from "drizzle-orm"; import { getAppDatabase } from "../../../platform/db/drizzle"; import type { AppDatabase } from "../../../platform/db/drizzle"; import { currentTimestampMs } from "../../../time"; +import { ACTIVE_SESSION_RUN_STATUSES } from "../../runtime/domain/session-run-lifecycle.machine"; +import { readNativeResumeRef } from "../../runtime/infrastructure/driver-instance/native-resume-ref-event"; +import { prepareRuntimeArtifactPromotion } from "../../runtime/infrastructure/driver-instance/runtime-artifact-attempt.repository"; +import { prepareNativeResumeRefProjection } from "../../runtime/infrastructure/native-resume-ref.repository"; +import { prepareDurableSessionAutoTitleProjection } from "../application/session-title.service"; import { createSessionRuntimeEventProjection } from "../domain/session-runtime-event-projection"; +import { normalizeSessionTitle } from "../domain/session-title"; import { prepareSessionAgentTaskSnapshotUpsert } from "./session-agent-task-snapshot.repository"; +import { prepareDurableSessionModelCallUsageProjection } from "./session-model-call.repository"; import type { + DriverRuntimeEventFence, InsertSessionEventResult, OneRuntimeEventPerSessionAllocation, OneRuntimeEventPerSessionInput, @@ -26,7 +45,7 @@ import type { SessionRuntimeEventRecord, SessionRuntimeEventSourceReceipt, } from "./session-runtime-event-store.types"; -import { appSessionViewerRuntimeEvents } from "./session-viewer-event-projection.repository"; +import { prepareSessionViewerRuntimeEventProjection } from "./session-viewer-event-projection.repository"; export type { OneRuntimeEventPerSessionInput, @@ -38,13 +57,18 @@ export type { } from "./session-runtime-event-store.types"; const MAX_SESSION_RUNTIME_EVENT_INSERT_ATTEMPTS = 5; -// D1 accepts at most 100 bound parameters; each fenced session_event row binds 22. -const MAX_SESSION_EVENT_ROWS_PER_INSERT = 4; +// D1 accepts at most 100 bound parameters; the run-active fence adds its own binds. +const MAX_SESSION_EVENT_ROWS_PER_INSERT = 2; const WRITABLE_SESSION_STATUSES = ["IDLE", "RUNNING", "RESCHEDULING"] as const; const TERMINAL_LIFECYCLE_WRITABLE_SESSION_STATUSES = [ ...WRITABLE_SESSION_STATUSES, "TERMINATED", ] as const; +const RUN_TERMINAL_EVENT_TYPES: ReadonlySet = new Set([ + "run.cancelled", + "run.completed", + "run.failed", +]); function isRecord(value: unknown): value is Record { return typeof value === "object" && value !== null && !Array.isArray(value); @@ -58,6 +82,12 @@ function isTerminalSessionLifecycleEvent(event: SessionRuntimeEventRecord): bool ); } +function assertNoRunTerminalEvents(records: readonly { event: SessionRuntimeEventRecord }[]): void { + if (records.some(({ event }) => RUN_TERMINAL_EVENT_TYPES.has(event.kind))) { + throw new Error("Run terminal events require the atomic terminal-run projection."); + } +} + function canWriteAfterTerminatedSession( records: readonly { event: SessionRuntimeEventRecord }[], ): boolean { @@ -107,6 +137,14 @@ function isSessionRuntimeEventSeqConflict(error: unknown): boolean { ); } +function isSessionRuntimeEventBatchFenceConflict(error: unknown): boolean { + const errorText = readErrorMessageTree(error); + return ( + errorText.includes("NOT NULL constraint failed: session_event.agent_id") || + errorText.includes("NOT NULL constraint failed: session_event.session_id") + ); +} + function readRuntimeEventEndedAt(event: SessionRuntimeEventRecord, fallbackMs: number): number { const endedAt = Date.parse(event.occurredAt); return Number.isFinite(endedAt) && endedAt >= fallbackMs ? endedAt : fallbackMs; @@ -116,20 +154,20 @@ function selectedValue(value: T, alias: string) { return sql`${value}`.as(alias); } -async function allocateSessionRuntimeEventBatch( +async function readSessionRuntimeEventBatchAllocation( database: D1Database, input: { allowTerminatedSession: boolean; - count: number; sessionId: SessionId; }, ): Promise { const session = (await getAppDatabase(database) - .update(sessionsTable) - .set({ - runtimeEventSeqCursor: sql`${sessionsTable.runtimeEventSeqCursor} + ${input.count}`, + .select({ + agentId: sessionsTable.agentId, + seqCursor: sessionsTable.runtimeEventSeqCursor, }) + .from(sessionsTable) .where( and( eq(sessionsTable.id, input.sessionId), @@ -137,10 +175,6 @@ async function allocateSessionRuntimeEventBatch( inArray(sessionsTable.status, sessionWritableStatusValues(input.allowTerminatedSession)), ), ) - .returning({ - agentId: sessionsTable.agentId, - seqCursor: sessionsTable.runtimeEventSeqCursor, - }) .get()) ?? null; if (session === null) { @@ -149,7 +183,8 @@ async function allocateSessionRuntimeEventBatch( return { agentId: session.agentId, - firstSeq: session.seqCursor - input.count + 1, + firstSeq: session.seqCursor + 1, + previousCursor: session.seqCursor, }; } @@ -157,6 +192,7 @@ async function persistSessionRuntimeEventRows( database: D1Database, input: { allowTerminatedSession: boolean; + driverFence?: DriverRuntimeEventFence; rows: SerializedSessionRuntimeEventInput[]; sessionId: SessionId; }, @@ -170,38 +206,72 @@ async function persistSessionRuntimeEventRows( }; } - const projectedRows = input.rows.map((row, sourceIndex) => ({ - row: { - ...row, - projection: createSessionRuntimeEventProjection(row.event), - }, - sourceIndex, + let pendingRows = input.rows.map((row) => ({ + ...row, + projection: createSessionRuntimeEventProjection(row.event, { + provenMcpCommandId: row.provenMcpCommandId, + }), })); const timestampMs = currentTimestampMs(); for (let attempt = 0; attempt < MAX_SESSION_RUNTIME_EVENT_INSERT_ATTEMPTS; attempt += 1) { - const allocation = await allocateSessionRuntimeEventBatch(database, { + if (pendingRows.length === 0) { + return { + insertedCount: 0, + insertedRows: [], + insertedSessionIds: [], + insertedSourceEventIds: [], + }; + } + const allocation = await readSessionRuntimeEventBatchAllocation(database, { allowTerminatedSession: input.allowTerminatedSession, - count: projectedRows.length, sessionId: input.sessionId, }); + const projectedRows = pendingRows.map((row, sourceIndex) => ({ row, sourceIndex })); try { return await insertSessionRuntimeEventRows(database, { allocation, + allowTerminatedSession: input.allowTerminatedSession, + ...(input.driverFence === undefined ? {} : { driverFence: input.driverFence }), rows: projectedRows, sessionId: input.sessionId, timestampMs, }); } catch (error) { if ( - attempt < MAX_SESSION_RUNTIME_EVENT_INSERT_ATTEMPTS - 1 && - isSessionRuntimeEventSeqConflict(error) + !isSessionRuntimeEventSeqConflict(error) && + !isSessionRuntimeEventBatchFenceConflict(error) ) { + throw error; + } + const receipts = await getSessionRuntimeEventSourceReceipts(database, { + sessionId: input.sessionId, + sourceEventIds: pendingRows.map((row) => row.sourceEventId), + }); + pendingRows = pendingRows.filter((row) => { + const receipt = receipts.get(row.sourceEventId); + if (receipt === undefined) { + return true; + } + if ( + receipt.semanticHash !== row.semanticHash || + receipt.type !== row.projection.eventType + ) { + throw new Error( + `Runtime event source ${row.sourceEventId} conflicts with its durable receipt.`, + { cause: error }, + ); + } + return false; + }); + if (attempt < MAX_SESSION_RUNTIME_EVENT_INSERT_ATTEMPTS - 1) { continue; } - throw error; + throw new Error("Runtime event batch lost its atomic session or active-run fence.", { + cause: error, + }); } } @@ -336,10 +406,12 @@ async function allocateOneRuntimeEventPerSession( record === undefined ? false : canWriteAfterTerminatedSession([record]); const session = (await appDb - .update(sessionsTable) - .set({ - runtimeEventSeqCursor: sql`${sessionsTable.runtimeEventSeqCursor} + 1`, + .select({ + agentId: sessionsTable.agentId, + previousCursor: sessionsTable.runtimeEventSeqCursor, + sessionId: sessionsTable.id, }) + .from(sessionsTable) .where( and( eq(sessionsTable.id, sessionId), @@ -347,17 +419,13 @@ async function allocateOneRuntimeEventPerSession( inArray(sessionsTable.status, sessionWritableStatusValues(allowTerminatedSession)), ), ) - .returning({ - agentId: sessionsTable.agentId, - seq: sessionsTable.runtimeEventSeqCursor, - sessionId: sessionsTable.id, - }) .get()) ?? null; if (session !== null) { allocations.set(session.sessionId, { agentId: session.agentId, - seq: session.seq, + previousCursor: session.previousCursor, + seq: session.previousCursor + 1, sessionId: session.sessionId, }); } @@ -366,19 +434,26 @@ async function allocateOneRuntimeEventPerSession( return allocations; } -function toOneRuntimeEventPerSessionRows( +async function toOneRuntimeEventPerSessionRows( records: readonly OneRuntimeEventPerSessionInput[], -): OneRuntimeEventPerSessionRowInput[] { - return records.map((record) => ({ - event: record.event, - occurredAt: record.occurredAt, - projection: createSessionRuntimeEventProjection(record.event), - sessionId: record.sessionId, - sourceEventId: readSessionRuntimeEventSourceEventId({ +): Promise { + return Promise.all( + records.map(async (record) => ({ + artifactAttemptId: null, + artifactManifestJson: null, + artifactManifestSha256: null, event: record.event, - sourceEventId: null, - }), - })); + occurredAt: record.occurredAt, + projection: createSessionRuntimeEventProjection(record.event), + provenMcpCommandId: null, + semanticHash: await createRuntimeEventSemanticHash(record.event), + sessionId: record.sessionId, + sourceEventId: readSessionRuntimeEventSourceEventId({ + event: record.event, + sourceEventId: null, + }), + })), + ); } function readSessionRuntimeEventSourceEventId(input: { @@ -415,9 +490,124 @@ function toOneRuntimeEventPerSessionInsertValues(input: { }); } +async function filterNewOneRuntimeEventPerSessionRows( + database: D1Database, + rows: readonly OneRuntimeEventPerSessionRowInput[], +): Promise { + const results = await Promise.all( + rows.map(async (row) => { + const receipt = ( + await getSessionRuntimeEventSourceReceipts(database, { + sessionId: row.sessionId, + sourceEventIds: [row.sourceEventId], + }) + ).get(row.sourceEventId); + if (receipt === undefined) { + return row; + } + if (receipt.semanticHash !== row.semanticHash || receipt.type !== row.projection.eventType) { + throw new Error( + `Runtime event source ${row.sourceEventId} conflicts with its durable receipt.`, + ); + } + return null; + }), + ); + + return results.filter((row): row is OneRuntimeEventPerSessionRowInput => row !== null); +} + +async function prepareDurableRuntimeEventSideEffectProjection( + database: D1Database, + value: SessionEventInsertValue, + driverFence: DriverRuntimeEventFence | undefined, +): Promise { + if (driverFence === undefined) { + return []; + } + + if (value.event.kind === "session.info.updated") { + const title = readRuntimeEventString(readRuntimeEventPayload(value.event), "title"); + + if (title === null || title.trim().length === 0) { + return []; + } + + return prepareDurableSessionAutoTitleProjection(database, { + createdAt: value.createdAt, + eventId: value.id, + eventSeq: value.seq, + semanticHash: value.semanticHash, + sessionId: value.sessionId, + title: normalizeSessionTitle(title), + }); + } + + if (value.event.kind === "usage.updated") { + const usage = parseNullableSessionUsageSummary(value.event.payload); + + if (usage === null || driverFence.sessionRunId === null) { + return []; + } + if (value.event.driverInstanceId !== driverFence.driverInstanceId) { + throw new Error("Durable usage event is missing its exact Driver identity."); + } + if (value.runId !== driverFence.sessionRunId) { + throw new Error("Durable usage event is missing its exact Session Run identity."); + } + + return prepareDurableSessionModelCallUsageProjection(database, { + createdAtMs: value.createdAt, + driverInstanceId: driverFence.driverInstanceId, + eventId: value.id, + semanticHash: value.semanticHash, + sessionId: value.sessionId, + sessionRunId: driverFence.sessionRunId, + sourceEventSeq: value.seq, + traceId: value.traceId ?? driverFence.sessionRunId, + usage, + }); + } + + if (value.event.kind !== "runtime.resume.updated") { + return []; + } + + const nativeResumeRef = readNativeResumeRef(value.event); + + if (nativeResumeRef === null || driverFence.sessionRunId === null) { + return []; + } + if (value.event.driverInstanceId !== driverFence.driverInstanceId) { + throw new Error("Durable native resume event is missing its exact Driver identity."); + } + if (value.runId !== driverFence.sessionRunId) { + throw new Error("Durable native resume event is missing its exact Session Run identity."); + } + + return prepareNativeResumeRefProjection(database, { + createdAt: value.createdAt, + driverInstanceId: driverFence.driverInstanceId, + eventId: value.id, + nativeResumeRef, + observedEventSeq: value.seq, + semanticHash: value.semanticHash, + sessionId: value.sessionId, + sessionRunId: driverFence.sessionRunId, + }); +} + async function insertSessionEventRows( database: D1Database, values: readonly SessionEventInsertValue[], + atomicAllocations?: ReadonlyMap< + SessionId, + { + allowTerminatedSession: boolean; + driverFence?: DriverRuntimeEventFence; + previousCursor: number; + } + >, ): Promise { if (values.length === 0) { return { @@ -431,19 +621,56 @@ async function insertSessionEventRows( const appDatabase = getAppDatabase(database); const statements: D1PreparedStatement[] = []; const receiptStatementIndexes: number[] = []; + const valuesBySession = Map.groupBy(values, (value) => value.sessionId); + const nextCursorBySession = new Map(); - for (let index = 0; index < values.length; index += MAX_SESSION_EVENT_ROWS_PER_INSERT) { - const chunk = values.slice(index, index + MAX_SESSION_EVENT_ROWS_PER_INSERT); + for (const [sessionId, allocation] of atomicAllocations ?? []) { + const sessionValues = valuesBySession.get(sessionId) ?? []; + if (sessionValues.length === 0) { + continue; + } + const nextCursor = allocation.previousCursor + sessionValues.length; + nextCursorBySession.set(sessionId, nextCursor); + const writableStatuses = sessionWritableStatusValues(allocation.allowTerminatedSession); + statements.push( + database + .prepare( + `UPDATE session + SET runtime_event_seq_cursor = ? + WHERE id = ? + AND runtime_event_seq_cursor = ? + AND archived_at IS NULL + AND status IN (${writableStatuses.map(() => "?").join(", ")})`, + ) + .bind(nextCursor, sessionId, allocation.previousCursor, ...writableStatuses), + ); + } + + const insertSize = atomicAllocations === undefined ? MAX_SESSION_EVENT_ROWS_PER_INSERT : 1; + for (let index = 0; index < values.length; index += insertSize) { + const chunk = values.slice(index, index + insertSize); const firstValue = chunk[0]; if (firstValue === undefined) { continue; } - const selection = createSessionEventInsertSelect(appDatabase, firstValue); + const selection = createSessionEventInsertSelect( + appDatabase, + firstValue, + nextCursorBySession.get(firstValue.sessionId) ?? null, + atomicAllocations?.get(firstValue.sessionId)?.driverFence, + ); for (const value of chunk.slice(1)) { - selection.unionAll(createSessionEventInsertSelect(appDatabase, value)); + selection.unionAll( + createSessionEventInsertSelect( + appDatabase, + value, + nextCursorBySession.get(value.sessionId) ?? null, + atomicAllocations?.get(value.sessionId)?.driverFence, + ), + ); } const query = appDatabase @@ -470,9 +697,69 @@ async function insertSessionEventRows( }), ); } + statements.push( + ...prepareSessionViewerRuntimeEventProjection(database, { + createdAt: value.createdAt, + event: value.event, + eventId: value.id, + sessionId: value.sessionId, + }), + ); + statements.push( + ...(await prepareDurableRuntimeEventSideEffectProjection( + database, + value, + atomicAllocations?.get(value.sessionId)?.driverFence, + )), + ); + if ( + value.artifactAttemptId !== null && + value.artifactManifestJson !== null && + value.artifactManifestSha256 !== null + ) { + statements.push( + ...prepareRuntimeArtifactPromotion(database, { + attemptId: value.artifactAttemptId, + eventId: value.id, + manifestJson: value.artifactManifestJson, + manifestSha256: value.artifactManifestSha256, + timestampMs: value.createdAt, + }), + ); + } } } + for (const [sessionId, nextCursor] of nextCursorBySession) { + const sessionValues = valuesBySession.get(sessionId) ?? []; + statements.push( + database + .prepare( + `INSERT INTO session_event (id) + SELECT ? + WHERE NOT EXISTS ( + SELECT 1 + FROM session AS s + WHERE s.id = ? + AND s.runtime_event_seq_cursor = ? + AND ( + SELECT COUNT(*) + FROM session_event AS e + WHERE e.session_id = s.id + AND e.id IN (SELECT value FROM json_each(?)) + ) = ? + )`, + ) + .bind( + createPlatformId(), + sessionId, + nextCursor, + JSON.stringify(sessionValues.map((value) => value.id)), + sessionValues.length, + ), + ); + } + const results = await database.batch<{ session_id: SessionId; source_event_id: string }>( statements, ); @@ -484,6 +771,42 @@ async function insertSessionEventRows( sourceEventId: row.source_event_id, })); }); + const insertedKeys = new Set(insertedRows.map(sessionSourceEventKey)); + const replayCandidates = values.filter( + (value) => + !insertedKeys.has( + sessionSourceEventKey({ + sessionId: value.sessionId, + sourceEventId: value.sourceEventId, + }), + ), + ); + const replayCandidatesBySession = new Map(); + for (const candidate of replayCandidates) { + replayCandidatesBySession.set(candidate.sessionId, [ + ...(replayCandidatesBySession.get(candidate.sessionId) ?? []), + candidate, + ]); + } + + for (const [sessionId, candidates] of replayCandidatesBySession) { + const receipts = await getSessionRuntimeEventSourceReceipts(database, { + sessionId, + sourceEventIds: candidates.map((candidate) => candidate.sourceEventId), + }); + for (const candidate of candidates) { + const receipt = receipts.get(candidate.sourceEventId); + if ( + receipt === undefined || + receipt.semanticHash !== candidate.semanticHash || + receipt.type !== candidate.eventType + ) { + throw new Error( + `Runtime event source ${candidate.sourceEventId} conflicts with its durable receipt.`, + ); + } + } + } return { insertedCount: insertedRows.length, @@ -508,43 +831,37 @@ export async function persistOneRuntimeEventPerSession( assertUniqueRuntimeEventSessions(input.records); assertOneRuntimeEventPerSessionMatches(input.records); + assertNoRunTerminalEvents(input.records); await ensureRuntimeEventRunsMatchSessions(database, readRuntimeEventRunScopes(input.records)); - const rows = toOneRuntimeEventPerSessionRows(input.records); + const rows = await toOneRuntimeEventPerSessionRows(input.records); + let pendingRows = await filterNewOneRuntimeEventPerSessionRows(database, rows); const timestampMs = currentTimestampMs(); for (let attempt = 0; attempt < MAX_SESSION_RUNTIME_EVENT_INSERT_ATTEMPTS; attempt += 1) { - const allocations = await allocateOneRuntimeEventPerSession(database, input.records); + const allocations = await allocateOneRuntimeEventPerSession(database, pendingRows); const values = toOneRuntimeEventPerSessionInsertValues({ allocations, - rows, + rows: pendingRows, timestampMs, }); + const atomicAllocations = new Map( + [...allocations].map(([sessionId, allocation]) => [ + sessionId, + { + allowTerminatedSession: + pendingRows.find((row) => row.sessionId === sessionId) !== undefined && + canWriteAfterTerminatedSession( + pendingRows.filter((row) => row.sessionId === sessionId), + ), + previousCursor: allocation.previousCursor, + }, + ]), + ); try { - const insertResult = await insertSessionEventRows(database, values); + const insertResult = await insertSessionEventRows(database, values, atomicAllocations); const insertedSessionIds = new Set(insertResult.insertedSessionIds); - const insertedKeys = new Set(insertResult.insertedRows.map(sessionSourceEventKey)); - - await appSessionViewerRuntimeEvents( - database, - rows.flatMap((row) => - insertedKeys.has( - sessionSourceEventKey({ - sessionId: row.sessionId, - sourceEventId: row.sourceEventId, - }), - ) - ? [ - { - event: row.event, - occurredAt: row.occurredAt, - sessionId: row.sessionId, - }, - ] - : [], - ), - ); return { persistedCount: insertResult.insertedCount, @@ -556,13 +873,19 @@ export async function persistOneRuntimeEventPerSession( }; } catch (error) { if ( - attempt < MAX_SESSION_RUNTIME_EVENT_INSERT_ATTEMPTS - 1 && - isSessionRuntimeEventSeqConflict(error) + !isSessionRuntimeEventSeqConflict(error) && + !isSessionRuntimeEventBatchFenceConflict(error) ) { + throw error; + } + pendingRows = await filterNewOneRuntimeEventPerSessionRows(database, pendingRows); + if (attempt < MAX_SESSION_RUNTIME_EVENT_INSERT_ATTEMPTS - 1) { continue; } - throw error; + throw new Error("Runtime event batch lost its atomic session or active-run fence.", { + cause: error, + }); } } @@ -573,7 +896,7 @@ export async function persistOneRuntimeEventPerSession( } function toSessionRuntimeEventInsertValue(input: { - allocation: SessionRuntimeEventBatchAllocation; + allocation: Pick; row: ProjectedSessionRuntimeEventInput; sessionId: SessionId; sourceIndex: number; @@ -588,56 +911,155 @@ function toSessionRuntimeEventInsertValue(input: { ? readRuntimeAgentTaskSnapshot(input.row.event) : null, agentId: input.allocation.agentId, + artifactAttemptId: input.row.artifactAttemptId, + artifactManifestJson: input.row.artifactManifestJson, + artifactManifestSha256: input.row.artifactManifestSha256, contentText: input.row.projection.contentText, createdAt: input.timestampMs + input.sourceIndex, endedAt: readRuntimeEventEndedAt(input.row.event, occurredAt), + event: input.row.event, eventType: input.row.projection.eventType, family: input.row.projection.family, id, + mcpCommandId: input.row.projection.mcpCommandId, occurredAt, processStatus: input.row.projection.processStatus, processType: input.row.projection.processType, runId: input.row.projection.runId, + runtimeOperationEventJson: null, + semanticHash: input.row.semanticHash, + terminalEventJson: null, seq: input.allocation.firstSeq + input.sourceIndex, sessionId: input.sessionId, sourceEventId: input.row.sourceEventId, source: input.row.projection.source, + streamId: input.row.projection.streamId, toolCallId: input.row.projection.toolCallId, + toolInputDeltaJson: input.row.projection.toolInputDeltaJson, toolInputJson: input.row.projection.toolInputJson, toolName: input.row.projection.toolName, + toolOutputDeltaText: input.row.projection.toolOutputDeltaText, + toolOutputText: input.row.projection.toolOutputText, + toolParentMessageId: input.row.projection.toolParentMessageId, + toolResultMessageId: input.row.projection.toolResultMessageId, + toolStatus: input.row.projection.toolStatus, tokens: input.row.projection.tokens, traceId: input.row.projection.traceId, visibility: input.row.projection.visibility, }; } -function createSessionEventInsertSelect(database: AppDatabase, value: SessionEventInsertValue) { +function createSessionEventInsertSelect( + database: AppDatabase, + value: SessionEventInsertValue, + requiredCursor: number | null = null, + driverFence?: DriverRuntimeEventFence, +) { return database .select({ agentId: selectedValue(value.agentId, "agent_id"), + artifactAttemptId: selectedValue(value.artifactAttemptId, "artifact_attempt_id"), + artifactManifestJson: selectedValue(value.artifactManifestJson, "artifact_manifest_json"), + artifactManifestSha256: selectedValue( + value.artifactManifestSha256, + "artifact_manifest_sha256", + ), contentText: selectedValue(value.contentText, "content_text"), createdAt: selectedValue(value.createdAt, "created_at"), endedAt: selectedValue(value.endedAt, "ended_at"), eventType: selectedValue(value.eventType, "event_type"), family: selectedValue(value.family, "family"), id: selectedValue(value.id, "id"), + mcpCommandId: selectedValue(value.mcpCommandId, "mcp_command_id"), occurredAt: selectedValue(value.occurredAt, "occurred_at"), processStatus: selectedValue(value.processStatus, "process_status"), processType: selectedValue(value.processType, "process_type"), runId: selectedValue(value.runId, "run_id"), + runtimeOperationEventJson: selectedValue( + value.runtimeOperationEventJson, + "runtime_operation_event_json", + ), + semanticHash: selectedValue(value.semanticHash, "semantic_hash"), seq: selectedValue(value.seq, "seq"), sessionId: selectedValue(value.sessionId, "session_id"), sourceEventId: selectedValue(value.sourceEventId, "source_event_id"), source: selectedValue(value.source, "source"), + streamId: selectedValue(value.streamId, "stream_id"), + terminalEventJson: selectedValue(value.terminalEventJson, "terminal_event_json"), toolCallId: selectedValue(value.toolCallId, "tool_call_id"), + toolInputDeltaJson: selectedValue(value.toolInputDeltaJson, "tool_input_delta_json"), toolInputJson: selectedValue(value.toolInputJson, "tool_input_json"), toolName: selectedValue(value.toolName, "tool_name"), + toolOutputDeltaText: selectedValue(value.toolOutputDeltaText, "tool_output_delta_text"), + toolOutputText: selectedValue(value.toolOutputText, "tool_output_text"), + toolParentMessageId: selectedValue(value.toolParentMessageId, "tool_parent_message_id"), + toolResultMessageId: selectedValue(value.toolResultMessageId, "tool_result_message_id"), + toolStatus: selectedValue(value.toolStatus, "tool_status"), tokens: selectedValue(value.tokens, "tokens"), traceId: selectedValue(value.traceId, "trace_id"), visibility: selectedValue(value.visibility, "visibility"), }) .from(sessionsTable) - .where(and(eq(sessionsTable.id, value.sessionId), isNull(sessionsTable.archivedAt))) + .where( + and( + eq(sessionsTable.id, value.sessionId), + isNull(sessionsTable.archivedAt), + requiredCursor === null + ? undefined + : eq(sessionsTable.runtimeEventSeqCursor, requiredCursor), + driverFence === undefined + ? undefined + : and( + exists( + database + .select({ id: driverInstancesTable.id }) + .from(driverInstancesTable) + .where( + and( + eq(driverInstancesTable.id, driverFence.driverInstanceId), + eq(driverInstancesTable.connectionId, driverFence.connectionId), + eq(driverInstancesTable.generation, driverFence.generation), + eq(driverInstancesTable.sandboxSessionId, value.sessionId), + ), + ), + ), + driverFence.sessionRunId === null + ? undefined + : and( + eq(sessionsTable.lastRunId, driverFence.sessionRunId), + eq(sessionsTable.status, "RUNNING"), + isNull(sessionsTable.statusOperationId), + exists( + database + .select({ id: sessionRunsTable.id }) + .from(sessionRunsTable) + .where( + and( + eq(sessionRunsTable.id, driverFence.sessionRunId), + eq(sessionRunsTable.sessionId, value.sessionId), + eq(sessionRunsTable.driverInstanceId, driverFence.driverInstanceId), + inArray(sessionRunsTable.status, ACTIVE_SESSION_RUN_STATUSES), + ), + ), + ), + ), + ), + value.runId === null + ? undefined + : exists( + database + .select({ id: sessionRunsTable.id }) + .from(sessionRunsTable) + .where( + and( + eq(sessionRunsTable.id, value.runId), + eq(sessionRunsTable.sessionId, value.sessionId), + inArray(sessionRunsTable.status, ACTIVE_SESSION_RUN_STATUSES), + ), + ), + ), + ), + ) .$dynamic(); } @@ -662,18 +1084,33 @@ async function insertSessionRuntimeEventRows( database: D1Database, input: { allocation: SessionRuntimeEventBatchAllocation; + allowTerminatedSession: boolean; + driverFence?: DriverRuntimeEventFence; rows: ProjectedSessionRuntimeEventRowInput[]; sessionId: SessionId; timestampMs: number; }, ): Promise { - return insertSessionEventRows(database, toSessionRuntimeEventInsertValues(input)); + return insertSessionEventRows( + database, + toSessionRuntimeEventInsertValues(input), + new Map([ + [ + input.sessionId, + { + allowTerminatedSession: input.allowTerminatedSession, + ...(input.driverFence === undefined ? {} : { driverFence: input.driverFence }), + previousCursor: input.allocation.previousCursor, + }, + ], + ]), + ); } async function filterNewSessionRuntimeEventInputs( database: D1Database, input: PersistSessionRuntimeEventsInput, -): Promise { +): Promise<(SessionRuntimeEventInput & { semanticHash: string })[]> { const sourceEventIds = input.records.map((record) => readSessionRuntimeEventSourceEventId({ event: record.event, @@ -685,19 +1122,34 @@ async function filterNewSessionRuntimeEventInputs( sessionId: input.sessionId, sourceEventIds, }); - const acceptedSourceIds = new Set(); + const acceptedHashes = new Map(); + const records = await Promise.all( + input.records.map(async (record) => ({ + ...record, + semanticHash: await createRuntimeEventSemanticHash(record.event), + })), + ); - return input.records.filter((record) => { + return records.filter((record) => { const sourceEventId = readSessionRuntimeEventSourceEventId({ event: record.event, sourceEventId: record.sourceEventId, }); + const receipt = persistedReceipts.get(sourceEventId); + const acceptedHash = acceptedHashes.get(sourceEventId); + + if ( + (receipt !== undefined && receipt.semanticHash !== record.semanticHash) || + (acceptedHash !== undefined && acceptedHash !== record.semanticHash) + ) { + throw new Error(`Runtime event source ${sourceEventId} conflicts with its durable receipt.`); + } - if (persistedReceipts.has(sourceEventId) || acceptedSourceIds.has(sourceEventId)) { + if (receipt !== undefined || acceptedHash !== undefined) { return false; } - acceptedSourceIds.add(sourceEventId); + acceptedHashes.set(sourceEventId, record.semanticHash); return true; }); } @@ -707,6 +1159,16 @@ export async function persistSessionRuntimeEvents( input: PersistSessionRuntimeEventsInput, ): Promise { assertRuntimeEventBatchSessionMatches(input); + assertNoRunTerminalEvents(input.records); + if ( + input.driverFence !== undefined && + input.records.some( + (record) => + record.event.runId !== undefined && record.event.runId !== input.driverFence?.sessionRunId, + ) + ) { + throw new Error("Driver runtime event does not match its fenced Session Run."); + } await ensureRuntimeEventRunsMatchSessions( database, readRuntimeEventRunScopes( @@ -727,36 +1189,29 @@ export async function persistSessionRuntimeEvents( }; } - const rows = records.map((record) => ({ - event: record.event, - occurredAt: record.occurredAt, - sourceEventId: readSessionRuntimeEventSourceEventId({ + const rows = await Promise.all( + records.map(async (record) => ({ + artifactAttemptId: record.artifactAttemptId ?? null, + artifactManifestJson: record.artifactManifestJson ?? null, + artifactManifestSha256: record.artifactManifestSha256 ?? null, event: record.event, - sourceEventId: record.sourceEventId, - }), - })); + occurredAt: record.occurredAt, + provenMcpCommandId: record.provenMcpCommandId ?? null, + semanticHash: record.semanticHash, + sourceEventId: readSessionRuntimeEventSourceEventId({ + event: record.event, + sourceEventId: record.sourceEventId, + }), + })), + ); const result = await persistSessionRuntimeEventRows(database, { allowTerminatedSession: canWriteAfterTerminatedSession(records), + ...(input.driverFence === undefined ? {} : { driverFence: input.driverFence }), rows, sessionId: input.sessionId, }); const insertedSourceEventIds = new Set(result.insertedSourceEventIds); - await appSessionViewerRuntimeEvents( - database, - rows.flatMap((row) => - insertedSourceEventIds.has(row.sourceEventId) - ? [ - { - event: row.event, - occurredAt: row.occurredAt, - sessionId: input.sessionId, - }, - ] - : [], - ), - ); - return { persistedCount: result.insertedCount, persistedEvents: rows @@ -782,6 +1237,7 @@ export async function getSessionRuntimeEventSourceReceipts( const rows = await getAppDatabase(database) .select({ event_id: sessionEventsTable.sourceEventId, + semantic_hash: sessionEventsTable.semanticHash, seq: sessionEventsTable.seq, type: sessionEventsTable.eventType, }) @@ -799,6 +1255,7 @@ export async function getSessionRuntimeEventSourceReceipts( for (const row of rows) { receipts.set(row.event_id, { eventId: row.event_id, + semanticHash: row.semantic_hash, seq: row.seq, type: row.type, }); diff --git a/apps/api/src/modules/sessions/infrastructure/session-runtime-event-store.types.ts b/apps/api/src/modules/sessions/infrastructure/session-runtime-event-store.types.ts index 7e37637b..f65f78a1 100644 --- a/apps/api/src/modules/sessions/infrastructure/session-runtime-event-store.types.ts +++ b/apps/api/src/modules/sessions/infrastructure/session-runtime-event-store.types.ts @@ -1,5 +1,12 @@ import type { AgentTaskSnapshot } from "@mosoo/contracts/session"; -import type { AgentId, RuntimeEventId, SessionId, SessionRunId } from "@mosoo/id"; +import type { + AgentId, + DriverCommandId, + DriverInstanceId, + RuntimeEventId, + SessionId, + SessionRunId, +} from "@mosoo/id"; import type { RuntimeEventEnvelope } from "@mosoo/runtime-events"; import type { createSessionRuntimeEventProjection } from "../domain/session-runtime-event-projection"; @@ -7,10 +14,18 @@ import type { createSessionRuntimeEventProjection } from "../domain/session-runt type SessionRuntimeEventProjection = ReturnType; export interface PersistSessionRuntimeEventsInput { + readonly driverFence?: DriverRuntimeEventFence; records: readonly SessionRuntimeEventInput[]; readonly sessionId: SessionId; } +export interface DriverRuntimeEventFence { + readonly connectionId: string; + readonly driverInstanceId: DriverInstanceId; + readonly generation: number; + readonly sessionRunId: SessionRunId | null; +} + export interface PersistSessionRuntimeEventsResult { readonly persistedCount: number; readonly persistedEvents: readonly SessionRuntimeEventRecord[]; @@ -18,16 +33,25 @@ export interface PersistSessionRuntimeEventsResult { } export interface SessionRuntimeEventInput { + readonly artifactAttemptId?: string | null; + readonly artifactManifestJson?: string | null; + readonly artifactManifestSha256?: string | null; readonly event: SessionRuntimeEventRecord; readonly occurredAt: number | null; + readonly provenMcpCommandId?: DriverCommandId | null; readonly sourceEventId: string | null; } export type SessionRuntimeEventRecord = RuntimeEventEnvelope; export interface SerializedSessionRuntimeEventInput { + readonly artifactAttemptId: string | null; + readonly artifactManifestJson: string | null; + readonly artifactManifestSha256: string | null; readonly event: SessionRuntimeEventRecord; readonly occurredAt: number | null; + readonly provenMcpCommandId: DriverCommandId | null; + readonly semanticHash: string; readonly sourceEventId: string; } @@ -43,10 +67,12 @@ export interface ProjectedSessionRuntimeEventRowInput { export interface SessionRuntimeEventBatchAllocation { readonly agentId: AgentId; readonly firstSeq: number; + readonly previousCursor: number; } export interface OneRuntimeEventPerSessionAllocation { readonly agentId: AgentId; + readonly previousCursor: number; readonly seq: number; readonly sessionId: SessionId; } @@ -69,6 +95,7 @@ export interface PersistOneRuntimeEventPerSessionResult { export interface SessionRuntimeEventSourceReceipt { readonly eventId: string; + readonly semanticHash: string | null; readonly seq: number; readonly type: string; } @@ -76,23 +103,38 @@ export interface SessionRuntimeEventSourceReceipt { export interface SessionEventInsertValue { readonly agentTaskSnapshot: AgentTaskSnapshot | null; readonly agentId: AgentId; + readonly artifactAttemptId: string | null; + readonly artifactManifestJson: string | null; + readonly artifactManifestSha256: string | null; readonly contentText: string; readonly createdAt: number; readonly endedAt: number; + readonly event: SessionRuntimeEventRecord; readonly eventType: string; readonly family: SessionRuntimeEventProjection["family"]; readonly id: RuntimeEventId; + readonly mcpCommandId: DriverCommandId | null; readonly occurredAt: number; readonly processStatus: SessionRuntimeEventProjection["processStatus"]; readonly processType: SessionRuntimeEventProjection["processType"]; readonly runId: SessionRunId | null; + readonly runtimeOperationEventJson: string | null; + readonly semanticHash: string; + readonly terminalEventJson: string | null; readonly seq: number; readonly sessionId: SessionId; readonly source: SessionRuntimeEventProjection["source"]; readonly sourceEventId: string; + readonly streamId: string | null; readonly toolCallId: string | null; + readonly toolInputDeltaJson: string | null; readonly toolInputJson: string | null; readonly toolName: string | null; + readonly toolOutputDeltaText: string | null; + readonly toolOutputText: string | null; + readonly toolParentMessageId: string | null; + readonly toolResultMessageId: string | null; + readonly toolStatus: SessionRuntimeEventProjection["toolStatus"]; readonly tokens: number | null; readonly traceId: string | null; readonly visibility: SessionRuntimeEventProjection["visibility"]; diff --git a/apps/api/src/modules/sessions/infrastructure/session-viewer-event-projection.repository.ts b/apps/api/src/modules/sessions/infrastructure/session-viewer-event-projection.repository.ts index c18521cb..1750b3dd 100644 --- a/apps/api/src/modules/sessions/infrastructure/session-viewer-event-projection.repository.ts +++ b/apps/api/src/modules/sessions/infrastructure/session-viewer-event-projection.repository.ts @@ -2,246 +2,331 @@ import type { SessionPermissionRequestView, SessionReadinessSnapshotView, } from "@mosoo/ag-ui-session"; -import { - SessionPermissionRequestViewSchema, - SessionReadinessSnapshotViewSchema, -} from "@mosoo/ag-ui-session"; +import { SessionReadinessSnapshotViewSchema } from "@mosoo/ag-ui-session"; import { parseSchemaValue } from "@mosoo/contracts/validation"; -import { sessionPermissionRequestsTable, sessionReadinessSnapshotsTable } from "@mosoo/db"; -import type { DriverInstanceId, SessionId, SessionRunId } from "@mosoo/id"; +import { createPlatformId } from "@mosoo/id"; +import type { RuntimeEventId, SessionId, SessionRunId } from "@mosoo/id"; import { readRuntimeEventPayload, readRuntimeEventPermissionRequest, + readRuntimeRunPayload, readRuntimeEventString, } from "@mosoo/runtime-events"; import type { RuntimeEventEnvelope } from "@mosoo/runtime-events"; -import { and, eq } from "drizzle-orm"; - -import { getAppDatabase } from "../../../platform/db/drizzle"; -import { currentTimestampMs } from "../../../time"; export interface SessionViewerProjectionRuntimeEvent { + readonly createdAt: number; readonly event: RuntimeEventEnvelope; - readonly occurredAt: number | null; + readonly eventId: RuntimeEventId; readonly sessionId: SessionId; } -function toProjectionTimestamp(record: SessionViewerProjectionRuntimeEvent): number { - if (record.occurredAt !== null) { - return record.occurredAt; - } +const INSERTED_EVENT_FENCE = `EXISTS ( + SELECT 1 + FROM session_event AS receipt + WHERE receipt.id = ? + AND receipt.session_id = ? + AND receipt.event_type = ? +)`; - const occurredAt = Date.parse(record.event.occurredAt); - return Number.isFinite(occurredAt) ? occurredAt : currentTimestampMs(); -} - -function parsePermissionRequestViews(value: unknown): SessionPermissionRequestView[] | null { - if (!Array.isArray(value)) { - return null; - } - - const requests: SessionPermissionRequestView[] = []; - - for (const entry of value) { - requests.push(parseSchemaValue(SessionPermissionRequestViewSchema, entry)); - } - - return requests; +function preparePermissionRequestUpsert( + database: D1Database, + record: SessionViewerProjectionRuntimeEvent, + request: SessionPermissionRequestView, +): D1PreparedStatement { + return database + .prepare( + `INSERT INTO session_permission_request ( + created_at, driver_instance_id, raw_input, request_id, run_id, + session_id, title, tool_call_id, tool_kind, updated_at + ) + SELECT ?, ?, ?, ?, ?, ?, ?, ?, ?, ? + WHERE ${INSERTED_EVENT_FENCE} + ON CONFLICT (session_id, request_id) DO UPDATE SET + driver_instance_id = excluded.driver_instance_id, + raw_input = excluded.raw_input, + run_id = excluded.run_id, + title = excluded.title, + tool_call_id = excluded.tool_call_id, + tool_kind = excluded.tool_kind, + updated_at = excluded.updated_at`, + ) + .bind( + record.createdAt, + request.driverInstanceId, + request.rawInput, + request.requestId, + request.runId, + record.sessionId, + request.title, + request.toolCallId, + request.toolKind, + record.createdAt, + record.eventId, + record.sessionId, + record.event.kind, + ); } -async function upsertPermissionRequest( +function preparePermissionRequestDelete( database: D1Database, record: SessionViewerProjectionRuntimeEvent, -): Promise { - const request = readRuntimeEventPermissionRequest(record.event); + runId: SessionRunId, + requestId: string, +): D1PreparedStatement { + return database + .prepare( + `DELETE FROM session_permission_request + WHERE session_id = ? + AND run_id = ? + AND request_id = ? + AND ${INSERTED_EVENT_FENCE}`, + ) + .bind(record.sessionId, runId, requestId, record.eventId, record.sessionId, record.event.kind); +} - if (request === null) { - return; +function requirePermissionRunId(record: SessionViewerProjectionRuntimeEvent): SessionRunId { + if (record.event.runId === undefined) { + throw new Error(`Runtime event ${record.event.kind} requires an exact Session Run identity.`); } - - const timestamp = toProjectionTimestamp(record); - - await getAppDatabase(database) - .insert(sessionPermissionRequestsTable) - .values({ - createdAt: timestamp, - driverInstanceId: request.driverInstanceId, - rawInput: request.rawInput, - requestId: request.requestId, - runId: request.runId, - sessionId: record.sessionId, - title: request.title, - toolCallId: request.toolCallId, - toolKind: request.toolKind, - updatedAt: timestamp, - }) - .onConflictDoUpdate({ - set: { - driverInstanceId: request.driverInstanceId, - rawInput: request.rawInput, - runId: request.runId, - title: request.title, - toolCallId: request.toolCallId, - toolKind: request.toolKind, - updatedAt: timestamp, - }, - target: [sessionPermissionRequestsTable.sessionId, sessionPermissionRequestsTable.requestId], - }) - .run(); + return record.event.runId; } -async function replacePermissionRequests( +function preparePermissionRunStatusUpdate( database: D1Database, record: SessionViewerProjectionRuntimeEvent, - permissionRequests: readonly SessionPermissionRequestView[], -): Promise { - const timestamp = toProjectionTimestamp(record); - const db = getAppDatabase(database); - - await db - .delete(sessionPermissionRequestsTable) - .where(eq(sessionPermissionRequestsTable.sessionId, record.sessionId)) - .run(); - - if (permissionRequests.length === 0) { - return; - } - - await db - .insert(sessionPermissionRequestsTable) - .values( - permissionRequests.map((request) => ({ - createdAt: timestamp, - driverInstanceId: request.driverInstanceId as DriverInstanceId, - rawInput: request.rawInput, - requestId: request.requestId, - runId: request.runId as SessionRunId, - sessionId: record.sessionId, - title: request.title, - toolCallId: request.toolCallId, - toolKind: request.toolKind, - updatedAt: timestamp, - })), + runId: SessionRunId, +): D1PreparedStatement { + return database + .prepare( + `WITH desired AS ( + SELECT CASE WHEN EXISTS ( + SELECT 1 FROM session_permission_request AS permission + WHERE permission.session_id = ? AND permission.run_id = ? + ) THEN 'waiting_input' ELSE 'running' END AS next_status + ) + UPDATE session_run + SET status = (SELECT next_status FROM desired), + status_changed_at = CASE + WHEN status <> (SELECT next_status FROM desired) THEN ? ELSE status_changed_at END, + status_event = CASE + WHEN status <> (SELECT next_status FROM desired) + THEN CASE (SELECT next_status FROM desired) + WHEN 'waiting_input' THEN 'run.wait_for_input' ELSE 'run.start' END + ELSE status_event END, + status_operation_id = CASE + WHEN status <> (SELECT next_status FROM desired) THEN NULL ELSE status_operation_id END, + status_seq = status_seq + CASE + WHEN status <> (SELECT next_status FROM desired) THEN 1 ELSE 0 END, + status_source = CASE + WHEN status <> (SELECT next_status FROM desired) THEN ? ELSE status_source END, + updated_at = CASE + WHEN status <> (SELECT next_status FROM desired) THEN ? ELSE updated_at END + WHERE id = ? + AND session_id = ? + AND status IN ('running', 'waiting_input') + AND ${INSERTED_EVENT_FENCE}`, ) - .run(); + .bind( + record.sessionId, + runId, + record.createdAt, + record.event.origin, + record.createdAt, + runId, + record.sessionId, + record.eventId, + record.sessionId, + record.event.kind, + ); } -async function removePermissionRequestById( +function preparePermissionProjectionGuard( database: D1Database, - input: { - readonly requestId: string; - readonly sessionId: SessionId; - }, -): Promise { - await getAppDatabase(database) - .delete(sessionPermissionRequestsTable) - .where( - and( - eq(sessionPermissionRequestsTable.sessionId, input.sessionId), - eq(sessionPermissionRequestsTable.requestId, input.requestId), - ), + record: SessionViewerProjectionRuntimeEvent, + runId: SessionRunId, +): D1PreparedStatement { + return database + .prepare( + `INSERT INTO session_event (id) + SELECT ? + WHERE NOT EXISTS ( + SELECT 1 + FROM session_event AS receipt + INNER JOIN session_run AS run + ON run.id = ? AND run.session_id = receipt.session_id + WHERE receipt.id = ? + AND receipt.session_id = ? + AND receipt.event_type = ? + AND run.status = CASE WHEN EXISTS ( + SELECT 1 FROM session_permission_request AS permission + WHERE permission.session_id = receipt.session_id AND permission.run_id = run.id + ) THEN 'waiting_input' ELSE 'running' END + )`, ) - .run(); + .bind( + createPlatformId(), + runId, + record.eventId, + record.sessionId, + record.event.kind, + ); } -async function clearRunPermissionRequests( +function preparePermissionRequested( database: D1Database, record: SessionViewerProjectionRuntimeEvent, -): Promise { - const runId = record.event.runId; - - if (runId === undefined) { - return; +): D1PreparedStatement[] { + const request = readRuntimeEventPermissionRequest(record.event); + if (request === null) { + return []; } - - await getAppDatabase(database) - .delete(sessionPermissionRequestsTable) - .where( - and( - eq(sessionPermissionRequestsTable.sessionId, record.sessionId), - eq(sessionPermissionRequestsTable.runId, runId), - ), - ) - .run(); + const runId = requirePermissionRunId(record); + if (request.runId !== runId) { + throw new Error("Permission request payload conflicts with its event Run identity."); + } + return [ + preparePermissionRequestUpsert(database, record, request), + preparePermissionRunStatusUpdate(database, record, runId), + preparePermissionProjectionGuard(database, record, runId), + ]; } -async function appPermissionResolution( +function preparePermissionResolved( database: D1Database, record: SessionViewerProjectionRuntimeEvent, -): Promise { +): D1PreparedStatement[] { const payload = readRuntimeEventPayload(record.event); - const permissionRequests = parsePermissionRequestViews(payload["permissionRequests"]); - - if (permissionRequests !== null) { - await replacePermissionRequests(database, record, permissionRequests); - return; - } - const requestId = readRuntimeEventString(payload, "requestId"); - - if (requestId !== null) { - await removePermissionRequestById(database, { - requestId, - sessionId: record.sessionId, - }); + if (requestId === null) { + return []; } + const runId = requirePermissionRunId(record); + return [ + preparePermissionRequestDelete(database, record, runId, requestId), + preparePermissionRunStatusUpdate(database, record, runId), + preparePermissionProjectionGuard(database, record, runId), + ]; } -async function upsertReadinessSnapshot( +function prepareReadinessUpsert( database: D1Database, record: SessionViewerProjectionRuntimeEvent, -): Promise { +): D1PreparedStatement { const readiness = parseSchemaValue( SessionReadinessSnapshotViewSchema, record.event.payload, ) satisfies SessionReadinessSnapshotView; - const timestamp = toProjectionTimestamp(record); - const readinessJson = JSON.stringify(readiness); - await getAppDatabase(database) - .insert(sessionReadinessSnapshotsTable) - .values({ - readinessJson, - sessionId: record.sessionId, - updatedAt: timestamp, - }) - .onConflictDoUpdate({ - set: { - readinessJson, - updatedAt: timestamp, - }, - target: sessionReadinessSnapshotsTable.sessionId, - }) - .run(); + return database + .prepare( + `INSERT INTO session_readiness_snapshot (readiness_json, session_id, updated_at) + SELECT ?, ?, ? + WHERE ${INSERTED_EVENT_FENCE} + ON CONFLICT (session_id) DO UPDATE SET + readiness_json = excluded.readiness_json, + updated_at = excluded.updated_at`, + ) + .bind( + JSON.stringify(readiness), + record.sessionId, + record.createdAt, + record.eventId, + record.sessionId, + record.event.kind, + ); } -export async function appSessionViewerRuntimeEvents( +function prepareRunStarted( database: D1Database, - records: readonly SessionViewerProjectionRuntimeEvent[], -): Promise { - for (const record of records) { - switch (record.event.kind) { - case "permission.requested": { - await upsertPermissionRequest(database, record); - break; - } - case "permission.resolved": { - await appPermissionResolution(database, record); - break; - } - case "run.cancelled": - case "run.completed": - case "run.failed": { - await clearRunPermissionRequests(database, record); - break; - } - case "session.readiness.updated": { - await upsertReadinessSnapshot(database, record); - break; - } - default: { - break; - } + record: SessionViewerProjectionRuntimeEvent, +): D1PreparedStatement[] { + const runId = requirePermissionRunId(record); + const run = readRuntimeRunPayload(record.event).run; + const startedAt = + run?.startedAt === null || run?.startedAt === undefined + ? Number.NaN + : Date.parse(run.startedAt); + if (run === null || run.id !== runId || run.status !== "running" || !Number.isFinite(startedAt)) { + throw new Error("Runtime run.started projection requires one exact running Run view."); + } + + const update = database + .prepare( + `UPDATE session_run + SET error_code = NULL, + error_details_json = NULL, + error_message = NULL, + error_retryable = NULL, + started_at = COALESCE(started_at, ?), + status = CASE WHEN status IN ('queued', 'booting') THEN 'running' ELSE status END, + status_changed_at = CASE + WHEN status IN ('queued', 'booting') THEN ? ELSE status_changed_at END, + status_event = CASE + WHEN status IN ('queued', 'booting') THEN 'run.start' ELSE status_event END, + status_operation_id = CASE + WHEN status IN ('queued', 'booting') THEN NULL ELSE status_operation_id END, + status_seq = status_seq + CASE WHEN status IN ('queued', 'booting') THEN 1 ELSE 0 END, + status_source = CASE + WHEN status IN ('queued', 'booting') THEN ? ELSE status_source END, + updated_at = CASE + WHEN status IN ('queued', 'booting') THEN ? ELSE updated_at END + WHERE id = ? + AND session_id = ? + AND status IN ('queued', 'booting', 'running', 'waiting_input') + AND ${INSERTED_EVENT_FENCE}`, + ) + .bind( + startedAt, + record.createdAt, + record.event.origin, + record.createdAt, + runId, + record.sessionId, + record.eventId, + record.sessionId, + record.event.kind, + ); + const guard = database + .prepare( + `INSERT INTO session_event (id) + SELECT ? + WHERE NOT EXISTS ( + SELECT 1 + FROM session_event AS receipt + INNER JOIN session_run AS run + ON run.id = ? AND run.session_id = receipt.session_id + WHERE receipt.id = ? + AND receipt.session_id = ? + AND receipt.event_type = 'run.started' + AND run.status IN ('running', 'waiting_input') + AND run.started_at IS NOT NULL + )`, + ) + .bind(createPlatformId(), runId, record.eventId, record.sessionId); + + return [update, guard]; +} + +export function prepareSessionViewerRuntimeEventProjection( + database: D1Database, + record: SessionViewerProjectionRuntimeEvent, +): D1PreparedStatement[] { + switch (record.event.kind) { + case "permission.requested": { + return preparePermissionRequested(database, record); + } + case "permission.resolved": { + return preparePermissionResolved(database, record); + } + case "run.started": { + return prepareRunStarted(database, record); + } + case "session.readiness.updated": { + return [prepareReadinessUpsert(database, record)]; + } + default: { + return []; } } } diff --git a/apps/api/src/modules/sessions/infrastructure/session-viewer-live-snapshot.repository.ts b/apps/api/src/modules/sessions/infrastructure/session-viewer-live-snapshot.repository.ts index 9380d536..fbb721ce 100644 --- a/apps/api/src/modules/sessions/infrastructure/session-viewer-live-snapshot.repository.ts +++ b/apps/api/src/modules/sessions/infrastructure/session-viewer-live-snapshot.repository.ts @@ -33,10 +33,12 @@ import { toIsoString } from "../../../time"; import { fileStore } from "../../files/application/file-store"; import { parseStoredAgentTaskSnapshot } from "./session-agent-task-snapshot.repository"; import { createInitialSessionLiveState } from "./session-live-state.reducer"; +import { applyStoredSessionArtifacts } from "./session-message-reference.repository"; import { loadStoredSessionMessages } from "./session-message-snapshot.repository"; interface SessionViewerStateSessionRow { id: SessionId; + runtime_event_seq_cursor: number; status: SessionStatus; title: string | null; updated_at: number; @@ -51,6 +53,7 @@ interface SessionViewerStateJoinedRow extends SessionViewerStateSessionRow { run_error_code: string | null; run_error_details_json: string | null; run_error_message: string | null; + run_error_retryable: boolean | null; run_id: SessionRunId | null; run_model: string | null; run_provider: string | null; @@ -76,6 +79,7 @@ interface SessionViewerStateRunRow { error_code: string | null; error_details_json: string | null; error_message: string | null; + error_retryable: boolean | null; id: SessionRunId; model: string | null; provider: string | null; @@ -102,6 +106,13 @@ export interface LoadSessionViewerStateInput { viewerId: PlatformId; } +export interface LoadedSessionViewerState { + runtimeEventSeqCursor: number; + state: SessionLiveState; +} + +const MAX_CONSISTENT_SNAPSHOT_ATTEMPTS = 5; + async function listSessionViewerStateSnapshotRows( database: D1Database, sessionId: SessionId, @@ -118,6 +129,7 @@ async function listSessionViewerStateSnapshotRows( run_error_code: sessionRunsTable.errorCode, run_error_details_json: sessionRunsTable.errorDetailsJson, run_error_message: sessionRunsTable.errorMessage, + run_error_retryable: sessionRunsTable.errorRetryable, run_id: sessionRunsTable.id, run_model: sessionRunsTable.model, run_provider: sessionRunsTable.provider, @@ -126,6 +138,7 @@ async function listSessionViewerStateSnapshotRows( run_trace_id: sessionRunsTable.traceId, run_trigger: sessionRunsTable.trigger, run_updated_at: sessionRunsTable.updatedAt, + runtime_event_seq_cursor: sessionsTable.runtimeEventSeqCursor, status: sessionsTable.status, task_driver_instance_id: sessionAgentTaskSnapshotsTable.driverInstanceId, task_run_id: sessionAgentTaskSnapshotsTable.runId, @@ -245,7 +258,7 @@ function toRunError(row: SessionViewerStateRunRow): RunError | null { code: row.error_code, details: parseJsonRecord(row.error_details_json), message: row.error_message, - retryable: false, + retryable: row.error_retryable ?? false, }; } @@ -288,6 +301,7 @@ function toJoinedSessionRunSummary(row: SessionViewerStateJoinedRow): SessionRun error_code: row.run_error_code, error_details_json: row.run_error_details_json, error_message: row.run_error_message, + error_retryable: row.run_error_retryable, id: row.run_id, model: row.run_model, provider: row.run_provider, @@ -406,23 +420,22 @@ function applyCanonicalSessionState( }; } -export async function loadSessionViewerState( +async function loadSessionViewerStateSnapshotOnce( database: D1Database, input: LoadSessionViewerStateInput, -): Promise { - const snapshotRowsPromise = listSessionViewerStateSnapshotRows(database, input.sessionId); +): Promise { + const snapshotRows = await listSessionViewerStateSnapshotRows(database, input.sessionId); + const session = getFirstSnapshotRow(snapshotRows).session; const messagesPromise = loadStoredSessionMessages(database, input.sessionId); const permissionRequestsPromise = listActivePermissionRequests(database, input.sessionId); const readinessPromise = getLatestReadinessSnapshot(database, input.sessionId); const sessionFilesPromise = fileStore.listReadySessionFiles(database, input.sessionId); - const [snapshotRows, messages, permissionRequests, readiness, sessionFiles] = await Promise.all([ - snapshotRowsPromise, + const [messages, permissionRequests, readiness, sessionFiles] = await Promise.all([ messagesPromise, permissionRequestsPromise, readinessPromise, sessionFilesPromise, ]); - const session = getFirstSnapshotRow(snapshotRows).session; const latestRun = toJoinedSessionRunSummary(session); const taskSnapshot = toJoinedAgentTaskSnapshot(session); const baseState = createInitialSessionLiveState({ @@ -445,5 +458,50 @@ export async function loadSessionViewerState( viewerId: input.viewerId, }); - return state; + const resolvedState = + session.runtime_event_seq_cursor === 0 + ? state + : await applyStoredSessionArtifacts(database, { + endSeq: session.runtime_event_seq_cursor, + includeActiveRunArtifacts: latestRun !== null && !isTerminalRunStatus(latestRun.status), + runId: latestRun?.id ?? null, + sessionId: input.sessionId, + state, + }); + + return { + runtimeEventSeqCursor: session.runtime_event_seq_cursor, + state: resolvedState, + }; +} + +export async function loadSessionViewerStateSnapshot( + database: D1Database, + input: LoadSessionViewerStateInput, +): Promise { + for (let attempt = 0; attempt < MAX_CONSISTENT_SNAPSHOT_ATTEMPTS; attempt += 1) { + const snapshot = await loadSessionViewerStateSnapshotOnce(database, input); + const current = await getAppDatabase(database) + .select({ runtimeEventSeqCursor: sessionsTable.runtimeEventSeqCursor }) + .from(sessionsTable) + .where(eq(sessionsTable.id, input.sessionId)) + .limit(1) + .get(); + + if (current === undefined) { + throw new Error("Session not found."); + } + if (current.runtimeEventSeqCursor === snapshot.runtimeEventSeqCursor) { + return snapshot; + } + } + + throw new Error("Session changed while its viewer state snapshot was loading."); +} + +export async function loadSessionViewerState( + database: D1Database, + input: LoadSessionViewerStateInput, +): Promise { + return (await loadSessionViewerStateSnapshot(database, input)).state; } diff --git a/apps/api/src/modules/sessions/infrastructure/session/client.ts b/apps/api/src/modules/sessions/infrastructure/session/client.ts index b246e09b..bf369e24 100644 --- a/apps/api/src/modules/sessions/infrastructure/session/client.ts +++ b/apps/api/src/modules/sessions/infrastructure/session/client.ts @@ -15,6 +15,12 @@ function getSessionStub(env: ApiBindings, sessionId: SessionId): DurableObjectSt return binding.get(binding.idFromName(sessionId)); } +export interface SessionViewerEventBatch { + events: AgUiSessionEvent[]; + previousRuntimeEventSeqCursor: number | null; + runtimeEventSeqCursor: number | null; +} + function createSessionDoRequest(sessionId: SessionId, path: string, init?: RequestInit): Request { const headers = new Headers(init?.headers); headers.set(SESSION_ID_HEADER, sessionId); @@ -52,16 +58,19 @@ export async function connectSessionViewerWebSocket( ); } -export async function publishSessionViewerEvents( +export async function publishSessionViewerEventBatches( env: ApiBindings, sessionId: SessionId | null, - events: AgUiSessionEvent[], + batches: SessionViewerEventBatch[], ): Promise { - if (sessionId === null || sessionId === "" || events.length === 0) { + if ( + sessionId === null || + sessionId === "" || + batches.every((batch) => batch.events.length === 0) + ) { return; } - - await getSessionStub(env, sessionId).publishEvents(sessionId, events); + await getSessionStub(env, sessionId).publishEventBatches(sessionId, batches); } export async function connectSessionPublicEventWebSocket( diff --git a/apps/api/src/modules/sessions/infrastructure/session/do.ts b/apps/api/src/modules/sessions/infrastructure/session/do.ts index 96146dc7..800e0484 100644 --- a/apps/api/src/modules/sessions/infrastructure/session/do.ts +++ b/apps/api/src/modules/sessions/infrastructure/session/do.ts @@ -139,16 +139,41 @@ export class Session extends DurableObject { return normalizedSessionId; } - async publishEvents(sessionId: string, events: AgUiSessionEvent[]): Promise { + async publishEvents( + sessionId: string, + events: AgUiSessionEvent[], + runtimeEventSeqCursor: number | null = null, + previousRuntimeEventSeqCursor: number | null = null, + ): Promise { this.#ensureActiveRpcSession(sessionId); if (events.length > 0) { this.#publicEventSockets.notifyEventsAvailable(); } - await this.#viewerSockets.broadcastEvents(events); + await this.#viewerSockets.broadcastEvents( + events, + runtimeEventSeqCursor, + previousRuntimeEventSeqCursor, + ); + } + + async publishEventBatches( + sessionId: string, + batches: Array<{ + events: AgUiSessionEvent[]; + previousRuntimeEventSeqCursor: number | null; + runtimeEventSeqCursor: number | null; + }>, + ): Promise { + this.#ensureActiveRpcSession(sessionId); + if (batches.some((batch) => batch.events.length > 0)) { + this.#publicEventSockets.notifyEventsAvailable(); + } + await this.#viewerSockets.broadcastEventBatches(batches); } async syncViewers(sessionId: string): Promise { this.#ensureActiveRpcSession(sessionId); + this.#publicEventSockets.notifyEventsAvailable(); await this.#viewerSockets.broadcastStateSync(); } diff --git a/apps/api/src/modules/sessions/infrastructure/session/viewer-live-state.ts b/apps/api/src/modules/sessions/infrastructure/session/viewer-live-state.ts index b9a7d54a..3dede25c 100644 --- a/apps/api/src/modules/sessions/infrastructure/session/viewer-live-state.ts +++ b/apps/api/src/modules/sessions/infrastructure/session/viewer-live-state.ts @@ -2,6 +2,7 @@ import type { SessionId } from "@mosoo/id"; import type { AuthenticatedViewer } from "../../../auth/application/viewer-auth.service"; import { reconcileStaleActiveSessionRun } from "../../../runtime/application/session-runs/stale-run-reconciliation.service"; +import { loadSessionAgentTaskState } from "../session-agent-task-snapshot.repository"; import type { SessionLiveState } from "../session-live-state.types"; import { loadSessionViewerState } from "../session-viewer-live-snapshot.repository"; @@ -21,7 +22,20 @@ export async function loadViewerLiveState( (await reconcileStaleActiveSessionRun(input.database, input.sessionId)); if (input.cachedState && !reconciledStaleRun) { - return normalizeViewerLiveState(input.cachedState, input); + const taskState = await loadSessionAgentTaskState(input.database, input.sessionId); + const cacheGenerationIsCurrent = + taskState !== null && + input.cachedState.lifecycle === "RUNNING" && + taskState.runId === input.cachedState.run.id && + taskState.runStatus === input.cachedState.run.status && + taskState.driverInstanceId === input.cachedState.infra.driverInstanceId; + + if (cacheGenerationIsCurrent) { + return normalizeViewerLiveState( + { ...input.cachedState, taskSnapshot: taskState.snapshot }, + input, + ); + } } return loadSessionViewerState(input.database, { diff --git a/apps/api/src/modules/sessions/infrastructure/session/viewer-permissions.ts b/apps/api/src/modules/sessions/infrastructure/session/viewer-permissions.ts index adbc18ba..a753d1b3 100644 --- a/apps/api/src/modules/sessions/infrastructure/session/viewer-permissions.ts +++ b/apps/api/src/modules/sessions/infrastructure/session/viewer-permissions.ts @@ -34,7 +34,7 @@ export async function rejectDisconnectedViewerPermissionRequests( await appendSessionRuntimeEvents({ bindings: input.env, deliver: false, - events: [result.event], + events: result.events, sessionId: input.attachment.sessionId, }); diff --git a/apps/api/src/modules/sessions/infrastructure/session/viewer-socket-hub.ts b/apps/api/src/modules/sessions/infrastructure/session/viewer-socket-hub.ts index e19f809a..e4bddbea 100644 --- a/apps/api/src/modules/sessions/infrastructure/session/viewer-socket-hub.ts +++ b/apps/api/src/modules/sessions/infrastructure/session/viewer-socket-hub.ts @@ -7,6 +7,7 @@ import { } from "../../../../platform/cloudflare/durable-object-support"; import { createErrorLogContext, logError, logInfo } from "../../../../platform/cloudflare/logger"; import type { ApiBindings } from "../../../../platform/cloudflare/worker-types"; +import { currentTimestampMs } from "../../../../time"; import type { SessionLiveState } from "../session-live-state.types"; import { json } from "./requests"; import { readSessionViewerSocketHeaders } from "./socket-headers"; @@ -28,6 +29,8 @@ import { declare const WebSocketPair: new () => [WebSocket, WebSocket]; +const VIEWER_CURSOR_RECONCILIATION_DELAY_MS = 10_000; + interface SessionViewerSocketHubOptions { ctx: DurableObjectState; env: ApiBindings; @@ -36,6 +39,12 @@ interface SessionViewerSocketHubOptions { withSessionLogContext: (fn: () => T) => T; } +export interface SessionViewerEventBatch { + events: AgUiSessionEvent[]; + previousRuntimeEventSeqCursor: number | null; + runtimeEventSeqCursor: number | null; +} + function getSocketAttachment(ws: WebSocket): SessionSocketAttachment | null { const attachment: unknown = ws.deserializeAttachment(); return isViewerSocketAttachment(attachment) ? attachment : null; @@ -58,39 +67,108 @@ export class SessionViewerSocketHub { this.#withSessionLogContext = options.withSessionLogContext; } - async broadcastEvents(events: AgUiSessionEvent[]): Promise { - if (events.length === 0) { + async broadcastEvents( + events: AgUiSessionEvent[], + runtimeEventSeqCursor: number | null = null, + previousRuntimeEventSeqCursor: number | null = null, + ): Promise { + await this.broadcastEventBatches([ + { events, previousRuntimeEventSeqCursor, runtimeEventSeqCursor }, + ]); + } + + async broadcastEventBatches(batches: SessionViewerEventBatch[]): Promise { + if (batches.every((batch) => batch.events.length === 0)) { return; } await this.#runStateOperation(async () => { - const broadcast = buildViewerBroadcastFrames({ - cachedState: this.#liveStateCache, - events, + const sockets = this.#getViewerSockets().flatMap((socket) => { + const attachment = getSocketAttachment(socket); + return attachment === null || socket.readyState !== WebSocket.OPEN + ? [] + : [{ attachment, frames: [] as string[], requiresStateSync: false, socket }]; }); - if (!broadcast) { - return; - } - - if (broadcast.state) { - this.#liveStateCache = broadcast.state; + for (const batch of batches) { + const broadcast = buildViewerBroadcastFrames({ + cachedState: batch.runtimeEventSeqCursor === null ? this.#liveStateCache : null, + events: batch.events, + }); + if (broadcast === null) { + continue; + } + if (batch.runtimeEventSeqCursor !== null) { + this.#liveStateCache = null; + } else if (broadcast.state !== null) { + this.#liveStateCache = broadcast.state; + } + for (const target of sockets) { + if (target.requiresStateSync) { + continue; + } + if (target.attachment.runtimeEventSeqCursor === undefined) { + target.frames = []; + target.requiresStateSync = true; + continue; + } + if ( + batch.runtimeEventSeqCursor !== null && + target.attachment.runtimeEventSeqCursor >= batch.runtimeEventSeqCursor + ) { + continue; + } + if ( + batch.runtimeEventSeqCursor !== null && + (batch.previousRuntimeEventSeqCursor === null || + target.attachment.runtimeEventSeqCursor !== batch.previousRuntimeEventSeqCursor) + ) { + target.frames = []; + target.requiresStateSync = true; + continue; + } + target.frames.push(...broadcast.frames); + if (batch.runtimeEventSeqCursor !== null) { + target.attachment = { + ...target.attachment, + runtimeEventSeqCursor: batch.runtimeEventSeqCursor, + }; + } + } } - for (const socket of this.#getViewerSockets()) { - const attachment = getSocketAttachment(socket); - - if (!attachment || socket.readyState !== WebSocket.OPEN) { + for (const target of sockets) { + if (target.requiresStateSync) { continue; } - - sendFrames(socket, broadcast.frames); + if (target.frames.length === 0) { + continue; + } + try { + sendFrames(target.socket, target.frames); + target.socket.serializeAttachment(target.attachment); + } catch { + closeOpenSocket(target.socket, 1011, "session.viewer.event-delivery-failed"); + } + } + const stateSyncTargets = sockets + .filter((target) => target.requiresStateSync) + .map(({ attachment, socket }) => ({ attachment, socket })); + if (stateSyncTargets.length > 0) { + await sendViewerSocketStateSyncBatch({ + database: this.#env.DB, + sockets: stateSyncTargets, + updateLiveStateCache: (state) => { + this.#rememberLoadedLiveState(state); + }, + }); } }); } async broadcastStateSync(): Promise { await this.#runStateOperation(async () => { + this.#liveStateCache = null; const sockets = this.#getViewerSockets() .map((socket) => ({ attachment: getSocketAttachment(socket), socket })) .filter( @@ -103,9 +181,7 @@ export class SessionViewerSocketHub { ); await sendViewerSocketStateSyncBatch({ - cachedState: this.#liveStateCache, database: this.#env.DB, - getLatestCachedState: () => this.#liveStateCache, sockets, updateLiveStateCache: (state) => { this.#rememberLoadedLiveState(state); @@ -137,7 +213,11 @@ export class SessionViewerSocketHub { this.#rememberSessionId(attachment.sessionId); this.#ctx.acceptWebSocket(server, ["viewer"]); server.serializeAttachment(attachment); - this.#ctx.waitUntil(clearViewerPermissionCleanupAlarm({ storage: this.#ctx.storage })); + this.#ctx.waitUntil( + clearViewerPermissionCleanupAlarm({ storage: this.#ctx.storage }).then(() => + this.#scheduleViewerCursorReconciliation(), + ), + ); this.#ctx.waitUntil(this.#sendViewerStateSync(server, attachment)); this.#withSessionLogContext(() => { @@ -219,15 +299,23 @@ export class SessionViewerSocketHub { async handleAlarm(): Promise { await this.#runStateOperation(async () => { - await runViewerPermissionCleanupAlarm({ - cachedState: this.#liveStateCache, - env: this.#env, - hasOpenViewer: (sessionId) => this.#hasOpenViewer(sessionId), - storage: this.#ctx.storage, - updateLiveStateCache: (state) => { - this.#rememberLoadedLiveState(state); - }, - }); + try { + await runViewerPermissionCleanupAlarm({ + cachedState: this.#liveStateCache, + env: this.#env, + hasOpenViewer: (sessionId) => this.#hasOpenViewer(sessionId), + storage: this.#ctx.storage, + updateLiveStateCache: (state) => { + this.#rememberLoadedLiveState(state); + }, + }); + } finally { + try { + await this.#reconcileViewerCursors(); + } finally { + await this.#scheduleViewerCursorReconciliation(); + } + } }); } @@ -235,9 +323,7 @@ export class SessionViewerSocketHub { await this.#runStateOperation(async () => { await sendViewerSocketStateSync({ attachment, - cachedState: this.#liveStateCache, database: this.#env.DB, - getLatestCachedState: () => this.#liveStateCache, updateLiveStateCache: (state) => { this.#rememberLoadedLiveState(state); }, @@ -274,10 +360,87 @@ export class SessionViewerSocketHub { }); } + async #reconcileViewerCursors(): Promise { + const sessionId = this.#resolveSessionId(); + if (sessionId === null) { + return; + } + const durable = await this.#env.DB.prepare( + "SELECT runtime_event_seq_cursor FROM session WHERE id = ?", + ) + .bind(sessionId) + .first<{ runtime_event_seq_cursor: number }>(); + const sockets = this.#getViewerSockets().flatMap((socket) => { + const attachment = getSocketAttachment(socket); + return attachment !== null && + durable !== null && + (attachment.runtimeEventSeqCursor === undefined || + attachment.runtimeEventSeqCursor !== durable.runtime_event_seq_cursor) + ? [{ attachment, socket }] + : []; + }); + if (sockets.length === 0) { + return; + } + + await sendViewerSocketStateSyncBatch({ + database: this.#env.DB, + sockets, + updateLiveStateCache: (state) => { + this.#rememberLoadedLiveState(state); + }, + }); + } + + async #scheduleViewerCursorReconciliation(): Promise { + const sessionId = this.#resolveSessionId(); + if (sessionId === null || !this.#hasOpenViewer(sessionId)) { + return; + } + await this.#ctx.storage.setAlarm(currentTimestampMs() + VIEWER_CURSOR_RECONCILIATION_DELAY_MS); + } + + #resolveSessionId(): string | null { + const rememberedSessionId = this.#getSessionId(); + const attachedSessionIds = new Set(); + + for (const socket of this.#getViewerSockets()) { + const attachment = getSocketAttachment(socket); + if (attachment !== null) { + attachedSessionIds.add(attachment.sessionId); + } + } + + if (rememberedSessionId !== null) { + for (const socket of this.#getViewerSockets()) { + const attachment = getSocketAttachment(socket); + if (attachment !== null && attachment.sessionId !== rememberedSessionId) { + closeOpenSocket(socket, 1008, "session.viewer.session-mismatch"); + } + } + return rememberedSessionId; + } + + if (attachedSessionIds.size !== 1) { + if (attachedSessionIds.size > 1) { + this.closeSockets("session.viewer.session-mismatch"); + } + return null; + } + + const [sessionId] = attachedSessionIds; + if (sessionId === undefined) { + return null; + } + this.#rememberSessionId(sessionId); + return sessionId; + } + async #scheduleViewerPermissionsOnLastDisconnect( attachment: ViewerSocketAttachment, ): Promise { if (this.#hasOpenViewer(attachment.sessionId)) { + await this.#scheduleViewerCursorReconciliation(); return; } diff --git a/apps/api/src/modules/sessions/infrastructure/session/viewer-socket-state-sync.ts b/apps/api/src/modules/sessions/infrastructure/session/viewer-socket-state-sync.ts index 89edba6e..a70ba7d5 100644 --- a/apps/api/src/modules/sessions/infrastructure/session/viewer-socket-state-sync.ts +++ b/apps/api/src/modules/sessions/infrastructure/session/viewer-socket-state-sync.ts @@ -8,14 +8,12 @@ import { import { reconcileStaleActiveSessionRun } from "../../../runtime/application/session-runs/stale-run-reconciliation.service"; import { getActiveAppSessionParticipantAccess } from "../../domain/session-access.policy"; import type { SessionLiveState } from "../session-live-state.types"; -import { loadViewerLiveState } from "./viewer-live-state"; +import { loadSessionViewerStateSnapshot } from "../session-viewer-live-snapshot.repository"; import type { ViewerSocketAttachment } from "./viewer-socket"; interface SendViewerSocketStateSyncOptions { attachment: ViewerSocketAttachment; - cachedState: SessionLiveState | null; database: D1Database; - getLatestCachedState(): SessionLiveState | null; updateLiveStateCache(state: SessionLiveState | null): void; ws: WebSocket; } @@ -26,9 +24,7 @@ interface ViewerSocketStateSyncTarget { } interface SendViewerSocketStateSyncBatchOptions { - cachedState: SessionLiveState | null; database: D1Database; - getLatestCachedState(): SessionLiveState | null; sockets: ViewerSocketStateSyncTarget[]; updateLiveStateCache(state: SessionLiveState | null): void; } @@ -56,9 +52,7 @@ export async function sendViewerSocketStateSync( options: SendViewerSocketStateSyncOptions, ): Promise { await sendViewerSocketStateSyncBatch({ - cachedState: options.cachedState, database: options.database, - getLatestCachedState: options.getLatestCachedState, sockets: [ { attachment: options.attachment, @@ -73,7 +67,6 @@ export async function sendViewerSocketStateSyncBatch( options: SendViewerSocketStateSyncBatchOptions, ): Promise { const groupedTargets = groupOpenViewerSockets(options.sockets); - let cachedState = options.cachedState; const reconciledStaleRunsBySessionId = new Map(); for (const targets of groupedTargets.values()) { @@ -97,28 +90,30 @@ export async function sendViewerSocketStateSyncBatch( throw error; } - cachedState ??= options.getLatestCachedState(); - const reconciledStaleRun = await getReconciledStaleRun( + await getReconciledStaleRun( options.database, firstTarget.attachment.sessionId, reconciledStaleRunsBySessionId, ); - const state = await loadViewerLiveState({ - cachedState, - database: options.database, - reconciledStaleRun, + const snapshot = await loadSessionViewerStateSnapshot(options.database, { sessionId: firstTarget.attachment.sessionId, - viewer: firstTarget.attachment.viewer, + viewerId: firstTarget.attachment.viewer.id, }); - const latestState = options.getLatestCachedState(); - const stateToSend = latestState !== null && latestState !== cachedState ? latestState : state; + const stateToSend = snapshot.state; const frames = createStateSyncFrames(stateToSend); options.updateLiveStateCache(stateToSend); - cachedState = stateToSend; for (const target of targets) { - sendFrames(target.socket, frames); + try { + sendFrames(target.socket, frames); + target.socket.serializeAttachment({ + ...target.attachment, + runtimeEventSeqCursor: snapshot.runtimeEventSeqCursor, + } satisfies ViewerSocketAttachment); + } catch { + closeOpenSocket(target.socket, 1011, "session.viewer.state-sync-failed"); + } } } } diff --git a/apps/api/src/modules/sessions/infrastructure/session/viewer-socket.ts b/apps/api/src/modules/sessions/infrastructure/session/viewer-socket.ts index 6646a904..e1a73ba4 100644 --- a/apps/api/src/modules/sessions/infrastructure/session/viewer-socket.ts +++ b/apps/api/src/modules/sessions/infrastructure/session/viewer-socket.ts @@ -2,6 +2,7 @@ import type { SessionViewerSocketContext } from "./socket-headers"; export interface ViewerSocketAttachment extends SessionViewerSocketContext { role: "viewer"; + runtimeEventSeqCursor?: number; } export type SessionSocketAttachment = ViewerSocketAttachment; @@ -21,6 +22,10 @@ export function isViewerSocketAttachment(value: unknown): value is ViewerSocketA typeof value["publicOrigin"] === "string" && typeof value["appId"] === "string" && typeof value["sessionId"] === "string" && + (value["runtimeEventSeqCursor"] === undefined || + (typeof value["runtimeEventSeqCursor"] === "number" && + Number.isSafeInteger(value["runtimeEventSeqCursor"]) && + value["runtimeEventSeqCursor"] >= 0)) && isRecord(viewer) && typeof viewer["email"] === "string" && typeof viewer["emailVerified"] === "boolean" && diff --git a/apps/api/src/modules/skills/application/skill-package.shared.ts b/apps/api/src/modules/skills/application/skill-package.shared.ts index 6ef518ca..24edd355 100644 --- a/apps/api/src/modules/skills/application/skill-package.shared.ts +++ b/apps/api/src/modules/skills/application/skill-package.shared.ts @@ -35,7 +35,7 @@ export async function sha256Hex(bytes: Uint8Array): Promise { ? bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength) : Uint8Array.from(bytes).buffer; const digest = await crypto.subtle.digest("SHA-256", buffer); - return [...new Uint8Array(digest)].map((byte) => byte.toString(16).padStart(2, "0")).join(""); + return new Uint8Array(digest).toHex(); } export function inferMimeType(path: string): string | null { diff --git a/apps/api/src/modules/users/application/viewer-context.service.ts b/apps/api/src/modules/users/application/viewer-context.service.ts index 85f04c43..adb40c1c 100644 --- a/apps/api/src/modules/users/application/viewer-context.service.ts +++ b/apps/api/src/modules/users/application/viewer-context.service.ts @@ -111,19 +111,8 @@ async function getViewerAccountState( function getViewerAuth(bindings: ApiBindings, viewer: AuthenticatedViewer | null): ViewerAuth { const methods: AuthMethod[] = ["email_otp"]; - const authBindings = bindings as ApiBindings & { - GOOGLE_OAUTH_CLIENT_ID?: string; - GOOGLE_OAUTH_CLIENT_SECRET?: string; - }; - if ( - authBindings.GOOGLE_OAUTH_CLIENT_ID?.trim() !== null && - authBindings.GOOGLE_OAUTH_CLIENT_ID?.trim() !== undefined && - authBindings.GOOGLE_OAUTH_CLIENT_ID?.trim() !== "" && - authBindings.GOOGLE_OAUTH_CLIENT_SECRET?.trim() !== null && - authBindings.GOOGLE_OAUTH_CLIENT_SECRET?.trim() !== undefined && - authBindings.GOOGLE_OAUTH_CLIENT_SECRET?.trim() !== "" - ) { + if (bindings.GOOGLE_OAUTH_CLIENT_ID?.trim() && bindings.GOOGLE_OAUTH_CLIENT_SECRET?.trim()) { methods.push("google_oauth"); } diff --git a/apps/api/src/platform/cloudflare/app-deployment-workflow.ts b/apps/api/src/platform/cloudflare/app-deployment-workflow.ts new file mode 100644 index 00000000..9634a17c --- /dev/null +++ b/apps/api/src/platform/cloudflare/app-deployment-workflow.ts @@ -0,0 +1,15 @@ +import { WorkflowEntrypoint } from "cloudflare:workers"; +import type { WorkflowEvent, WorkflowStep } from "cloudflare:workers"; + +import type { ApiCommandMessage } from "../../modules/api-command/application/api-command-message"; +import { runAppDeploymentWorkflow } from "../../modules/api-command/application/api-command-workflow"; +import type { ApiBindings } from "./worker-types"; + +export class AppDeploymentWorkflow extends WorkflowEntrypoint { + override async run( + event: Readonly>, + step: WorkflowStep, + ): Promise { + await runAppDeploymentWorkflow(this.env, event.payload, step); + } +} diff --git a/apps/api/src/platform/cloudflare/create-api-worker.ts b/apps/api/src/platform/cloudflare/create-api-worker.ts index 0d4ad3ed..8ad5721a 100644 --- a/apps/api/src/platform/cloudflare/create-api-worker.ts +++ b/apps/api/src/platform/cloudflare/create-api-worker.ts @@ -1,12 +1,20 @@ import { MOSOO_CONSOLE_HOST, MOSOO_LEGACY_CONSOLE_HOST } from "@mosoo/contracts/origin"; import { enqueueScheduledMaintenanceCommand } from "../../modules/api-command/application/api-command-enqueue"; -import { redriveFailedApiCommandEnqueues } from "../../modules/api-command/application/api-command-ledger"; +import { + reconcileAppDeploymentWorkflowDeliveries, + redriveFailedApiCommandEnqueues, +} from "../../modules/api-command/application/api-command-ledger"; import type { ApiCommandMessage } from "../../modules/api-command/application/api-command-message"; import { processApiCommandDeadLetterMessage, processApiCommandMessage, } from "../../modules/api-command/application/api-command-processor"; +import { + dispatchAppDeploymentGatewayRequest, + resolveActiveAppDeploymentScriptName, +} from "../../modules/apps/application/app-deployment-gateway"; +import { createErrorLogContext, logError } from "./logger"; import type { ApiBindings } from "./worker-types"; interface ApiHttpApp { @@ -36,6 +44,17 @@ export function createApiWorker(): ExportedHandler { return Response.redirect(url.toString(), 308); } + const gatewayResponse = await dispatchAppDeploymentGatewayRequest(request, { + appDeploymentDomain: env.MOSOO_APP_DEPLOYMENT_DOMAIN, + dispatcher: env.APP_DEPLOYMENT_DISPATCHER, + resolveActiveScriptName: (mosooSubdomain) => + resolveActiveAppDeploymentScriptName(env.DB, mosooSubdomain), + }); + + if (gatewayResponse !== null) { + return gatewayResponse; + } + const app = await getHttpApp(); const response = await app.fetch(request, env, ctx); @@ -43,6 +62,14 @@ export function createApiWorker(): ExportedHandler { }, async scheduled(controller: ScheduledController, env: ApiBindings): Promise { await redriveFailedApiCommandEnqueues(env); + try { + await reconcileAppDeploymentWorkflowDeliveries(env, controller.scheduledTime); + } catch (error) { + logError("api-command.app_deployment_workflow_reconciliation_failed", { + ...createErrorLogContext(error), + scheduledTime: controller.scheduledTime, + }); + } await enqueueScheduledMaintenanceCommand(env, { scheduledTime: controller.scheduledTime, }); diff --git a/apps/api/src/platform/cloudflare/worker-types.ts b/apps/api/src/platform/cloudflare/worker-types.ts index e429ce75..3bd5b5c8 100644 --- a/apps/api/src/platform/cloudflare/worker-types.ts +++ b/apps/api/src/platform/cloudflare/worker-types.ts @@ -19,6 +19,10 @@ interface ApiCommandQueueBinding { API_COMMAND_QUEUE: Queue; } +interface AppDeploymentWorkflowBinding { + APP_DEPLOYMENT_WORKFLOW: Workflow; +} + interface OptionalLocalProviderFetchProxyBindings { MOSOO_PROVIDER_FETCH_PROXY_TOKEN?: string; MOSOO_PROVIDER_FETCH_PROXY_URL?: string; @@ -58,6 +62,7 @@ interface OptionalRuntimeSubjectPlatformBindings { } export type ApiBindings = Env & + AppDeploymentWorkflowBinding & ApiCommandQueueBinding & OptionalSandboxBinding & OptionalDriverConnectionBinding & diff --git a/apps/api/src/platform/db/drizzle.ts b/apps/api/src/platform/db/drizzle.ts index 90cdb28a..11530724 100644 --- a/apps/api/src/platform/db/drizzle.ts +++ b/apps/api/src/platform/db/drizzle.ts @@ -144,10 +144,10 @@ async function readAllRows(statement: D1PreparedStatement): Promise const row = await statement.first(); return row === null ? result - : ({ + : { ...result, results: [row], - } as D1Result); + }; } catch (error) { if (!isCompatMethodNotImplementedError(error) || typeof statement.first !== "function") { throw error; diff --git a/apps/api/src/shared/bytes.ts b/apps/api/src/shared/bytes.ts index c1c3f331..16c257f0 100644 --- a/apps/api/src/shared/bytes.ts +++ b/apps/api/src/shared/bytes.ts @@ -15,39 +15,18 @@ export function toArrayBuffer(bytes: Uint8Array): ArrayBuffer { } export function toBase64(bytes: Uint8Array): string { - let binary = ""; - - for (const byte of bytes) { - binary += String.fromCodePoint(byte); - } - - return btoa(binary); + return bytes.toBase64(); } -export function fromBase64(value: string): Uint8Array { - const binary = atob(value); - const bytes = new Uint8Array(binary.length); - - for (let index = 0; index < binary.length; index += 1) { - const byte = binary.codePointAt(index); - - if (byte === undefined) { - throw new Error("Base64 payload is invalid."); - } - - bytes[index] = byte; - } - - return bytes; +export function fromBase64(value: string): Uint8Array { + return Uint8Array.fromBase64(value); } export function toBase64Url(value: Uint8Array | string): string { const bytes = typeof value === "string" ? new TextEncoder().encode(value) : value; - return toBase64(bytes).replaceAll("+", "-").replaceAll("/", "_").replace(/=+$/u, ""); + return bytes.toBase64({ alphabet: "base64url", omitPadding: true }); } -export function fromBase64Url(value: string): Uint8Array { - const normalized = value.replaceAll("-", "+").replaceAll("_", "/"); - const padded = normalized.padEnd(normalized.length + ((4 - (normalized.length % 4)) % 4), "="); - return fromBase64(padded); +export function fromBase64Url(value: string): Uint8Array { + return Uint8Array.fromBase64(value, { alphabet: "base64url" }); } diff --git a/apps/api/src/shared/shell.ts b/apps/api/src/shared/shell.ts new file mode 100644 index 00000000..9a9df617 --- /dev/null +++ b/apps/api/src/shared/shell.ts @@ -0,0 +1,3 @@ +export function quoteShellArg(value: string): string { + return `'${value.replaceAll("'", `'"'"'`)}'`; +} diff --git a/apps/api/tests/agent-package-file-import.test.ts b/apps/api/tests/agent-package-file-import.test.ts index 7ed70387..fb809e65 100644 --- a/apps/api/tests/agent-package-file-import.test.ts +++ b/apps/api/tests/agent-package-file-import.test.ts @@ -264,7 +264,7 @@ async function createFixture(input: { archiveBytes?: Uint8Array } = {}) { `); const bucket = new MemoryByteBucket(); const bindings = createPublicHttpTestBindings(database, { - fileBucket: bucket as unknown as R2Bucket, + fileBucket: bucket, }) as ApiBindings; const archiveBytes = input.archiveBytes ?? createAgentPackageArchiveBytes(createPackageFixture(input)); diff --git a/apps/api/tests/agent-runtime-events-projection.test.ts b/apps/api/tests/agent-runtime-events-projection.test.ts index d775313d..e351572d 100644 --- a/apps/api/tests/agent-runtime-events-projection.test.ts +++ b/apps/api/tests/agent-runtime-events-projection.test.ts @@ -1,7 +1,8 @@ import { describe, expect, test } from "bun:test"; +import { createMcpExecuteFailedEventIdentity } from "@mosoo/agent-driver/events"; +import type { DriverCommandId } from "@mosoo/id"; import { createRuntimeEvent } from "@mosoo/runtime-events"; -import type { RuntimeEventKind } from "@mosoo/runtime-events"; import { readPermissionRequestViews, @@ -109,13 +110,56 @@ const PROJECTION_CASES = [ ] as const; describe("agent runtime event projection", () => { + test("accepts only the content-addressed identity of a proven MCP failure", () => { + const commandId = "01J0000000000000000000000X" as DriverCommandId; + const failed = createMcpExecuteFailedEventIdentity({ + commandId, + rawInput: '{"issue":"A-1"}', + rawOutput: "provider rejected the request", + title: "createIssue", + toolCallId: "tool-A-1", + }); + const event = createRuntimeEvent({ + correlationId: commandId, + id: "failed-mcp-tool", + kind: "tool.call.updated", + occurredAt: "2026-08-31T00:00:00.000Z", + payload: failed.payload, + sessionId: "session-1", + sourceEventId: failed.sourceEventId, + }); + + expect( + createSessionRuntimeEventProjection(event, { provenMcpCommandId: commandId }), + ).toMatchObject({ + mcpCommandId: commandId, + toolOutputText: "provider rejected the request", + toolStatus: "failed", + }); + expect(() => + createSessionRuntimeEventProjection( + { ...event, sourceEventId: `${failed.sourceEventId}0` }, + { provenMcpCommandId: commandId }, + ), + ).toThrow("Proven MCP command does not match its terminal tool event"); + expect(() => + createSessionRuntimeEventProjection( + { + ...event, + payload: { ...failed.payload, rawOutput: "tampered provider failure" }, + }, + { provenMcpCommandId: commandId }, + ), + ).toThrow("Proven MCP command does not match its terminal tool event"); + }); + test("derives v2 runtime families from the runtime event contract", () => { for (const event of PROJECTION_CASES) { const projection = createSessionRuntimeEventProjection( createRuntimeEvent({ actor: "system", id: `${event.kind}-${event.family}`, - kind: event.kind as RuntimeEventKind, + kind: event.kind, occurredAt: "2026-05-26T00:00:00.000Z", origin: "system", payload: event.value, @@ -136,7 +180,7 @@ describe("agent runtime event projection", () => { } }); - test("apps completed and failed tool output as process-ready content", () => { + test("projects completed, failed, and cancelled tool snapshots", () => { const completed = createSessionRuntimeEventProjection( createRuntimeEvent({ actor: "driver", @@ -170,27 +214,88 @@ describe("agent runtime event projection", () => { sessionId: "session-1", }), ); + const cancelled = createSessionRuntimeEventProjection( + createRuntimeEvent({ + actor: "driver", + id: "tool-3", + kind: "tool.call.updated", + occurredAt: "2026-05-26T00:00:02.000Z", + origin: "driver", + payload: { + status: "cancelled", + title: "Shell", + toolCallId: "tool-3", + }, + sessionId: "session-1", + }), + ); expect(completed).toMatchObject({ - contentText: "Shell result: hello world", + contentText: "hello\nworld", processStatus: "available", processType: "tool.use.completed", toolCallId: "tool-1", - toolInputJson: '{"command":"echo hello","timeout":30}', - toolName: null, + toolInputDeltaJson: null, + toolInputJson: '{"timeout":30,"command":"echo hello"}', + toolName: "Shell", + toolOutputDeltaText: null, + toolOutputText: "hello\nworld", + toolStatus: "completed", }); expect(failed).toMatchObject({ - contentText: "Shell result: permission denied", + contentText: "permission denied", processStatus: "error", processType: "tool.use.completed", toolCallId: "tool-2", + toolInputDeltaJson: null, toolInputJson: null, - toolName: null, + toolName: "Shell", + toolOutputDeltaText: null, + toolOutputText: "permission denied", + toolStatus: "failed", + }); + expect(cancelled).toMatchObject({ + contentText: "", + processStatus: "available", + processType: "tool.use.completed", + toolCallId: "tool-3", + toolInputDeltaJson: null, + toolInputJson: null, + toolName: "Shell", + toolOutputDeltaText: null, + toolOutputText: null, + toolStatus: "cancelled", + }); + }); + + test("projects message failure as an error boundary without changing streamed text", () => { + const projection = createSessionRuntimeEventProjection( + createRuntimeEvent({ + actor: "driver", + id: "message-failed", + kind: "message.failed", + occurredAt: "2026-05-26T00:00:00.000Z", + origin: "driver", + payload: { + error: { code: "runtime.failed", message: "Provider failed." }, + messageId: "message-1", + role: "agent", + }, + sessionId: "session-1", + }), + ); + + expect(projection).toMatchObject({ + contentText: "Message updated.", + eventType: "message.failed", + family: "message", + processStatus: "error", + processType: "agent.message.delta", }); }); test.each(["{}", '{"command":', '{"cwd":"/workspace"}'])( - "keeps running streamed tool input %s out of the canonical projection", + "preserves running tool input snapshot %s without guessing delta semantics", (rawInput) => { const projection = createSessionRuntimeEventProjection( createRuntimeEvent({ @@ -208,10 +313,36 @@ describe("agent runtime event projection", () => { }), ); - expect(projection.toolInputJson).toBeNull(); + expect(projection).toMatchObject({ + toolInputDeltaJson: null, + toolInputJson: rawInput, + }); }, ); + test("projects running tool input deltas separately from snapshots", () => { + const projection = createSessionRuntimeEventProjection( + createRuntimeEvent({ + actor: "driver", + id: "tool-stream", + kind: "tool.call.updated", + occurredAt: "2026-05-26T00:00:00.000Z", + origin: "driver", + payload: { + rawInputDelta: '{"command":', + status: "running", + toolCallId: "tool-stream", + }, + sessionId: "session-1", + }), + ); + + expect(projection).toMatchObject({ + toolInputDeltaJson: '{"command":', + toolInputJson: null, + }); + }); + test("rejects malformed completed tool output before process projection", () => { const event = createRuntimeEvent({ actor: "driver", @@ -256,8 +387,9 @@ describe("agent runtime event projection", () => { exitCode: 1, }, message: "Runtime failed.", - recoverable: true, + retryable: true, }, + recoverable: true, }, runId: "run-1", sessionId: "session-1", diff --git a/apps/api/tests/api-command-queue.test.ts b/apps/api/tests/api-command-queue.test.ts index 0ab181b1..0d3e1aab 100644 --- a/apps/api/tests/api-command-queue.test.ts +++ b/apps/api/tests/api-command-queue.test.ts @@ -1,21 +1,59 @@ import { describe, expect, test } from "bun:test"; -import { apiCommandsTable } from "@mosoo/db"; +import { apiCommandsTable, appDeploymentRunsTable, appDeploymentsTable } from "@mosoo/db"; +import { createPlatformId } from "@mosoo/id"; +import type { SandboxBackupId } from "@mosoo/id"; +import type { WorkflowStep } from "cloudflare:workers"; import { eq } from "drizzle-orm"; +import { + enqueueAppDeploymentScriptReconciliationCommand, + enqueueSandboxBackupReconciliationCommand, +} from "../src/modules/api-command/application/api-command-enqueue"; import { API_COMMAND_LEASE_MS, API_COMMAND_QUEUE_DELIVERY_PENDING_CODE, API_COMMAND_QUEUE_SEND_FAILED_CODE, admitApiCommand, claimApiCommand, + completeApiCommand, deliverApiCommand, enqueueApiCommand, + getAppDeploymentWorkflowInstanceId, + markApiCommandDeadLettered, + markApiCommandFailed, + prepareApiCommand, + reconcileAppDeploymentWorkflowDeliveries, redriveFailedApiCommandEnqueues, + releaseApiCommandForRetry, renewApiCommandClaim, } from "../src/modules/api-command/application/api-command-ledger"; +import type { ApiCommandClaim } from "../src/modules/api-command/application/api-command-ledger"; import type { ApiCommandMessage } from "../src/modules/api-command/application/api-command-message"; -import { processApiCommandMessage } from "../src/modules/api-command/application/api-command-processor"; +import { parseApiCommandMessage } from "../src/modules/api-command/application/api-command-message"; +import { parseApiCommandPayload } from "../src/modules/api-command/application/api-command-payload"; +import { + APP_DEPLOYMENT_RUN_DISPATCH_MAX_ATTEMPTS, + APP_DEPLOYMENT_RUN_DISPATCH_RETRY_EXHAUSTED_CODE, +} from "../src/modules/api-command/application/api-command-policy"; +import { + cleanupAppDeploymentScript, + deleteAppDeploymentArtifactPrefix, + processApiCommandDeadLetterMessage, + processApiCommandDelivery, + processApiCommandMessage, +} from "../src/modules/api-command/application/api-command-processor"; +import { + APP_DEPLOYMENT_WORKFLOW_EXECUTION_BUDGET_MS, + ApiCommandWorkflowRetryError, + runAppDeploymentWorkflow, +} from "../src/modules/api-command/application/api-command-workflow"; +import { appDeploymentCandidateScriptName } from "../src/modules/apps/application/app-deployment-executor.service"; +import type { AppDeploymentScriptCandidateAuthority } from "../src/modules/apps/application/app-deployment-script-reconciliation.service"; +import { + markAppDeploymentScriptUploadStarted, + registerAppDeploymentScriptCandidate, +} from "../src/modules/apps/application/app-deployment-script-reconciliation.service"; import type { ApiBindings } from "../src/platform/cloudflare/worker-types"; import { createApiCommandQueueStub, @@ -23,10 +61,374 @@ import { createPublicHttpTestBindings, createRecordedQueueMessage, nowMsForTest, + PUBLIC_API_TEST_IDS, } from "./helpers/public-api-http-test-fixture"; +async function claimCommand(input: { + commandId: ApiCommandMessage["commandId"]; + database: D1Database; + deliveryGeneration?: number; + nowMs: number; + claimOwner: string; +}): Promise { + const result = await claimApiCommand({ + ...input, + deliveryGeneration: input.deliveryGeneration ?? 1, + }); + if (result.kind !== "claimed") { + throw new Error(`Expected claimed API command, received ${result.kind}.`); + } + return result.claim; +} + +async function processAppWorkflowDeliveryForTest( + bindings: ApiBindings, + message: Message, + nowMs: () => number, + options: Parameters[3] = {}, +): Promise { + const disposition = await processApiCommandDelivery(bindings, message.body, nowMs, options); + if (disposition.kind === "finished") { + message.ack(); + } else { + message.retry({ delaySeconds: disposition.delaySeconds }); + } +} + +function createInvokingWorkflowStep() { + const calls: { config: unknown; name: string }[] = []; + const step = { + async do(name: string, config: unknown, callback: () => Promise) { + calls.push({ config, name }); + return callback(); + }, + } as unknown as WorkflowStep; + return { calls, step }; +} + +type TestWorkflowStatus = InstanceStatus["status"]; + +function createRecordingAppDeploymentWorkflow( + input: { + createMode?: "created" | "empty" | "throw_after_create" | "throw_before_create"; + initialStatus?: TestWorkflowStatus; + } = {}, +) { + const created: { id: string; params: ApiCommandMessage }[] = []; + const getCalls: string[] = []; + const instances = new Map(); + const restarted: { id: string; options?: WorkflowInstanceRestartOptions }[] = []; + const resumed: string[] = []; + let statusHook: ((id: string) => Promise | void) | undefined; + const workflow = { + async createBatch(batch: { id?: string; params?: ApiCommandMessage }[]) { + if (input.createMode === "throw_before_create") { + throw new Error("Workflow create response failed before acceptance."); + } + const createdInstances = batch.map(({ id, params }) => { + if (id === undefined || params === undefined) { + throw new Error("Expected an explicit Workflow instance ID and params."); + } + created.push({ id, params }); + if (!instances.has(id)) { + instances.set(id, input.initialStatus ?? "queued"); + } + return { id }; + }); + if (input.createMode === "throw_after_create") { + throw new Error("Workflow create response was lost after acceptance."); + } + if (input.createMode === "empty") { + return []; + } + return createdInstances; + }, + async get(id: string) { + getCalls.push(id); + if (!instances.has(id)) { + throw new Error("Workflow instance does not exist."); + } + return { + id, + async restart(options?: WorkflowInstanceRestartOptions) { + restarted.push({ id, options }); + }, + async resume() { + resumed.push(id); + }, + async status() { + await statusHook?.(id); + return { status: instances.get(id) ?? "unknown" }; + }, + }; + }, + } as unknown as Workflow; + return { + created, + deleteInstance: (id: string) => instances.delete(id), + getCalls, + instances, + restarted, + resumed, + setStatus: (id: string, status: TestWorkflowStatus) => instances.set(id, status), + setStatusHook: (hook: (id: string) => Promise | void) => { + statusHook = hook; + }, + workflow, + }; +} + +async function createAppDeploymentCommandFixture(input: { + errorCode?: string; + errorMessage?: string; + runStatus: + | "activating" + | "building" + | "failed" + | "preparing" + | "queued" + | "submitted" + | "submitting"; + workflow?: ReturnType; +}) { + const database = await createPublicHttpContractDatabase(); + const queue = createApiCommandQueueStub(); + const appDeploymentWorkflow = input.workflow ?? createRecordingAppDeploymentWorkflow(); + const bindings = createPublicHttpTestBindings(database, { + apiCommandQueue: queue, + }) as ApiBindings; + bindings.APP_DEPLOYMENT_WORKFLOW = appDeploymentWorkflow.workflow; + const deploymentId = PUBLIC_API_TEST_IDS.deployment; + const runId = PUBLIC_API_TEST_IDS.run; + + await database + .app() + .insert(appDeploymentsTable) + .values({ + appId: PUBLIC_API_TEST_IDS.app, + createdAt: nowMsForTest(), + defaultBranch: "main", + id: deploymentId, + latestRunId: runId, + mosooSubdomain: "api-command-test", + ownerAccountId: PUBLIC_API_TEST_IDS.ownerAccount, + repoName: "repo", + repoOwner: "owner", + repoUrl: "https://github.com/owner/repo", + sourceKind: "github_public", + updatedAt: nowMsForTest(), + }) + .run(); + await database + .app() + .insert(appDeploymentRunsTable) + .values({ + appId: PUBLIC_API_TEST_IDS.app, + createdAt: nowMsForTest(), + deploymentId, + errorCode: input.errorCode ?? null, + errorMessage: input.errorMessage ?? null, + id: runId, + sourceBranch: "main", + sourceCommitSha: "a".repeat(40), + status: input.runStatus, + updatedAt: nowMsForTest(), + }) + .run(); + const commandId = await enqueueApiCommand(bindings, { + dedupeKey: `app_deployment_run_dispatch:${runId}`, + kind: "app_deployment_run_dispatch", + payload: { appDeploymentRunId: runId }, + }); + const body = appDeploymentWorkflow.created[0]?.params ?? { + commandId, + deliveryGeneration: 1, + }; + + return { + appDeploymentWorkflow, + bindings, + body, + commandId, + database, + deploymentId, + queue, + runId, + }; +} + +async function registerAppDeploymentCandidateForTest( + fixture: Awaited>, + authority: AppDeploymentScriptCandidateAuthority, + nowMs: number, + readyForOutcome = false, +): Promise { + const scriptName = appDeploymentCandidateScriptName(authority); + await registerAppDeploymentScriptCandidate(fixture.database, { + authority, + deploymentId: fixture.deploymentId, + nowMs, + runId: fixture.runId, + scriptName, + }); + if (readyForOutcome) { + await markAppDeploymentScriptUploadStarted(fixture.database, { + authority, + deploymentId: fixture.deploymentId, + nowMs, + runId: fixture.runId, + scriptName, + }); + await fixture.database + .app() + .update(appDeploymentRunsTable) + .set({ status: "activating" }) + .where(eq(appDeploymentRunsTable.id, fixture.runId)) + .run(); + } + return scriptName; +} + describe("API command queue", () => { - test("dedupes producer-side and sends only the command id", async () => { + test("requires a positive safe delivery generation in every queue message", () => { + const commandId = "01J0000000000000000000000C"; + + expect(parseApiCommandMessage({ commandId, deliveryGeneration: 1 })).toEqual({ + commandId, + deliveryGeneration: 1, + }); + expect( + parseApiCommandMessage({ commandId, deliveryGeneration: Number.MAX_SAFE_INTEGER }), + ).toEqual({ commandId, deliveryGeneration: Number.MAX_SAFE_INTEGER }); + + for (const deliveryGeneration of [undefined, 0, 1.5, Number.MAX_SAFE_INTEGER + 1]) { + expect(() => parseApiCommandMessage({ commandId, deliveryGeneration })).toThrow(); + } + }); + + test("keeps the D1 backup page independent from the opaque R2 cursor", () => { + const payload = { cursor: null, databasePage: 0, scheduledTime: nowMsForTest() }; + expect( + parseApiCommandPayload("sandbox_backup_reconciliation", JSON.stringify(payload)), + ).toEqual(payload); + for (const databasePage of [-1, 0.5, Number.MAX_SAFE_INTEGER + 1]) { + expect(() => + parseApiCommandPayload( + "sandbox_backup_reconciliation", + JSON.stringify({ ...payload, databasePage }), + ), + ).toThrow(); + } + }); + + test("continues a D1 backup backlog without an R2 cursor", async () => { + const database = await createPublicHttpContractDatabase(); + const queue = createApiCommandQueueStub(); + const bucket = { + async delete(): Promise {}, + async list(): Promise { + return { delimitedPrefixes: [], objects: [], truncated: false }; + }, + } as unknown as R2Bucket; + const bindings = { + ...createPublicHttpTestBindings(database, { apiCommandQueue: queue }), + SANDBOX_STATE_BUCKET: bucket, + } as ApiBindings; + database.execute("DROP TRIGGER sandbox_backup_delete_intent_authority"); + await database.batch( + Array.from({ length: 65 }, () => + database + .prepare( + `INSERT INTO sandbox_backup_delete_intent ( + attempted_at, backup_id, created_at, delete_after, deleted_at + ) VALUES (NULL, ?, 0, 0, NULL)`, + ) + .bind(createPlatformId()), + ), + ); + const scheduledTime = nowMsForTest(); + await enqueueSandboxBackupReconciliationCommand(bindings, { + cursor: null, + databasePage: 0, + scheduledTime, + }); + + const first = createRecordedQueueMessage({ body: queue.sent[0].body }); + await processApiCommandMessage(bindings, first.message, nowMsForTest); + + expect(first.recorded).toEqual([{ type: "ack" }]); + expect(queue.sent).toHaveLength(2); + const rows = await database + .app() + .select({ dedupeKey: apiCommandsTable.dedupeKey, payloadJson: apiCommandsTable.payloadJson }) + .from(apiCommandsTable) + .orderBy(apiCommandsTable.dedupeKey) + .all(); + expect(rows.map(({ dedupeKey }) => dedupeKey)).toEqual([ + `sandbox_backup_reconciliation:${scheduledTime}:0:start`, + `sandbox_backup_reconciliation:${scheduledTime}:1:start`, + ]); + expect(parseApiCommandPayload("sandbox_backup_reconciliation", rows[1].payloadJson)).toEqual({ + cursor: null, + databasePage: 1, + scheduledTime, + }); + + const second = createRecordedQueueMessage({ body: queue.sent[1].body }); + await processApiCommandMessage(bindings, second.message, nowMsForTest); + + expect(second.recorded).toEqual([{ type: "ack" }]); + expect(queue.sent).toHaveLength(2); + await expect( + database + .prepare( + "SELECT count(*) AS count FROM sandbox_backup_delete_intent WHERE deleted_at IS NOT NULL", + ) + .first(), + ).resolves.toEqual({ count: 65 }); + }); + + test("enqueues and parses an App deployment script reconciliation page", async () => { + const database = await createPublicHttpContractDatabase(); + const queue = createApiCommandQueueStub(); + const bindings = createPublicHttpTestBindings(database, { + apiCommandQueue: queue, + }) as ApiBindings; + const scheduledTime = Date.UTC(2026, 7, 30, 4); + + await enqueueAppDeploymentScriptReconciliationCommand(bindings, { + cursor: null, + scheduledTime, + }); + + const [row] = await database + .app() + .select({ + dedupeKey: apiCommandsTable.dedupeKey, + id: apiCommandsTable.id, + kind: apiCommandsTable.kind, + payloadJson: apiCommandsTable.payloadJson, + }) + .from(apiCommandsTable) + .all(); + expect(row).toMatchObject({ + dedupeKey: `app-deployment-script-reconciliation:${scheduledTime}:root`, + kind: "app_deployment_script_reconciliation", + }); + expect(queue.sent[0]?.body).toEqual({ commandId: row?.id, deliveryGeneration: 1 }); + expect(parseApiCommandPayload(row.kind, row.payloadJson)).toEqual({ + cursor: null, + scheduledTime, + }); + expect(() => + parseApiCommandPayload( + "app_deployment_script_reconciliation", + JSON.stringify({ cursor: "", scheduledTime }), + ), + ).toThrow("cursor must not be empty"); + }); + + test("dedupes producer-side and sends the durable delivery generation", async () => { const database = await createPublicHttpContractDatabase(); const queue = createApiCommandQueueStub(); const bindings = createPublicHttpTestBindings(database, { @@ -47,7 +449,7 @@ describe("API command queue", () => { expect(duplicateId).toBe(firstId); expect(queue.sent).toEqual([ { - body: { commandId: firstId }, + body: { commandId: firstId, deliveryGeneration: 1 }, contentType: "json", delaySeconds: null, id: "queued-1", @@ -91,7 +493,649 @@ describe("API command queue", () => { }); await deliverApiCommand(bindings, admission); - expect(queue.sent[0]?.body).toEqual({ commandId: admission.commandId }); + expect(queue.sent[0]?.body).toEqual({ + commandId: admission.commandId, + deliveryGeneration: 1, + }); + }); + + test("routes App deployment commands to one fixed Workflow instance", async () => { + const fixture = await createAppDeploymentCommandFixture({ runStatus: "queued" }); + + expect(fixture.queue.sent).toEqual([]); + expect(fixture.appDeploymentWorkflow.created).toEqual([ + { + id: getAppDeploymentWorkflowInstanceId(fixture.commandId, 1), + params: { commandId: fixture.commandId, deliveryGeneration: 1 }, + }, + ]); + }); + + test("runs App deployment in one bounded Workflow step with one absolute deadline", async () => { + const fixture = await createAppDeploymentCommandFixture({ runStatus: "queued" }); + const workflowStep = createInvokingWorkflowStep(); + const stepStartedAt = nowMsForTest() + 100; + let nowCallCount = 0; + let receivedDeadlineMs: number | undefined; + + await runAppDeploymentWorkflow( + fixture.bindings, + fixture.body, + workflowStep.step, + () => { + nowCallCount += 1; + return nowCallCount === 1 ? stepStartedAt : stepStartedAt + 5_000; + }, + { + dispatchAppDeploymentRun: async (_bindings, _payload, authority, options) => { + receivedDeadlineMs = options?.deadlineMs; + await authority.requireOwnership(); + return { + errorCode: "workflow_test_terminal", + errorMessage: "The Workflow test completed without an external deployment.", + kind: "terminal_failure", + runId: fixture.runId, + }; + }, + }, + ); + + expect(workflowStep.calls).toEqual([ + { + config: { + retries: { backoff: "constant", delay: "30 seconds", limit: 1_000 }, + timeout: "30 minutes", + }, + name: "dispatch", + }, + ]); + expect(receivedDeadlineMs).toBe(stepStartedAt + APP_DEPLOYMENT_WORKFLOW_EXECUTION_BUDGET_MS); + }); + + test("throws the dedicated Workflow retry error while the D1 claim is busy", async () => { + const fixture = await createAppDeploymentCommandFixture({ runStatus: "queued" }); + const claimedAt = nowMsForTest() + 100; + await claimCommand({ + claimOwner: "workflow-busy-owner", + commandId: fixture.commandId, + database: fixture.database, + nowMs: claimedAt, + }); + + await expect( + runAppDeploymentWorkflow( + fixture.bindings, + fixture.body, + createInvokingWorkflowStep().step, + () => claimedAt + 1, + ), + ).rejects.toBeInstanceOf(ApiCommandWorkflowRetryError); + }); + + test("hands residual main and DLQ App messages to Workflow without dispatching", async () => { + const fixture = await createAppDeploymentCommandFixture({ runStatus: "queued" }); + const main = createRecordedQueueMessage({ body: fixture.body }); + const deadLetter = createRecordedQueueMessage({ body: fixture.body }); + let dispatchCount = 0; + + await processApiCommandMessage(fixture.bindings, main.message, nowMsForTest, { + dispatchAppDeploymentRun: async () => { + dispatchCount += 1; + return { kind: "skipped" }; + }, + }); + await processApiCommandDeadLetterMessage(fixture.bindings, deadLetter.message, nowMsForTest); + + expect(dispatchCount).toBe(0); + expect(main.recorded).toEqual([{ type: "ack" }]); + expect(deadLetter.recorded).toEqual([{ type: "ack" }]); + expect(fixture.appDeploymentWorkflow.created.map(({ id }) => id)).toEqual([ + getAppDeploymentWorkflowInstanceId(fixture.commandId, 1), + getAppDeploymentWorkflowInstanceId(fixture.commandId, 1), + getAppDeploymentWorkflowInstanceId(fixture.commandId, 1), + ]); + }); + + test("retries a residual App message when Workflow handoff is ambiguous", async () => { + const workflow = createRecordingAppDeploymentWorkflow({ + createMode: "throw_after_create", + }); + const fixture = await createAppDeploymentCommandFixture({ runStatus: "queued", workflow }); + const recorded = createRecordedQueueMessage({ body: fixture.body }); + let dispatchCount = 0; + + await processApiCommandMessage(fixture.bindings, recorded.message, nowMsForTest, { + dispatchAppDeploymentRun: async () => { + dispatchCount += 1; + return { kind: "skipped" }; + }, + }); + + expect(dispatchCount).toBe(0); + expect(recorded.recorded).toEqual([{ delaySeconds: 30, type: "retry" }]); + }); + + test.each(["missing", "stale", "terminal"] as const)( + "acks a %s residual App message without Workflow handoff", + async (state) => { + const fixture = await createAppDeploymentCommandFixture({ runStatus: "queued" }); + if (state === "missing") { + await fixture.database + .app() + .delete(apiCommandsTable) + .where(eq(apiCommandsTable.id, fixture.commandId)) + .run(); + } else { + await fixture.database + .app() + .update(apiCommandsTable) + .set(state === "stale" ? { deliveryGeneration: 2 } : { status: "failed" }) + .where(eq(apiCommandsTable.id, fixture.commandId)) + .run(); + } + const recorded = createRecordedQueueMessage({ body: fixture.body }); + + await processApiCommandMessage(fixture.bindings, recorded.message, nowMsForTest); + + expect(recorded.recorded).toEqual([{ type: "ack" }]); + expect(fixture.appDeploymentWorkflow.created).toHaveLength(1); + }, + ); + + test("collects every paginated artifact key before deleting the prefix", async () => { + const remaining = new Set(["apps/a/one", "apps/a/two", "apps/a/three", "other"]); + const listCursors: Array = []; + const deleted: string[][] = []; + const bucket = { + async delete(keys: string | string[]) { + const page = typeof keys === "string" ? [keys] : keys; + deleted.push(page); + for (const key of page) { + remaining.delete(key); + } + }, + async list(options: R2ListOptions) { + listCursors.push(options.cursor); + const keys = options.cursor === undefined ? ["apps/a/one", "apps/a/two"] : ["apps/a/three"]; + return { + cursor: options.cursor === undefined ? "next" : undefined, + delimitedPrefixes: [], + objects: keys.map((key) => ({ key })), + truncated: options.cursor === undefined, + } as unknown as R2Objects; + }, + } as unknown as R2Bucket; + + await deleteAppDeploymentArtifactPrefix(bucket, "apps/a/"); + + expect(listCursors).toEqual([undefined, "next"]); + expect(deleted).toEqual([["apps/a/one", "apps/a/two", "apps/a/three"]]); + expect([...remaining]).toEqual(["other"]); + }); + + test("fails artifact cleanup before deleting when a truncated page has no cursor", async () => { + let deleteCount = 0; + const bucket = { + async delete() { + deleteCount += 1; + }, + async list() { + return { + delimitedPrefixes: [], + objects: [{ key: "apps/a/one" }], + truncated: true, + } as unknown as R2Objects; + }, + } as unknown as R2Bucket; + + await expect(deleteAppDeploymentArtifactPrefix(bucket, "apps/a/")).rejects.toThrow( + "no continuation cursor", + ); + expect(deleteCount).toBe(0); + }); + + test("stops both artifact producers before deleting their outputs", async () => { + const database = await createPublicHttpContractDatabase(); + const bindings = createPublicHttpTestBindings(database) as ApiBindings; + const buildDestroy = Promise.withResolvers(); + const deployDestroy = Promise.withResolvers(); + const bothDestroying = Promise.withResolvers(); + let destroying = 0; + let listCount = 0; + bindings.runtimeSubjectHandleFactory = (sandboxId) => + new Proxy( + {}, + { + get(_target, property) { + if (typeof property === "symbol" || property === "then") { + return undefined; + } + if (property === "setKeepAlive") { + return async () => {}; + } + if (property === "destroy") { + return async () => { + destroying += 1; + if (destroying === 2) { + bothDestroying.resolve(); + } + await (sandboxId === "build-sandbox" + ? buildDestroy.promise + : deployDestroy.promise); + }; + } + return async () => { + throw new Error(`Unexpected sandbox call: ${property}`); + }; + }, + }, + ); + bindings.FILE_BUCKET = { + async delete() {}, + async list() { + listCount += 1; + return { delimitedPrefixes: [], objects: [], truncated: false }; + }, + }; + const commandId = prepareApiCommand({ + dedupeKey: "app-script-cleanup:fence", + kind: "app_deployment_run_dispatch", + payload: { appDeploymentRunId: PUBLIC_API_TEST_IDS.run }, + }).commandId; + + const cleanup = cleanupAppDeploymentScript(bindings, { + artifactPrefix: "apps/script/", + attemptCount: 1, + buildSandboxId: "build-sandbox", + commandId, + deliveryGeneration: 1, + deploySandboxId: "deploy-sandbox", + deploymentId: PUBLIC_API_TEST_IDS.deployment, + reconcileCount: 0, + runId: PUBLIC_API_TEST_IDS.run, + scriptName: "script", + uploadStartedAt: null, + }); + await bothDestroying.promise; + expect(listCount).toBe(0); + + buildDestroy.resolve(); + await Promise.resolve(); + expect(listCount).toBe(0); + + deployDestroy.resolve(); + await cleanup; + expect(listCount).toBe(1); + }); + + test("does not delete outputs when either artifact producer cannot stop", async () => { + const database = await createPublicHttpContractDatabase(); + const bindings = createPublicHttpTestBindings(database) as ApiBindings; + let listCount = 0; + bindings.runtimeSubjectHandleFactory = (sandboxId) => + new Proxy( + {}, + { + get(_target, property) { + if (typeof property === "symbol" || property === "then") { + return undefined; + } + if (property === "setKeepAlive") { + return async () => {}; + } + if (property === "destroy") { + return async () => { + if (sandboxId === "build-sandbox") { + throw new Error("build sandbox is still producing artifacts"); + } + }; + } + return async () => { + throw new Error(`Unexpected sandbox call: ${property}`); + }; + }, + }, + ); + bindings.FILE_BUCKET = { + async delete() {}, + async list() { + listCount += 1; + return { delimitedPrefixes: [], objects: [], truncated: false }; + }, + }; + const commandId = prepareApiCommand({ + dedupeKey: "app-script-cleanup:producer-failure", + kind: "app_deployment_run_dispatch", + payload: { appDeploymentRunId: PUBLIC_API_TEST_IDS.run }, + }).commandId; + + await expect( + cleanupAppDeploymentScript(bindings, { + artifactPrefix: "apps/script/", + attemptCount: 1, + buildSandboxId: "build-sandbox", + commandId, + deliveryGeneration: 1, + deploySandboxId: "deploy-sandbox", + deploymentId: PUBLIC_API_TEST_IDS.deployment, + reconcileCount: 0, + runId: PUBLIC_API_TEST_IDS.run, + scriptName: "script", + uploadStartedAt: null, + }), + ).rejects.toBeInstanceOf(AggregateError); + expect(listCount).toBe(0); + }); + + test.each([ + "complete", + "errored", + "paused", + "queued", + "running", + "terminated", + "waiting", + "waitingForPause", + ] as const)("redrives an ambiguous Workflow create after an ACK loss in %s", async (status) => { + const workflow = createRecordingAppDeploymentWorkflow({ + createMode: "throw_after_create", + initialStatus: status, + }); + const fixture = await createAppDeploymentCommandFixture({ runStatus: "queued", workflow }); + + await expect( + fixture.database + .app() + .select({ lastErrorCode: apiCommandsTable.lastErrorCode }) + .from(apiCommandsTable) + .where(eq(apiCommandsTable.id, fixture.commandId)) + .get(), + ).resolves.toEqual({ lastErrorCode: API_COMMAND_QUEUE_SEND_FAILED_CODE }); + expect(workflow.getCalls).toEqual([]); + }); + + test("confirms a duplicate Workflow create that resolves with an empty batch", async () => { + const workflow = createRecordingAppDeploymentWorkflow({ createMode: "empty" }); + const fixture = await createAppDeploymentCommandFixture({ runStatus: "queued", workflow }); + + await expect( + fixture.database + .app() + .select({ lastErrorCode: apiCommandsTable.lastErrorCode }) + .from(apiCommandsTable) + .where(eq(apiCommandsTable.id, fixture.commandId)) + .get(), + ).resolves.toEqual({ lastErrorCode: null }); + expect(workflow.getCalls).toEqual([]); + }); + + test.each([ + { createMode: "throw_before_create", initialStatus: "queued" }, + { createMode: "throw_after_create", initialStatus: "unknown" }, + ] as const)("keeps an unconfirmed Workflow delivery pending: %o", async (workflowInput) => { + const workflow = createRecordingAppDeploymentWorkflow(workflowInput); + const fixture = await createAppDeploymentCommandFixture({ runStatus: "queued", workflow }); + + await expect( + fixture.database + .app() + .select({ lastErrorCode: apiCommandsTable.lastErrorCode }) + .from(apiCommandsTable) + .where(eq(apiCommandsTable.id, fixture.commandId)) + .get(), + ).resolves.toEqual({ lastErrorCode: API_COMMAND_QUEUE_SEND_FAILED_CODE }); + }); + + test.each(["queued", "running", "waiting"] as const)( + "leaves an active %s Workflow instance alone", + async (status) => { + const fixture = await createAppDeploymentCommandFixture({ runStatus: "queued" }); + const instanceId = getAppDeploymentWorkflowInstanceId(fixture.commandId, 1); + fixture.appDeploymentWorkflow.setStatus(instanceId, status); + + await reconcileAppDeploymentWorkflowDeliveries(fixture.bindings, nowMsForTest() + 1); + + expect(fixture.appDeploymentWorkflow.resumed).toEqual([]); + expect(fixture.appDeploymentWorkflow.restarted).toEqual([]); + await expect( + fixture.database + .app() + .select({ deliveryGeneration: apiCommandsTable.deliveryGeneration }) + .from(apiCommandsTable) + .where(eq(apiCommandsTable.id, fixture.commandId)) + .get(), + ).resolves.toEqual({ deliveryGeneration: 1 }); + }, + ); + + test.each(["paused", "waitingForPause"] as const)( + "resumes a %s Workflow because D1 remains active", + async (status) => { + const fixture = await createAppDeploymentCommandFixture({ runStatus: "queued" }); + const instanceId = getAppDeploymentWorkflowInstanceId(fixture.commandId, 1); + fixture.appDeploymentWorkflow.setStatus(instanceId, status); + + await reconcileAppDeploymentWorkflowDeliveries(fixture.bindings, nowMsForTest() + 1); + + expect(fixture.appDeploymentWorkflow.resumed).toEqual([instanceId]); + }, + ); + + test.each(["complete", "errored", "terminated"] as const)( + "restarts an unexpected %s Workflow from the beginning", + async (status) => { + const fixture = await createAppDeploymentCommandFixture({ runStatus: "queued" }); + const instanceId = getAppDeploymentWorkflowInstanceId(fixture.commandId, 1); + fixture.appDeploymentWorkflow.setStatus(instanceId, status); + + await reconcileAppDeploymentWorkflowDeliveries(fixture.bindings, nowMsForTest() + 1); + + expect(fixture.appDeploymentWorkflow.restarted).toEqual([ + { id: instanceId, options: undefined }, + ]); + }, + ); + + test("re-ensures the same Workflow ID when status is unknown", async () => { + const fixture = await createAppDeploymentCommandFixture({ runStatus: "queued" }); + const instanceId = getAppDeploymentWorkflowInstanceId(fixture.commandId, 1); + fixture.appDeploymentWorkflow.setStatus(instanceId, "unknown"); + + await reconcileAppDeploymentWorkflowDeliveries(fixture.bindings, nowMsForTest() + 1); + + expect(fixture.appDeploymentWorkflow.created.map(({ id }) => id)).toEqual([ + instanceId, + instanceId, + ]); + }); + + test("re-ensures the same Workflow ID when status cannot be observed", async () => { + const fixture = await createAppDeploymentCommandFixture({ runStatus: "queued" }); + const instanceId = getAppDeploymentWorkflowInstanceId(fixture.commandId, 1); + fixture.appDeploymentWorkflow.setStatusHook(() => { + throw new Error("Workflow status is unavailable."); + }); + + await reconcileAppDeploymentWorkflowDeliveries(fixture.bindings, nowMsForTest() + 1); + + expect(fixture.appDeploymentWorkflow.created.map(({ id }) => id)).toEqual([ + instanceId, + instanceId, + ]); + await expect( + fixture.database + .app() + .select({ deliveryGeneration: apiCommandsTable.deliveryGeneration }) + .from(apiCommandsTable) + .where(eq(apiCommandsTable.id, fixture.commandId)) + .get(), + ).resolves.toEqual({ deliveryGeneration: 1 }); + }); + + test("recreates a missing Workflow with the same generation and attempt budget", async () => { + const fixture = await createAppDeploymentCommandFixture({ runStatus: "queued" }); + const instanceId = getAppDeploymentWorkflowInstanceId(fixture.commandId, 1); + await fixture.database + .app() + .update(apiCommandsTable) + .set({ attemptCount: 2 }) + .where(eq(apiCommandsTable.id, fixture.commandId)) + .run(); + + for (const nowMs of [nowMsForTest() + 1, nowMsForTest() + 2]) { + fixture.appDeploymentWorkflow.deleteInstance(instanceId); + await reconcileAppDeploymentWorkflowDeliveries(fixture.bindings, nowMs); + } + + expect(fixture.appDeploymentWorkflow.created.map(({ id }) => id)).toEqual([ + instanceId, + instanceId, + instanceId, + ]); + await expect( + fixture.database + .app() + .select({ + attemptCount: apiCommandsTable.attemptCount, + deliveryGeneration: apiCommandsTable.deliveryGeneration, + }) + .from(apiCommandsTable) + .where(eq(apiCommandsTable.id, fixture.commandId)) + .get(), + ).resolves.toEqual({ attemptCount: 2, deliveryGeneration: 1 }); + }); + + test("terminalizes attempt four after restarting a terminated Workflow", async () => { + const fixture = await createAppDeploymentCommandFixture({ runStatus: "activating" }); + await fixture.database + .app() + .update(apiCommandsTable) + .set({ attemptCount: APP_DEPLOYMENT_RUN_DISPATCH_MAX_ATTEMPTS }) + .where(eq(apiCommandsTable.id, fixture.commandId)) + .run(); + fixture.appDeploymentWorkflow.setStatus( + getAppDeploymentWorkflowInstanceId(fixture.commandId, 1), + "terminated", + ); + + await reconcileAppDeploymentWorkflowDeliveries(fixture.bindings, nowMsForTest() + 1); + expect(fixture.appDeploymentWorkflow.restarted).toEqual([ + { + id: getAppDeploymentWorkflowInstanceId(fixture.commandId, 1), + options: undefined, + }, + ]); + const recorded = createRecordedQueueMessage({ body: fixture.body }); + let dispatchCount = 0; + await processAppWorkflowDeliveryForTest( + fixture.bindings, + recorded.message, + () => nowMsForTest() + 2, + { + dispatchAppDeploymentRun: async () => { + dispatchCount += 1; + return { kind: "skipped" }; + }, + }, + ); + + expect(dispatchCount).toBe(0); + expect(recorded.recorded).toEqual([{ type: "ack" }]); + await expect( + fixture.database + .app() + .select({ + attemptCount: apiCommandsTable.attemptCount, + deliveryGeneration: apiCommandsTable.deliveryGeneration, + status: apiCommandsTable.status, + }) + .from(apiCommandsTable) + .where(eq(apiCommandsTable.id, fixture.commandId)) + .get(), + ).resolves.toEqual({ + attemptCount: APP_DEPLOYMENT_RUN_DISPATCH_MAX_ATTEMPTS + 1, + deliveryGeneration: 1, + status: "failed", + }); + }); + + test("does not inspect a Workflow while its D1 claim lease is live", async () => { + const fixture = await createAppDeploymentCommandFixture({ runStatus: "queued" }); + await claimCommand({ + claimOwner: "live-workflow-owner", + commandId: fixture.commandId, + database: fixture.database, + nowMs: nowMsForTest(), + }); + + await reconcileAppDeploymentWorkflowDeliveries(fixture.bindings, nowMsForTest() + 1); + + expect(fixture.appDeploymentWorkflow.getCalls).toEqual([]); + }); + + test("round-robins a bounded Workflow reconciliation page without starvation", async () => { + const database = await createPublicHttpContractDatabase(); + const workflow = createRecordingAppDeploymentWorkflow(); + const bindings = createPublicHttpTestBindings(database) as ApiBindings; + bindings.APP_DEPLOYMENT_WORKFLOW = workflow.workflow; + const records = Array.from({ length: 201 }, (_, index) => + prepareApiCommand( + { + dedupeKey: `app_deployment_run_dispatch:fairness:${index}`, + kind: "app_deployment_run_dispatch", + payload: { appDeploymentRunId: PUBLIC_API_TEST_IDS.run }, + }, + { timestampMs: 100 }, + ), + ); + for (let offset = 0; offset < records.length; offset += 40) { + await database + .app() + .insert(apiCommandsTable) + .values(records.slice(offset, offset + 40).map(({ record }) => record)) + .run(); + } + for (const { commandId } of records) { + workflow.instances.set(getAppDeploymentWorkflowInstanceId(commandId, 1), "queued"); + } + + for (const nowMs of [2, 3, 4]) { + await reconcileAppDeploymentWorkflowDeliveries(bindings, nowMs); + } + + expect(workflow.getCalls).toHaveLength(300); + expect(new Set(workflow.getCalls).size).toBe(201); + }); + + test("does not move the reconciliation cursor after a concurrent generation change", async () => { + const fixture = await createAppDeploymentCommandFixture({ runStatus: "queued" }); + const concurrentUpdatedAt = nowMsForTest() + 7; + fixture.appDeploymentWorkflow.setStatusHook(async () => { + await fixture.database + .app() + .update(apiCommandsTable) + .set({ + deliveryGeneration: 2, + status: "succeeded", + updatedAt: concurrentUpdatedAt, + }) + .where(eq(apiCommandsTable.id, fixture.commandId)) + .run(); + }); + + await reconcileAppDeploymentWorkflowDeliveries(fixture.bindings, nowMsForTest() + 100); + + await expect( + fixture.database + .app() + .select({ + deliveryGeneration: apiCommandsTable.deliveryGeneration, + updatedAt: apiCommandsTable.updatedAt, + }) + .from(apiCommandsTable) + .where(eq(apiCommandsTable.id, fixture.commandId)) + .get(), + ).resolves.toEqual({ deliveryGeneration: 2, updatedAt: concurrentUpdatedAt }); }); test("marks malformed payload commands failed and acks the message", async () => { @@ -132,6 +1176,40 @@ describe("API command queue", () => { expect(recorded.recorded).toEqual([{ type: "ack" }]); }); + test("terminalizes a malformed Environment main-lane command before ACK", async () => { + const database = await createPublicHttpContractDatabase(); + const apiQueue = createApiCommandQueueStub(); + const environmentQueue = createApiCommandQueueStub(); + const bindings = { + ...createPublicHttpTestBindings(database, { apiCommandQueue: apiQueue }), + ENVIRONMENT_ARTIFACT_BUILD_QUEUE: environmentQueue, + } as ApiBindings; + const commandId = await enqueueApiCommand(bindings, { + dedupeKey: "environment-package-artifact:malformed", + kind: "environment_package_artifact_build", + payload: {}, + }); + const queued = environmentQueue.sent[0]?.body; + if (!queued) throw new Error("Expected an Environment artifact Queue message."); + const recorded = createRecordedQueueMessage({ body: queued }); + + await processApiCommandMessage(bindings, recorded.message, nowMsForTest); + + expect(apiQueue.sent).toHaveLength(0); + expect(recorded.recorded).toEqual([{ type: "ack" }]); + await expect( + database + .app() + .select({ + lastErrorCode: apiCommandsTable.lastErrorCode, + status: apiCommandsTable.status, + }) + .from(apiCommandsTable) + .where(eq(apiCommandsTable.id, commandId)) + .get(), + ).resolves.toEqual({ lastErrorCode: "invalid_payload", status: "failed" }); + }); + test("keeps a command claimable when Queue accepts it but send reports a timeout", async () => { const database = await createPublicHttpContractDatabase(); const retainedMessages: ApiCommandMessage[] = []; @@ -173,12 +1251,16 @@ describe("API command queue", () => { expect(commandId).toBe(retained.commandId); await expect( claimApiCommand({ + claimOwner: "consumer-after-timeout", commandId: retained.commandId, database, + deliveryGeneration: retained.deliveryGeneration, nowMs: nowMsForTest(), - ownerId: "consumer-after-timeout", }), - ).resolves.toMatchObject({ commandId: retained.commandId }); + ).resolves.toMatchObject({ + claim: { commandId: retained.commandId }, + kind: "claimed", + }); }); test("redrives a durable command after a definite Queue send failure", async () => { @@ -266,7 +1348,7 @@ describe("API command queue", () => { .get(); expect(queue.sent).toHaveLength(1); - expect(queue.sent[0]?.body).toEqual({ commandId }); + expect(queue.sent[0]?.body).toEqual({ commandId, deliveryGeneration: 1 }); expect(row).toEqual({ lastErrorCode: null, lastErrorMessage: null }); }); @@ -282,19 +1364,18 @@ describe("API command queue", () => { payload: { scheduledTime: nowMsForTest() }, }); - await claimApiCommand({ + const claim = await claimCommand({ + claimOwner: "owner-1", commandId, database, nowMs: 1_000, - ownerId: "owner-1", }); await expect( renewApiCommandClaim({ - commandId, + claim, database, nowMs: 2_000, - ownerId: "owner-1", }), ).resolves.toBe(true); @@ -309,4 +1390,942 @@ describe("API command queue", () => { expect(row?.claimExpiresAt).toBe(2_000 + API_COMMAND_LEASE_MS); }); + + test("classifies claimed, busy, stale, terminal, and missing deliveries", async () => { + const database = await createPublicHttpContractDatabase(); + const queue = createApiCommandQueueStub(); + const bindings = createPublicHttpTestBindings(database, { + apiCommandQueue: queue, + }) as ApiBindings; + const commandId = await enqueueApiCommand(bindings, { + dedupeKey: "scheduled:claim-dispositions", + kind: "scheduled_maintenance", + payload: { scheduledTime: nowMsForTest() }, + }); + const claimed = await claimApiCommand({ + claimOwner: "owner-a", + commandId, + database, + deliveryGeneration: 1, + nowMs: 1_000, + }); + + expect(claimed).toMatchObject({ + claim: { attemptCount: 1, claimOwner: "owner-a", commandId, deliveryGeneration: 1 }, + kind: "claimed", + }); + await expect( + claimApiCommand({ + claimOwner: "owner-b", + commandId, + database, + deliveryGeneration: 1, + nowMs: 2_000, + }), + ).resolves.toEqual({ + claimExpiresAt: 1_000 + API_COMMAND_LEASE_MS, + kind: "busy", + }); + await expect( + claimApiCommand({ + claimOwner: "owner-b", + commandId, + database, + deliveryGeneration: 2, + nowMs: 2_000, + }), + ).resolves.toEqual({ kind: "stale" }); + + if (claimed.kind !== "claimed") { + throw new Error("Expected claimed API command."); + } + await completeApiCommand({ claim: claimed.claim, database, nowMs: 2_000 }); + await expect( + claimApiCommand({ + claimOwner: "owner-b", + commandId, + database, + deliveryGeneration: 1, + nowMs: 3_000, + }), + ).resolves.toEqual({ kind: "terminal" }); + await expect( + claimApiCommand({ + claimOwner: "owner-b", + commandId: "01J0000000000000000000000Z", + database, + deliveryGeneration: 1, + nowMs: 3_000, + }), + ).resolves.toEqual({ kind: "missing" }); + }); + + test("fences completion at expiry and lets the next attempt take over", async () => { + const database = await createPublicHttpContractDatabase(); + const queue = createApiCommandQueueStub(); + const bindings = createPublicHttpTestBindings(database, { + apiCommandQueue: queue, + }) as ApiBindings; + const commandId = await enqueueApiCommand(bindings, { + dedupeKey: "scheduled:expiry-boundary", + kind: "scheduled_maintenance", + payload: { scheduledTime: nowMsForTest() }, + }); + const first = await claimCommand({ + claimOwner: "owner-a", + commandId, + database, + nowMs: 1_000, + }); + const expiresAt = 1_000 + API_COMMAND_LEASE_MS; + + await expect(completeApiCommand({ claim: first, database, nowMs: expiresAt })).resolves.toBe( + false, + ); + await expect( + claimApiCommand({ + claimOwner: "owner-b", + commandId, + database, + deliveryGeneration: 1, + nowMs: expiresAt, + }), + ).resolves.toMatchObject({ + claim: { attemptCount: 2, claimOwner: "owner-b" }, + kind: "claimed", + }); + }); + + test("rejects every stale attempt mutation after a successor takes over", async () => { + const mutations: readonly { + apply(claim: ApiCommandClaim, database: D1Database, nowMs: number): Promise; + name: string; + }[] = [ + { + apply: (claim, database, nowMs) => renewApiCommandClaim({ claim, database, nowMs }), + name: "renew", + }, + { + apply: (claim, database, nowMs) => completeApiCommand({ claim, database, nowMs }), + name: "complete", + }, + { + apply: (claim, database, nowMs) => + releaseApiCommandForRetry({ + claim, + database, + errorCode: "stale", + errorMessage: "stale", + nowMs, + }), + name: "release", + }, + { + apply: (claim, database, nowMs) => + markApiCommandFailed({ + claim, + database, + errorCode: "stale", + errorMessage: "stale", + nowMs, + }), + name: "fail", + }, + { + apply: (claim, database, nowMs) => + markApiCommandDeadLettered({ + claim, + database, + errorCode: "stale", + errorMessage: "stale", + nowMs, + }), + name: "dead-letter", + }, + ]; + + for (const mutation of mutations) { + const database = await createPublicHttpContractDatabase(); + const queue = createApiCommandQueueStub(); + const bindings = createPublicHttpTestBindings(database, { + apiCommandQueue: queue, + }) as ApiBindings; + const commandId = await enqueueApiCommand(bindings, { + dedupeKey: `scheduled:stale-${mutation.name}`, + kind: "scheduled_maintenance", + payload: { scheduledTime: nowMsForTest() }, + }); + const first = await claimCommand({ + claimOwner: "owner-a", + commandId, + database, + nowMs: 1_000, + }); + const takeoverAt = 1_000 + API_COMMAND_LEASE_MS; + await claimCommand({ + claimOwner: "owner-b", + commandId, + database, + nowMs: takeoverAt, + }); + const before = await database + .app() + .select() + .from(apiCommandsTable) + .where(eq(apiCommandsTable.id, commandId)) + .get(); + + await expect(mutation.apply(first, database, takeoverAt + 1)).resolves.toBe(false); + await expect( + database + .app() + .select() + .from(apiCommandsTable) + .where(eq(apiCommandsTable.id, commandId)) + .get(), + ).resolves.toEqual(before); + } + }); + + test("increments the delivery generation for an explicit terminal retry", async () => { + const database = await createPublicHttpContractDatabase(); + const queue = createApiCommandQueueStub(); + const bindings = createPublicHttpTestBindings(database, { + apiCommandQueue: queue, + }) as ApiBindings; + const commandId = await enqueueApiCommand(bindings, { + dedupeKey: "scheduled:terminal-retry", + kind: "scheduled_maintenance", + payload: { scheduledTime: 1 }, + }); + const first = await claimCommand({ + claimOwner: "owner-a", + commandId, + database, + nowMs: 1_000, + }); + await markApiCommandFailed({ + claim: first, + database, + errorCode: "failed", + errorMessage: "failed", + nowMs: 2_000, + }); + + const admission = await admitApiCommand(bindings, { + dedupeKey: "scheduled:terminal-retry", + kind: "scheduled_maintenance", + payload: { scheduledTime: 2 }, + retryTerminal: true, + }); + await deliverApiCommand(bindings, admission); + + await expect( + database + .app() + .select({ + attemptCount: apiCommandsTable.attemptCount, + deliveryGeneration: apiCommandsTable.deliveryGeneration, + status: apiCommandsTable.status, + }) + .from(apiCommandsTable) + .where(eq(apiCommandsTable.id, commandId)) + .get(), + ).resolves.toEqual({ attemptCount: 0, deliveryGeneration: 2, status: "queued" }); + expect(queue.sent.at(-1)?.body).toEqual({ commandId, deliveryGeneration: 2 }); + await expect( + claimApiCommand({ + claimOwner: "old-message", + commandId, + database, + deliveryGeneration: 1, + nowMs: 3_000, + }), + ).resolves.toEqual({ kind: "stale" }); + }); + + test("fails closed when delivery generation or attempt count is exhausted", async () => { + const database = await createPublicHttpContractDatabase(); + const queue = createApiCommandQueueStub(); + const bindings = createPublicHttpTestBindings(database, { + apiCommandQueue: queue, + }) as ApiBindings; + const commandId = await enqueueApiCommand(bindings, { + dedupeKey: "scheduled:exhausted", + kind: "scheduled_maintenance", + payload: { scheduledTime: 1 }, + }); + + database.execute( + `UPDATE api_command SET delivery_generation = ${Number.MAX_SAFE_INTEGER}, status = 'failed' WHERE id = '${commandId}'`, + ); + await expect( + admitApiCommand(bindings, { + dedupeKey: "scheduled:exhausted", + kind: "scheduled_maintenance", + payload: { scheduledTime: 2 }, + retryTerminal: true, + }), + ).rejects.toThrow("delivery generation is exhausted"); + + database.execute( + `UPDATE api_command SET attempt_count = ${Number.MAX_SAFE_INTEGER}, delivery_generation = 1, status = 'queued' WHERE id = '${commandId}'`, + ); + await expect( + claimApiCommand({ + claimOwner: "owner-a", + commandId, + database, + deliveryGeneration: 1, + nowMs: 1_000, + }), + ).rejects.toThrow("attempt count is exhausted"); + database.execute(`UPDATE api_command SET status = 'failed' WHERE id = '${commandId}'`); + await expect( + claimApiCommand({ + claimOwner: "owner-a", + commandId, + database, + deliveryGeneration: 1, + nowMs: 1_000, + }), + ).resolves.toEqual({ kind: "terminal" }); + }); + + test("rejects a dedupe key reused by a different command kind", async () => { + const database = await createPublicHttpContractDatabase(); + const queue = createApiCommandQueueStub(); + const bindings = createPublicHttpTestBindings(database, { + apiCommandQueue: queue, + }) as ApiBindings; + await enqueueApiCommand(bindings, { + dedupeKey: "shared:kind-collision", + kind: "scheduled_maintenance", + payload: { scheduledTime: 1 }, + }); + + await expect( + enqueueApiCommand(bindings, { + dedupeKey: "shared:kind-collision", + kind: "cost_ledger_reconciliation", + payload: { cursor: null, mode: "audit", scheduledTime: 1 }, + }), + ).rejects.toThrow("different command kind"); + expect(queue.sent).toHaveLength(1); + }); + + test("retries busy main and DLQ deliveries until the live lease expires", async () => { + const database = await createPublicHttpContractDatabase(); + const queue = createApiCommandQueueStub(); + const bindings = createPublicHttpTestBindings(database, { + apiCommandQueue: queue, + }) as ApiBindings; + const commandId = await enqueueApiCommand(bindings, { + dedupeKey: "scheduled:busy-redelivery", + kind: "scheduled_maintenance", + payload: { scheduledTime: 1 }, + }); + await claimCommand({ + claimOwner: "live-owner", + commandId, + database, + nowMs: 1_000, + }); + const before = await database + .app() + .select() + .from(apiCommandsTable) + .where(eq(apiCommandsTable.id, commandId)) + .get(); + const recorded = createRecordedQueueMessage({ + body: { commandId, deliveryGeneration: 1 }, + }); + const deadLetter = createRecordedQueueMessage({ + body: { commandId, deliveryGeneration: 1 }, + }); + + await processApiCommandMessage(bindings, recorded.message, () => 2_000); + await processApiCommandDeadLetterMessage(bindings, deadLetter.message, () => 2_000); + + expect(recorded.recorded).toEqual([{ delaySeconds: 299, type: "retry" }]); + expect(deadLetter.recorded).toEqual([{ delaySeconds: 299, type: "retry" }]); + await expect( + database + .app() + .select() + .from(apiCommandsTable) + .where(eq(apiCommandsTable.id, commandId)) + .get(), + ).resolves.toEqual(before); + }); + + test("acks old-generation main and DLQ deliveries without touching the live successor", async () => { + const database = await createPublicHttpContractDatabase(); + const queue = createApiCommandQueueStub(); + const bindings = createPublicHttpTestBindings(database, { + apiCommandQueue: queue, + }) as ApiBindings; + const commandId = await enqueueApiCommand(bindings, { + dedupeKey: "scheduled:old-deliveries", + kind: "scheduled_maintenance", + payload: { scheduledTime: 1 }, + }); + const first = await claimCommand({ + claimOwner: "owner-a", + commandId, + database, + nowMs: 1_000, + }); + await markApiCommandFailed({ + claim: first, + database, + errorCode: "failed", + errorMessage: "failed", + nowMs: 2_000, + }); + const admission = await admitApiCommand(bindings, { + dedupeKey: "scheduled:old-deliveries", + kind: "scheduled_maintenance", + payload: { scheduledTime: 2 }, + retryTerminal: true, + }); + const successor = await claimCommand({ + claimOwner: "owner-b", + commandId, + database, + deliveryGeneration: 2, + nowMs: 3_000, + }); + expect(admission.shouldDeliver).toBe(true); + const before = await database + .app() + .select() + .from(apiCommandsTable) + .where(eq(apiCommandsTable.id, commandId)) + .get(); + const main = createRecordedQueueMessage({ + body: { commandId, deliveryGeneration: 1 }, + }); + const deadLetter = createRecordedQueueMessage({ + body: { commandId, deliveryGeneration: 1 }, + }); + + await processApiCommandMessage(bindings, main.message, () => 4_000); + await processApiCommandDeadLetterMessage(bindings, deadLetter.message, () => 4_000); + + expect(successor.deliveryGeneration).toBe(2); + expect(main.recorded).toEqual([{ type: "ack" }]); + expect(deadLetter.recorded).toEqual([{ type: "ack" }]); + await expect( + database + .app() + .select() + .from(apiCommandsTable) + .where(eq(apiCommandsTable.id, commandId)) + .get(), + ).resolves.toEqual(before); + }); + + test("uses a new owner for takeover and stops the stale attempt before its effect", async () => { + const fixture = await createAppDeploymentCommandFixture({ runStatus: "activating" }); + const firstEntered = Promise.withResolvers(); + const releaseFirst = Promise.withResolvers(); + const owners: string[] = []; + let effectCount = 0; + let currentTimeMs = Date.now(); + const first = createRecordedQueueMessage({ + body: fixture.body, + id: "same-queue-delivery", + }); + const second = createRecordedQueueMessage({ + body: fixture.body, + id: "same-queue-delivery", + }); + + const firstTask = processAppWorkflowDeliveryForTest( + fixture.bindings, + first.message, + () => currentTimeMs, + { + dispatchAppDeploymentRun: async (_bindings, _payload, authority) => { + owners.push(authority.claimOwner); + await authority.requireOwnership(); + await registerAppDeploymentCandidateForTest(fixture, authority, currentTimeMs); + await authority.requireOwnership(); + firstEntered.resolve(); + await releaseFirst.promise; + await authority.requireOwnership(); + effectCount += 1; + return { + errorCode: "stale_effect", + errorMessage: "The stale attempt must not reach this effect.", + kind: "terminal_failure", + runId: fixture.runId, + }; + }, + }, + ); + await firstEntered.promise; + currentTimeMs += API_COMMAND_LEASE_MS; + + try { + await processAppWorkflowDeliveryForTest( + fixture.bindings, + second.message, + () => currentTimeMs, + { + dispatchAppDeploymentRun: async (_bindings, _payload, authority) => { + owners.push(authority.claimOwner); + await authority.requireOwnership(); + effectCount += 1; + return { + errorCode: "takeover_failure", + errorMessage: "The takeover owns the terminal result.", + kind: "terminal_failure", + runId: fixture.runId, + }; + }, + }, + ); + } finally { + releaseFirst.resolve(); + await firstTask; + } + + expect(owners).toHaveLength(2); + expect(owners[0]).not.toBe(owners[1]); + expect(effectCount).toBe(1); + expect(second.recorded).toEqual([{ type: "ack" }]); + expect(first.recorded).toEqual([{ delaySeconds: 30, type: "retry" }]); + await expect( + fixture.database + .app() + .select({ + activeScriptName: appDeploymentsTable.activeScriptName, + lastSuccessfulUrl: appDeploymentsTable.lastSuccessfulUrl, + }) + .from(appDeploymentsTable) + .where(eq(appDeploymentsTable.id, fixture.deploymentId)) + .get(), + ).resolves.toEqual({ activeScriptName: null, lastSuccessfulUrl: null }); + await expect( + fixture.database + .app() + .select({ + errorCode: appDeploymentRunsTable.errorCode, + status: appDeploymentRunsTable.status, + }) + .from(appDeploymentRunsTable) + .where(eq(appDeploymentRunsTable.id, fixture.runId)) + .get(), + ).resolves.toEqual({ errorCode: "takeover_failure", status: "failed" }); + }); + + test("latches an uncertain renewal error before the handler can continue", async () => { + const fixture = await createAppDeploymentCommandFixture({ runStatus: "activating" }); + let failNextRenewal = false; + let failedRenewalCount = 0; + const failingDatabase = new Proxy(fixture.database, { + get(target, property, receiver) { + if (property === "prepare") { + return (query: string) => { + if ( + failNextRenewal && + query.startsWith('update "api_command" set "claim_expires_at" = ?') + ) { + failNextRenewal = false; + failedRenewalCount += 1; + throw new Error("renewal response was lost"); + } + return target.prepare(query); + }; + } + const value = Reflect.get(target, property, receiver) as unknown; + return typeof value === "function" ? value.bind(target) : value; + }, + }) as D1Database; + const bindings = { ...fixture.bindings, DB: failingDatabase }; + const recorded = createRecordedQueueMessage({ body: fixture.body }); + let effectCount = 0; + + await processAppWorkflowDeliveryForTest(bindings, recorded.message, () => 1_000, { + dispatchAppDeploymentRun: async (_bindings, _payload, authority) => { + failNextRenewal = true; + await authority.requireOwnership().catch(() => undefined); + await authority.requireOwnership(); + effectCount += 1; + return { + errorCode: "unreachable", + errorMessage: "The handler must stop after an uncertain renewal.", + kind: "terminal_failure", + runId: fixture.runId, + }; + }, + }); + + expect(failedRenewalCount).toBe(1); + expect(effectCount).toBe(0); + expect(recorded.recorded).toEqual([{ delaySeconds: 30, type: "retry" }]); + await expect( + fixture.database + .app() + .select({ + claimOwner: apiCommandsTable.claimOwner, + status: apiCommandsTable.status, + }) + .from(apiCommandsTable) + .where(eq(apiCommandsTable.id, fixture.commandId)) + .get(), + ).resolves.toEqual({ claimOwner: null, status: "queued" }); + }); + + test("terminalizes a timed-out App command before a fourth dispatch effect", async () => { + const fixture = await createAppDeploymentCommandFixture({ runStatus: "activating" }); + fixture.database.execute(` + UPDATE api_command + SET attempt_count = ${APP_DEPLOYMENT_RUN_DISPATCH_MAX_ATTEMPTS}, + claim_expires_at = 1, + claim_owner = 'timed-out-attempt', + status = 'running' + WHERE id = '${fixture.commandId}' + `); + const recorded = createRecordedQueueMessage({ body: fixture.body }); + let dispatchCount = 0; + + await processAppWorkflowDeliveryForTest(fixture.bindings, recorded.message, () => 2, { + dispatchAppDeploymentRun: async () => { + dispatchCount += 1; + return { kind: "skipped" }; + }, + }); + + expect(dispatchCount).toBe(0); + expect(recorded.recorded).toEqual([{ type: "ack" }]); + await expect( + fixture.database + .app() + .select({ + errorCode: appDeploymentRunsTable.errorCode, + status: appDeploymentRunsTable.status, + }) + .from(appDeploymentRunsTable) + .where(eq(appDeploymentRunsTable.id, fixture.runId)) + .get(), + ).resolves.toEqual({ + errorCode: APP_DEPLOYMENT_RUN_DISPATCH_RETRY_EXHAUSTED_CODE, + status: "failed", + }); + await expect( + fixture.database + .app() + .select({ + attemptCount: apiCommandsTable.attemptCount, + lastErrorCode: apiCommandsTable.lastErrorCode, + status: apiCommandsTable.status, + }) + .from(apiCommandsTable) + .where(eq(apiCommandsTable.id, fixture.commandId)) + .get(), + ).resolves.toEqual({ + attemptCount: APP_DEPLOYMENT_RUN_DISPATCH_MAX_ATTEMPTS + 1, + lastErrorCode: APP_DEPLOYMENT_RUN_DISPATCH_RETRY_EXHAUSTED_CODE, + status: "failed", + }); + }); + + test("commits App deployment success and command success in one owned batch", async () => { + const fixture = await createAppDeploymentCommandFixture({ runStatus: "activating" }); + const currentTimeMs = Date.now(); + let activeScriptName = ""; + const targetUrl = "https://api-command-test.example.com"; + const recorded = createRecordedQueueMessage({ body: fixture.body }); + + await processAppWorkflowDeliveryForTest( + fixture.bindings, + recorded.message, + () => currentTimeMs, + { + dispatchAppDeploymentRun: async (_bindings, _payload, authority) => { + await authority.requireOwnership(); + activeScriptName = await registerAppDeploymentCandidateForTest( + fixture, + authority, + currentTimeMs, + true, + ); + await authority.requireOwnership(); + return { + activeScriptName, + deploymentId: fixture.deploymentId, + externalDeploymentId: "external-deployment", + externalProjectId: "external-project", + externalVersionId: "external-version", + kind: "succeeded", + runId: fixture.runId, + url: targetUrl, + }; + }, + }, + ); + + await expect( + fixture.database + .app() + .select({ + activeScriptName: appDeploymentsTable.activeScriptName, + lastSuccessfulUrl: appDeploymentsTable.lastSuccessfulUrl, + }) + .from(appDeploymentsTable) + .where(eq(appDeploymentsTable.id, fixture.deploymentId)) + .get(), + ).resolves.toEqual({ activeScriptName, lastSuccessfulUrl: targetUrl }); + await expect( + fixture.database + .app() + .select({ + externalDeploymentId: appDeploymentRunsTable.externalDeploymentId, + externalProjectId: appDeploymentRunsTable.externalProjectId, + externalVersionId: appDeploymentRunsTable.externalVersionId, + status: appDeploymentRunsTable.status, + targetScriptName: appDeploymentRunsTable.targetScriptName, + url: appDeploymentRunsTable.url, + }) + .from(appDeploymentRunsTable) + .where(eq(appDeploymentRunsTable.id, fixture.runId)) + .get(), + ).resolves.toEqual({ + externalDeploymentId: "external-deployment", + externalProjectId: "external-project", + externalVersionId: "external-version", + status: "success", + targetScriptName: activeScriptName, + url: targetUrl, + }); + await expect( + fixture.database + .app() + .select({ + claimOwner: apiCommandsTable.claimOwner, + status: apiCommandsTable.status, + }) + .from(apiCommandsTable) + .where(eq(apiCommandsTable.id, fixture.commandId)) + .get(), + ).resolves.toEqual({ claimOwner: null, status: "succeeded" }); + expect(recorded.recorded).toEqual([{ type: "ack" }]); + }); + + test("does not publish an App deployment outcome for a different candidate", async () => { + const fixture = await createAppDeploymentCommandFixture({ runStatus: "activating" }); + const currentTimeMs = Date.now(); + let persistedCandidate = ""; + let reportedCandidate = ""; + const recorded = createRecordedQueueMessage({ body: fixture.body }); + + await processAppWorkflowDeliveryForTest( + fixture.bindings, + recorded.message, + () => currentTimeMs, + { + dispatchAppDeploymentRun: async (_bindings, _payload, authority) => { + await authority.requireOwnership(); + persistedCandidate = await registerAppDeploymentCandidateForTest( + fixture, + authority, + currentTimeMs, + true, + ); + reportedCandidate = appDeploymentCandidateScriptName({ + ...authority, + attemptCount: authority.attemptCount + 1, + }); + await authority.requireOwnership(); + return { + activeScriptName: reportedCandidate, + deploymentId: fixture.deploymentId, + externalDeploymentId: "external-deployment", + externalProjectId: "external-project", + externalVersionId: "external-version", + kind: "succeeded", + runId: fixture.runId, + url: "https://api-command-test.example.com", + }; + }, + }, + ); + + await expect( + fixture.database + .app() + .select({ + activeScriptName: appDeploymentsTable.activeScriptName, + lastSuccessfulUrl: appDeploymentsTable.lastSuccessfulUrl, + }) + .from(appDeploymentsTable) + .where(eq(appDeploymentsTable.id, fixture.deploymentId)) + .get(), + ).resolves.toEqual({ activeScriptName: null, lastSuccessfulUrl: null }); + await expect( + fixture.database + .app() + .select({ + status: appDeploymentRunsTable.status, + targetScriptName: appDeploymentRunsTable.targetScriptName, + url: appDeploymentRunsTable.url, + }) + .from(appDeploymentRunsTable) + .where(eq(appDeploymentRunsTable.id, fixture.runId)) + .get(), + ).resolves.toEqual({ + status: "activating", + targetScriptName: persistedCandidate, + url: null, + }); + await expect( + fixture.database + .app() + .select({ status: apiCommandsTable.status }) + .from(apiCommandsTable) + .where(eq(apiCommandsTable.id, fixture.commandId)) + .get(), + ).resolves.toEqual({ status: "running" }); + expect(recorded.recorded).toEqual([{ delaySeconds: 30, type: "retry" }]); + }); + + test("preserves a pre-existing App failure while terminalizing its command", async () => { + const fixture = await createAppDeploymentCommandFixture({ + errorCode: "existing_failure", + errorMessage: "The deployment was already failed elsewhere.", + runStatus: "failed", + }); + const recorded = createRecordedQueueMessage({ body: fixture.body }); + + await processAppWorkflowDeliveryForTest( + fixture.bindings, + recorded.message, + () => nowMsForTest() + 1, + { + dispatchAppDeploymentRun: async (_bindings, _payload, authority) => { + await authority.requireOwnership(); + return { + errorCode: "later_failure", + errorMessage: "This error must not replace the business terminal state.", + kind: "terminal_failure", + runId: fixture.runId, + }; + }, + }, + ); + + await expect( + fixture.database + .app() + .select({ + errorCode: appDeploymentRunsTable.errorCode, + errorMessage: appDeploymentRunsTable.errorMessage, + status: appDeploymentRunsTable.status, + }) + .from(appDeploymentRunsTable) + .where(eq(appDeploymentRunsTable.id, fixture.runId)) + .get(), + ).resolves.toEqual({ + errorCode: "existing_failure", + errorMessage: "The deployment was already failed elsewhere.", + status: "failed", + }); + await expect( + fixture.database + .app() + .select({ + lastErrorCode: apiCommandsTable.lastErrorCode, + lastErrorMessage: apiCommandsTable.lastErrorMessage, + status: apiCommandsTable.status, + }) + .from(apiCommandsTable) + .where(eq(apiCommandsTable.id, fixture.commandId)) + .get(), + ).resolves.toEqual({ + lastErrorCode: "existing_failure", + lastErrorMessage: "The deployment was already failed elsewhere.", + status: "failed", + }); + expect(recorded.recorded).toEqual([{ type: "ack" }]); + }); + + test("fails an App command closed when its terminal run is missing", async () => { + const fixture = await createAppDeploymentCommandFixture({ runStatus: "activating" }); + await fixture.database + .app() + .delete(appDeploymentRunsTable) + .where(eq(appDeploymentRunsTable.id, fixture.runId)) + .run(); + const recorded = createRecordedQueueMessage({ body: fixture.body }); + + await processAppWorkflowDeliveryForTest( + fixture.bindings, + recorded.message, + () => nowMsForTest() + 1, + { + dispatchAppDeploymentRun: async (_bindings, _payload, authority) => { + await authority.requireOwnership(); + return { + errorCode: "missing_run_failure", + errorMessage: "The missing run cannot remain queued forever.", + kind: "terminal_failure", + runId: fixture.runId, + }; + }, + }, + ); + + await expect( + fixture.database + .app() + .select({ + lastErrorCode: apiCommandsTable.lastErrorCode, + lastErrorMessage: apiCommandsTable.lastErrorMessage, + status: apiCommandsTable.status, + }) + .from(apiCommandsTable) + .where(eq(apiCommandsTable.id, fixture.commandId)) + .get(), + ).resolves.toEqual({ + lastErrorCode: "missing_run_failure", + lastErrorMessage: "The missing run cannot remain queued forever.", + status: "failed", + }); + expect(recorded.recorded).toEqual([{ type: "ack" }]); + }); + + test("retries a DLQ message when its ledger claim cannot be persisted", async () => { + const database = await createPublicHttpContractDatabase(); + const queue = createApiCommandQueueStub(); + const baseBindings = createPublicHttpTestBindings(database, { + apiCommandQueue: queue, + }) as ApiBindings; + const commandId = await enqueueApiCommand(baseBindings, { + dedupeKey: "scheduled:dlq-database-error", + kind: "scheduled_maintenance", + payload: { scheduledTime: 1 }, + }); + const failingDatabase = new Proxy(database, { + get(target, property, receiver) { + if (property === "prepare") { + return () => { + throw new Error("database unavailable"); + }; + } + const value = Reflect.get(target, property, receiver) as unknown; + return typeof value === "function" ? value.bind(target) : value; + }, + }) as D1Database; + const bindings = { ...baseBindings, DB: failingDatabase }; + const recorded = createRecordedQueueMessage({ + body: { commandId, deliveryGeneration: 1 }, + }); + + await processApiCommandDeadLetterMessage(bindings, recorded.message, nowMsForTest); + + expect(recorded.recorded).toEqual([{ delaySeconds: 30, type: "retry" }]); + }); }); diff --git a/apps/api/tests/api-driver-boundary-fixtures.ts b/apps/api/tests/api-driver-boundary-fixtures.ts index d0d74336..cd440dd3 100644 --- a/apps/api/tests/api-driver-boundary-fixtures.ts +++ b/apps/api/tests/api-driver-boundary-fixtures.ts @@ -173,7 +173,9 @@ export function createDriverEvent(value: object): DriverEvent { createId: () => API_DRIVER_BOUNDARY_IDS.runtimeEvent, driverInstanceId: API_DRIVER_BOUNDARY_IDS.driverInstance, occurredAt: "1970-01-01T00:00:00.010Z", + runtimeId: "openai-runtime", sessionId: API_DRIVER_BOUNDARY_IDS.session, + traceId: "trace-1", }, value, ); @@ -195,6 +197,7 @@ export function createRuntimeSessionLink(): RuntimeSessionLink { sandboxId: API_DRIVER_BOUNDARY_IDS.sandbox, sandboxKind: "cattle", sandboxSubjectKind: "session", + runtimeId: "openai-runtime", sessionId: API_DRIVER_BOUNDARY_IDS.session, sessionRunId: API_DRIVER_BOUNDARY_IDS.sessionRun, sessionRunStatus: "running", diff --git a/apps/api/tests/api-driver-boundary.test.ts b/apps/api/tests/api-driver-boundary.test.ts index 30287f02..3cc1e759 100644 --- a/apps/api/tests/api-driver-boundary.test.ts +++ b/apps/api/tests/api-driver-boundary.test.ts @@ -1,22 +1,32 @@ import { describe, expect, test } from "bun:test"; -import { readFileSync } from "node:fs"; -import { EventType, MOSOO_CUSTOM_EVENT } from "@mosoo/ag-ui-session"; +import { + EventType, + MOSOO_CUSTOM_EVENT, + applyAgUiEventToSessionLiveState, +} from "@mosoo/ag-ui-session"; import { DRIVER_CONTROL_PORT_MAX, DRIVER_CONTROL_PORT_MIN, DRIVER_PROTOCOL_VERSION, parseDriverBootPayloadJson, } from "@mosoo/agent-driver/boot"; +import { + RUNTIME_EVENT_KINDS as DRIVER_RUNTIME_EVENT_KINDS, + RUNTIME_EVENT_SCHEMA_VERSION as DRIVER_RUNTIME_EVENT_SCHEMA_VERSION, + toRuntimeEventInput as toDriverRuntimeEventInput, +} from "@mosoo/agent-driver/events"; import { createDefaultAgentBuiltInTools } from "@mosoo/contracts/agent"; import { PLATFORM_ID_FIXTURES } from "@mosoo/id/testing"; -import { RUNTIME_EVENT_SCHEMA_VERSION, createRuntimeEvent } from "@mosoo/runtime-events"; +import { + RUNTIME_EVENT_KINDS, + RUNTIME_EVENT_SCHEMA_VERSION, + createRuntimeEvent, +} from "@mosoo/runtime-events"; import { getDriverControlPort } from "../src/modules/runtime/domain/sandbox-layout"; -import { - assertRuntimeEventMatchesDriverEnvelope, - assertRuntimeEventMatchesDriverLink, -} from "../src/modules/runtime/infrastructure/driver-instance/event-link-assertion"; +import { canonicalizeDriverEventEnvelope } from "../src/modules/runtime/infrastructure/driver-instance/driver-event-canonicalization"; +import { assertRuntimeEventMatchesDriverLink } from "../src/modules/runtime/infrastructure/driver-instance/event-link-assertion"; import { createBaseLiveState, readPermissionRequestViews, @@ -24,7 +34,10 @@ import { } from "../src/modules/runtime/infrastructure/driver-instance/event-projection"; import { appRuntimeDriverEvents } from "../src/modules/runtime/infrastructure/driver-instance/events"; import { readNativeResumeRef } from "../src/modules/runtime/infrastructure/driver-instance/native-resume-ref-event"; -import { parseDriverEventBatchInput } from "../src/modules/runtime/infrastructure/driver-instance/rpc-wire"; +import { + parseDriverEventBatchInput, + parseDriverEventBatchOutput, +} from "../src/modules/runtime/infrastructure/driver-instance/rpc-wire"; import { createDriverBootPayload, verifyRuntimeActionToken, @@ -56,10 +69,6 @@ const artifactPaths = { python: ["/workspace/.mosoo/environment-artifacts/artifact/python/site-packages"], }; -function readText(path: string): string { - return readFileSync(new URL(path, import.meta.url), "utf8"); -} - describe("API to driver boundary", () => { test("assigns driver control ports inside the sandbox image contract", () => { const port = getDriverControlPort("driver-01KRZRFGXAA788FW1GDBT7F0EZ"); @@ -72,195 +81,6 @@ describe("API to driver boundary", () => { expect(AGENT_DRIVER_PROCESS_COMMAND).toBe("agent-driver"); }); - test("uses the agent-driver runtime contract for runtime selection", () => { - const runtimeConfig = readText("../src/modules/runtime/domain/runtime-config.ts"); - const agentConfig = readText( - "../src/modules/agents/application/agent-versioned-config.service.ts", - ); - const nativeResumeRef = readText( - "../src/modules/runtime/infrastructure/native-resume-ref.repository.ts", - ); - const nativeResumeRefEvent = readText( - "../src/modules/runtime/infrastructure/driver-instance/native-resume-ref-event.ts", - ); - - expect(runtimeConfig).toContain('from "@mosoo/agent-driver/runtime"'); - expect(runtimeConfig).not.toContain('from "@mosoo/driver-protocol"'); - expect(agentConfig).toContain('from "@mosoo/agent-driver/runtime"'); - expect(agentConfig).not.toContain('from "@mosoo/driver-protocol"'); - expect(nativeResumeRef).toContain('from "@mosoo/agent-driver/runtime"'); - expect(nativeResumeRef).not.toContain('from "@mosoo/driver-protocol"'); - expect(nativeResumeRefEvent).toContain('from "@mosoo/agent-driver/runtime"'); - expect(nativeResumeRefEvent).not.toContain('from "@mosoo/driver-protocol"'); - }); - - test("uses agent-driver boot constants for process startup", () => { - const bootToken = readText("../src/modules/runtime/infrastructure/runtime-boot-token.ts"); - const provisioning = readText( - "../src/modules/runtime/infrastructure/runtime-sandbox-provisioning/runtime-driver-provisioning.service.ts", - ); - const driverRecord = readText( - "../src/modules/runtime/infrastructure/driver-instance/driver-instance-record.repository.ts", - ); - const sandboxLayout = readText("../src/modules/runtime/domain/sandbox-layout.ts"); - - expect(bootToken).toContain('from "@mosoo/agent-driver/boot"'); - expect(bootToken).toContain("DRIVER_PROTOCOL_VERSION"); - expect(bootToken).not.toContain('from "@mosoo/driver-protocol"'); - expect(provisioning).toContain('from "@mosoo/agent-driver/boot"'); - expect(provisioning).toContain("DRIVER_BOOT_PAYLOAD_FILE_ENV_NAME"); - expect(provisioning).toContain("const bootPayload = createDriverBootPayload"); - expect(provisioning).toContain("JSON.stringify(bootPayload)"); - expect(driverRecord).toContain('from "@mosoo/agent-driver/boot"'); - expect(driverRecord).toContain("DRIVER_PROTOCOL_VERSION"); - expect(sandboxLayout).toContain('from "@mosoo/agent-driver/boot"'); - expect(sandboxLayout).toContain("DRIVER_CONTROL_PORT_COUNT"); - expect(sandboxLayout).toContain("DRIVER_CONTROL_PORT_MIN"); - }); - - test("uses agent-driver sandbox path contracts", () => { - const runtimeProfile = readText("../src/modules/runtime/application/agent-runtime-profile.ts"); - const subjectPlatform = readText( - "../src/modules/runtime/infrastructure/runtime-subject-lifecycle/runtime-subject-platform.ts", - ); - - expect(runtimeProfile).toContain('from "@mosoo/agent-driver/paths"'); - expect(subjectPlatform).toContain('from "@mosoo/agent-driver/paths"'); - }); - - test("publishes agent-driver event envelope contracts", () => { - const driverPublicEvents = readText("../../driver/src/events.ts"); - const rpc = readText("../src/modules/runtime/infrastructure/driver-instance/rpc.ts"); - const rpcWire = readText("../src/modules/runtime/infrastructure/driver-instance/rpc-wire.ts"); - const ingestion = readText( - "../src/modules/runtime/infrastructure/driver-instance/rpc-event-ingestion-controller.ts", - ); - const projection = readText("../src/modules/runtime/infrastructure/driver-instance/events.ts"); - const receipts = readText( - "../src/modules/runtime/infrastructure/driver-instance/driver-event-receipts.ts", - ); - const replayFilter = readText( - "../src/modules/runtime/infrastructure/driver-instance/runtime-event-replay-filter.ts", - ); - const fixtures = readText("./api-driver-boundary-fixtures.ts"); - - expect(driverPublicEvents).toContain("./protocol/events"); - expect(rpcWire).toContain('from "@mosoo/agent-driver/events"'); - expect(rpcWire).toContain("parseDriverEventEnvelope"); - expect(rpcWire).not.toContain('from "@mosoo/driver-protocol"'); - expect(rpc).not.toContain("toAgentDriverEventEnvelopes"); - expect(ingestion).toContain("state.readProcessedDriverEventReceipts(input.events)"); - expect(ingestion).toContain("state.filterUnprocessedDriverEvents(input.events)"); - expect(projection).toContain('from "@mosoo/agent-driver/events"'); - expect(projection).not.toContain('from "@mosoo/driver-protocol"'); - expect(receipts).toContain('from "@mosoo/agent-driver/events"'); - expect(replayFilter).toContain('from "@mosoo/agent-driver/events"'); - expect(fixtures).toContain('from "@mosoo/agent-driver/events"'); - }); - - test("uses agent-driver ORPC contracts behind the API wire parser", () => { - const rpc = readText("../src/modules/runtime/infrastructure/driver-instance/rpc.ts"); - const controller = readText( - "../src/modules/runtime/infrastructure/driver-instance/rpc-controller.ts", - ); - const command = readText( - "../src/modules/runtime/infrastructure/driver-instance/rpc-command-controller.ts", - ); - const eventIngestion = readText( - "../src/modules/runtime/infrastructure/driver-instance/rpc-event-ingestion-controller.ts", - ); - const handshake = readText( - "../src/modules/runtime/infrastructure/driver-instance/rpc-handshake-controller.ts", - ); - const state = readText("../src/modules/runtime/infrastructure/driver-instance/state.ts"); - const runtimeState = readText( - "../src/modules/runtime/infrastructure/driver-instance/runtime-state.ts", - ); - const runtimeStateStore = readText( - "../src/modules/runtime/infrastructure/driver-instance/runtime-state-store.ts", - ); - const lifecycle = readText( - "../src/modules/runtime/infrastructure/driver-instance/lifecycle.ts", - ); - const handler = readText( - "../src/modules/runtime/infrastructure/driver-instance/rpc-handler.ts", - ); - const rpcWire = readText("../src/modules/runtime/infrastructure/driver-instance/rpc-wire.ts"); - - expect(rpc).toContain('from "@mosoo/agent-driver/orpc"'); - expect(rpc).toContain('from "./rpc-wire"'); - expect(controller).toContain('from "@mosoo/agent-driver/orpc"'); - expect(command).toContain('from "@mosoo/agent-driver/orpc"'); - expect(eventIngestion).toContain('from "@mosoo/agent-driver/orpc"'); - expect(handshake).toContain('from "@mosoo/agent-driver/orpc"'); - expect(state).toContain('from "@mosoo/agent-driver/orpc"'); - expect(runtimeState).toContain('from "@mosoo/agent-driver/orpc"'); - expect(runtimeStateStore).toContain('from "@mosoo/agent-driver/orpc"'); - expect(runtimeStateStore).toContain("parseDriverHelloInput"); - expect(runtimeStateStore).not.toContain('from "@mosoo/driver-protocol"'); - expect(lifecycle).toContain('from "@mosoo/agent-driver/orpc"'); - expect(controller).not.toContain('from "@mosoo/driver-protocol"'); - expect(eventIngestion).not.toContain('from "@mosoo/driver-protocol"'); - expect(rpc).not.toContain('from "@mosoo/driver-protocol"'); - expect(rpcWire).toContain("runtimeOrpcRouter"); - expect(rpcWire).toContain('from "@mosoo/agent-driver/orpc"'); - expect(rpcWire).toContain('from "@mosoo/agent-driver/events"'); - expect(rpcWire).not.toContain('from "@mosoo/driver-protocol"'); - expect(handler).not.toContain('from "@mosoo/driver-protocol"'); - expect(handler).toContain("runtimeOrpcRouter"); - }); - - test("builds session config traces from the agent-driver boot payload", () => { - const dispatchRun = readText( - "../src/modules/runtime/application/session-runs/dispatch-run.service.ts", - ); - const callbackContract = readText( - "../src/modules/runtime/application/execution-plane/driver-boot-payload-prepared.ts", - ); - const configTrace = readText( - "../src/modules/runtime/application/session-definition/session-config-trace-event.ts", - ); - - expect(callbackContract).toContain('from "@mosoo/agent-driver/boot"'); - expect(dispatchRun).toContain("onBootPayloadPrepared: async ({ bootPayload })"); - expect(dispatchRun).not.toContain("toAgentDriverBootPayload(bootPayload)"); - expect(dispatchRun).toContain("buildSessionConfigTraceValue(bootPayload)"); - expect(dispatchRun).toContain("bootPayload.execution.session.mcpServers.length"); - expect(dispatchRun).toContain("bootPayload.execution.provider"); - expect(configTrace).toContain('from "@mosoo/agent-driver/boot"'); - expect(configTrace).not.toContain('from "@mosoo/driver-protocol"'); - }); - - test("owns platform driver snapshots inside the API runtime domain", () => { - const driverSnapshot = readText("../src/modules/runtime/domain/driver-snapshot.ts"); - const runtimeProfile = readText("../src/modules/runtime/application/agent-runtime-profile.ts"); - const executionTypes = readText( - "../src/modules/runtime/application/session-definition/session-execution.types.ts", - ); - const executionSpec = readText( - "../src/modules/runtime/infrastructure/runtime-sandbox-provisioning/runtime-driver-execution-spec.builder.ts", - ); - const sandboxSessionTypes = readText( - "../src/modules/runtime/infrastructure/sandbox-session/sandbox-session.types.ts", - ); - const sandboxConversationCodec = readText( - "../src/modules/runtime/infrastructure/sandbox-session/sandbox-conversation-session-codec.ts", - ); - const mcpRuntime = readText("../src/modules/mcp/application/mcp-runtime.service.ts"); - const fixtures = readText("./api-driver-boundary-fixtures.ts"); - - expect(driverSnapshot).toContain('from "@mosoo/agent-driver/runtime"'); - expect(driverSnapshot).not.toContain('from "@mosoo/driver-protocol"'); - expect(runtimeProfile).toContain('from "../domain/driver-snapshot"'); - expect(executionTypes).toContain('from "../../domain/driver-snapshot"'); - expect(executionSpec).toContain('from "../../domain/driver-snapshot"'); - expect(sandboxSessionTypes).toContain('from "../../domain/driver-snapshot"'); - expect(sandboxConversationCodec).toContain('from "../../domain/driver-snapshot"'); - expect(sandboxConversationCodec).toContain("readSandboxConversationOriginRecord"); - expect(mcpRuntime).toContain('from "../../runtime/domain/driver-snapshot"'); - expect(fixtures).toContain('from "../src/modules/runtime/domain/driver-snapshot"'); - }); - test("builds a driver execution spec with scoped grants and profile env", async () => { const execution = await buildExecutionSpec(bindings, { builtInTools: [ @@ -479,8 +299,10 @@ describe("API to driver boundary", () => { messageId: "message-1", role: "agent", }, + runtimeId: "openai-runtime", schemaVersion: RUNTIME_EVENT_SCHEMA_VERSION, sessionId: API_DRIVER_BOUNDARY_IDS.session, + traceId: "trace-1", visibility: "participant", }); }); @@ -510,8 +332,264 @@ describe("API to driver boundary", () => { expect(envelope.occurredAt).toBe("1970-01-01T00:00:00.010Z"); }); + test("requires source identity on every accepted driver event receipt", () => { + const receipt = { + eventId: "source-1", + seq: 1, + type: "message.delta", + }; + + expect(parseDriverEventBatchOutput({ accepted: [receipt] })).toEqual({ + accepted: [receipt], + }); + expect(() => + parseDriverEventBatchOutput({ + accepted: [{ seq: 1, type: "message.delta" }], + }), + ).toThrow(); + expect(() => + parseDriverEventBatchOutput({ + accepted: [{ ...receipt, eventId: "" }], + }), + ).toThrow(); + }); + + test("keeps the Driver and API canonical event vocabularies exactly aligned", () => { + expect(RUNTIME_EVENT_SCHEMA_VERSION).toBe(DRIVER_RUNTIME_EVENT_SCHEMA_VERSION); + expect(RUNTIME_EVENT_KINDS).toEqual(DRIVER_RUNTIME_EVENT_KINDS); + }); + + test("admits an envelope built by the production Driver event builder", () => { + const occurredAt = "2026-08-29T00:00:00.000Z"; + const [driverEvent] = toDriverRuntimeEventInput( + { + createId: () => API_DRIVER_BOUNDARY_IDS.runtimeEvent, + driverInstanceId: API_DRIVER_BOUNDARY_IDS.driverInstance, + occurredAt, + runId: API_DRIVER_BOUNDARY_IDS.sessionRun, + sessionId: API_DRIVER_BOUNDARY_IDS.session, + sourceEventId: "driver-builder-message-delta", + } as Parameters[0], + { + kind: "message.delta", + payload: { + contentDelta: "Driver-built payload", + messageId: "driver-builder-message", + role: "agent", + }, + }, + ); + + expect(driverEvent).toBeDefined(); + expect( + parseDriverEventBatchInput({ + driverInstanceId: API_DRIVER_BOUNDARY_IDS.driverInstance, + events: [ + { + event: driverEvent, + eventId: "driver-builder-message-delta", + occurredAt, + }, + ], + }).events, + ).toEqual([ + { + event: driverEvent, + eventId: "driver-builder-message-delta", + occurredAt, + }, + ]); + }); + + test("admits Driver terminal wire fixtures through the independent API boundary", async () => { + const link = createRuntimeSessionLink(); + const occurredAt = "1970-01-01T00:00:01.000Z"; + const fixtures = [ + { + id: "01J0000000000000000000001C", + kind: "message.cancelled", + payload: { messageId: "message-cancelled", role: "agent" }, + }, + { + id: "01J0000000000000000000001D", + kind: "message.failed", + payload: { + error: { + code: "runtime.failed", + details: {}, + message: "Runtime failed.", + retryable: false, + }, + messageId: "message-failed", + role: "agent", + }, + }, + { + id: "01J0000000000000000000001E", + kind: "thought.cancelled", + payload: { thoughtId: "thought-cancelled" }, + }, + { + id: "01J0000000000000000000001F", + kind: "tool.call.updated", + payload: { status: "cancelled", toolCallId: "tool-cancelled" }, + }, + ] as const; + const batch = parseDriverEventBatchInput({ + driverInstanceId: API_DRIVER_BOUNDARY_IDS.driverInstance, + events: fixtures.map(({ id, kind, payload }, index) => ({ + event: { + actor: "driver", + delivery: "lossless", + driverInstanceId: API_DRIVER_BOUNDARY_IDS.driverInstance, + id, + kind, + occurredAt, + origin: "driver", + payload, + runId: API_DRIVER_BOUNDARY_IDS.sessionRun, + runtimeId: "openai-runtime", + schemaVersion: DRIVER_RUNTIME_EVENT_SCHEMA_VERSION, + sessionId: API_DRIVER_BOUNDARY_IDS.session, + traceId: "trace-1", + visibility: "participant", + }, + eventId: `source-terminal-${index}`, + occurredAt, + })), + }); + const projection = await appRuntimeDriverEvents({ DB: new SqliteD1Database() } as ApiBindings, { + currentLiveState: createBaseLiveState({ + callerId: link.callerId, + creatorId: link.creatorId, + driverInstanceId: API_DRIVER_BOUNDARY_IDS.driverInstance, + sessionId: link.sessionId, + }), + driverInstanceId: API_DRIVER_BOUNDARY_IDS.driverInstance, + events: batch.events, + link, + }); + + expect( + projection.runtimeEvents.map(({ event }) => ({ + id: event.id, + kind: event.kind, + payload: event.payload, + schemaVersion: event.schemaVersion, + })), + ).toEqual( + fixtures.map(({ id, kind, payload }) => ({ + id, + kind, + payload, + schemaVersion: RUNTIME_EVENT_SCHEMA_VERSION, + })), + ); + expect(projection.sessionDeliveryEvents.map(({ event }) => event.type)).toEqual([ + EventType.TEXT_MESSAGE_END, + EventType.TEXT_MESSAGE_END, + EventType.REASONING_MESSAGE_END, + EventType.CUSTOM, + ]); + }); + + test("replays tool snapshots and deltas through the same live-state reducer", async () => { + const link = createRuntimeSessionLink(); + const currentLiveState = createBaseLiveState({ + callerId: link.callerId, + creatorId: link.creatorId, + driverInstanceId: API_DRIVER_BOUNDARY_IDS.driverInstance, + sessionId: link.sessionId, + }); + const updates = [ + { + parentMessageId: "assistant-1", + rawInputDelta: '{"cmd":', + rawOutputDelta: "partial", + status: "running", + title: "Shell", + toolCallId: "tool-1", + }, + { + rawInput: '{"cmd":"ls"', + rawOutput: "snapshot", + status: "running", + toolCallId: "tool-1", + }, + { + rawInputDelta: ',"tail":true}', + rawOutputDelta: " tail", + status: "completed", + toolCallId: "tool-1", + }, + ] as const; + const batch = parseDriverEventBatchInput({ + driverInstanceId: API_DRIVER_BOUNDARY_IDS.driverInstance, + events: updates.map((payload, index) => ({ + event: createDriverEvent({ + kind: "tool.call.updated", + payload, + runId: API_DRIVER_BOUNDARY_IDS.sessionRun, + }), + eventId: `source-tool-${index}`, + occurredAt: `1970-01-01T00:00:0${index + 1}.000Z`, + })), + }); + const projection = await appRuntimeDriverEvents({ DB: new SqliteD1Database() } as ApiBindings, { + currentLiveState, + driverInstanceId: API_DRIVER_BOUNDARY_IDS.driverInstance, + events: batch.events, + link, + }); + const replayed = projection.sessionDeliveryEvents.reduce( + (state, record) => applyAgUiEventToSessionLiveState(state, record.event), + currentLiveState, + ); + + expect({ ...projection.nextLiveState, updatedAt: null }).toEqual({ + ...replayed, + updatedAt: null, + }); + expect(replayed.messages).toEqual([ + expect.objectContaining({ + id: "assistant-1", + segments: [ + { + argsText: '{"cmd":"ls","tail":true}', + kind: "tool_use", + path: null, + runId: API_DRIVER_BOUNDARY_IDS.sessionRun, + tool: "Shell", + toolCallId: "tool-1", + }, + { + kind: "tool_result", + output: "snapshot tail", + runId: API_DRIVER_BOUNDARY_IDS.sessionRun, + tool: "Shell", + toolCallId: "tool-1", + }, + ], + }), + ]); + expect( + projection.sessionDeliveryEvents.map(({ event }) => + event.type === EventType.CUSTOM ? event.name : event.type, + ), + ).toEqual(Array(3).fill(MOSOO_CUSTOM_EVENT.sessionToolUpdated.name)); + }); + test("apps admitted driver wire events into API runtime and viewer events", async () => { const link = createRuntimeSessionLink(); + const permissionRequest = { + driverInstanceId: API_DRIVER_BOUNDARY_IDS.driverInstance, + rawInput: "pwd", + requestId: "permission-1", + runId: API_DRIVER_BOUNDARY_IDS.sessionRun, + title: "Allow shell command?", + toolCallId: "tool-1", + toolKind: "shell", + }; const permissionRequested = createDriverEvent({ kind: "permission.requested", payload: { @@ -537,14 +615,23 @@ describe("API to driver boundary", () => { }, ], }); + const baseLiveState = createBaseLiveState({ + callerId: link.callerId, + creatorId: link.creatorId, + driverInstanceId: API_DRIVER_BOUNDARY_IDS.driverInstance, + sessionId: link.sessionId, + }); const projection = await appRuntimeDriverEvents({ DB: new SqliteD1Database() } as ApiBindings, { - currentLiveState: createBaseLiveState({ - callerId: link.callerId, - creatorId: link.creatorId, - driverInstanceId: API_DRIVER_BOUNDARY_IDS.driverInstance, - sessionId: link.sessionId, - }), + currentLiveState: { + ...baseLiveState, + lifecycle: "RUNNING", + run: { + ...baseLiveState.run, + id: API_DRIVER_BOUNDARY_IDS.sessionRun, + status: "running", + }, + }, driverInstanceId: API_DRIVER_BOUNDARY_IDS.driverInstance, events: batch.events, link, @@ -564,23 +651,22 @@ describe("API to driver boundary", () => { expect(projection.liveStateChanged).toBe(true); expect(projection.sessionDeliveryEvents[0]?.event).toMatchObject({ name: MOSOO_CUSTOM_EVENT.sessionPermissionsUpdated.name, + type: EventType.CUSTOM, value: { - permissionRequests: [ - { - driverInstanceId: API_DRIVER_BOUNDARY_IDS.driverInstance, - rawInput: "pwd", - requestId: "permission-1", - runId: API_DRIVER_BOUNDARY_IDS.sessionRun, - title: "Allow shell command?", - toolCallId: "tool-1", - toolKind: "shell", - }, - ], + permissionRequest, + permissionRequests: [], + }, + }); + expect(projection.nextLiveState).toMatchObject({ + permissionRequests: [permissionRequest], + run: { + id: API_DRIVER_BOUNDARY_IDS.sessionRun, + status: "waiting_input", }, }); }); - test("adds failed tool result delivery before terminal run update", async () => { + test("accepts a production Driver terminal envelope without inventing failed tool output", async () => { const link = createRuntimeSessionLink(); const baseLiveState = createBaseLiveState({ callerId: link.callerId, @@ -588,26 +674,51 @@ describe("API to driver boundary", () => { driverInstanceId: API_DRIVER_BOUNDARY_IDS.driverInstance, sessionId: link.sessionId, }); - const runFailed = createDriverEvent({ - kind: "run.failed", - payload: { - error: { - code: "runtime.failed", - message: "Runtime driver control socket is not connected.", + const pendingToolUse = { + argsText: '{"cmd":"pwd"}', + kind: "tool_use", + path: null, + tool: "Shell", + toolCallId: "tool-1", + } as const; + const runError = { + code: "runtime.failed", + details: {}, + message: "Runtime driver control socket is not connected.", + retryable: false, + }; + const transportSourceId = "driver-builder-run-failed"; + const [driverRunFailed] = toDriverRuntimeEventInput( + { + createId: () => API_DRIVER_BOUNDARY_IDS.runtimeEvent, + driverInstanceId: API_DRIVER_BOUNDARY_IDS.driverInstance, + occurredAt: "1970-01-01T00:00:01.000Z", + runId: API_DRIVER_BOUNDARY_IDS.sessionRun, + runtimeId: "openai-runtime", + sessionId: API_DRIVER_BOUNDARY_IDS.session, + sourceEventId: transportSourceId, + } as Parameters[0], + { + kind: "run.failed", + payload: { + error: runError, + recoverable: false, }, }, - runId: API_DRIVER_BOUNDARY_IDS.sessionRun, - }); + ); const batch = parseDriverEventBatchInput({ driverInstanceId: API_DRIVER_BOUNDARY_IDS.driverInstance, events: [ { - event: runFailed, - eventId: "source-run-failed", + event: driverRunFailed, + eventId: transportSourceId, occurredAt: "1970-01-01T00:00:01.000Z", }, ], }); + const canonicalEnvelope = canonicalizeDriverEventEnvelope(batch.events[0], { + traceId: link.traceId, + }); const projection = await appRuntimeDriverEvents({ DB: new SqliteD1Database() } as ApiBindings, { currentLiveState: { @@ -620,15 +731,7 @@ describe("API to driver boundary", () => { id: "assistant-1", plan: [], role: "assistant", - segments: [ - { - argsText: '{"cmd":"pwd"}', - kind: "tool_use", - path: null, - tool: "Shell", - toolCallId: "tool-1", - }, - ], + segments: [pendingToolUse], }, ], run: { @@ -638,35 +741,45 @@ describe("API to driver boundary", () => { }, }, driverInstanceId: API_DRIVER_BOUNDARY_IDS.driverInstance, - events: batch.events, + events: [canonicalEnvelope], link, }); const canonicalFailureSourceId = `session-run-terminal:${API_DRIVER_BOUNDARY_IDS.sessionRun}:run.failed`; + expect(canonicalEnvelope).toMatchObject({ + event: { sourceEventId: canonicalFailureSourceId, traceId: link.traceId }, + eventId: transportSourceId, + }); expect(projection.runtimeEvents).toMatchObject([{ sourceEventId: canonicalFailureSourceId }]); expect(projection.sessionDeliveryEvents.map((record) => record.sourceEventId)).toEqual([ canonicalFailureSourceId, - canonicalFailureSourceId, - canonicalFailureSourceId, - ]); - expect(projection.sessionDeliveryEvents.map((record) => record.event.type)).toEqual([ - EventType.TOOL_CALL_RESULT, - EventType.TOOL_CALL_END, - EventType.CUSTOM, ]); expect(projection.sessionDeliveryEvents[0]?.event).toMatchObject({ - content: - "Tool failed before returning a result: Runtime driver control socket is not connected.", - toolCallId: "tool-1", - type: EventType.TOOL_CALL_RESULT, + name: MOSOO_CUSTOM_EVENT.sessionRunUpdated.name, + type: EventType.CUSTOM, + value: { + lifecycle: "IDLE", + run: { + error: runError, + id: API_DRIVER_BOUNDARY_IDS.sessionRun, + status: "failed", + }, + }, }); - expect(projection.nextLiveState?.messages[0]?.segments.at(-1)).toEqual({ - kind: "tool_result", - output: - "Tool failed before returning a result: Runtime driver control socket is not connected.", - tool: "Shell", - toolCallId: "tool-1", + expect(projection.nextLiveState).toMatchObject({ + infra: { + driverInstanceId: null, + lastFailureMessage: runError.message, + lastFailureReason: runError.code, + }, + lifecycle: "IDLE", + run: { + error: runError, + id: API_DRIVER_BOUNDARY_IDS.sessionRun, + status: "failed", + }, }); + expect(projection.nextLiveState?.messages[0]?.segments).toEqual([pendingToolUse]); }); test("rejects legacy event shapes from the driver channel", () => { @@ -693,7 +806,9 @@ describe("API to driver boundary", () => { contentDelta: "wrong session", messageId: "message-1", }, + runtimeId: "openai-runtime", sessionId: "01J0000000000000000000000M", + traceId: "trace-1", }), { driverInstanceId: API_DRIVER_BOUNDARY_IDS.driverInstance, @@ -713,7 +828,9 @@ describe("API to driver boundary", () => { startedAt: "1970-01-01T00:00:00.010Z", }, runId: "01J0000000000000000000000P", + runtimeId: "openai-runtime", sessionId: API_DRIVER_BOUNDARY_IDS.session, + traceId: "trace-1", }), { driverInstanceId: API_DRIVER_BOUNDARY_IDS.driverInstance, @@ -733,7 +850,9 @@ describe("API to driver boundary", () => { contentDelta: "wrong driver", messageId: "message-1", }, + runtimeId: "openai-runtime", sessionId: API_DRIVER_BOUNDARY_IDS.session, + traceId: "trace-1", }), { driverInstanceId: API_DRIVER_BOUNDARY_IDS.driverInstance, @@ -753,7 +872,9 @@ describe("API to driver boundary", () => { contentDelta: "missing run", messageId: "message-1", }, + runtimeId: "openai-runtime", sessionId: API_DRIVER_BOUNDARY_IDS.session, + traceId: "trace-1", }), { driverInstanceId: API_DRIVER_BOUNDARY_IDS.driverInstance, @@ -773,7 +894,9 @@ describe("API to driver boundary", () => { messageId: "message-1", }, runId: API_DRIVER_BOUNDARY_IDS.sessionRun, + runtimeId: "openai-runtime", sessionId: API_DRIVER_BOUNDARY_IDS.session, + traceId: "trace-1", }), { driverInstanceId: API_DRIVER_BOUNDARY_IDS.driverInstance, @@ -794,7 +917,9 @@ describe("API to driver boundary", () => { messageId: "message-1", }, runId: API_DRIVER_BOUNDARY_IDS.sessionRun, + runtimeId: "openai-runtime", sessionId: API_DRIVER_BOUNDARY_IDS.session, + traceId: "trace-1", }), { driverInstanceId: API_DRIVER_BOUNDARY_IDS.driverInstance, @@ -813,7 +938,9 @@ describe("API to driver boundary", () => { occurredAt: "1970-01-01T00:00:00.010Z", payload: { tasks }, runId: API_DRIVER_BOUNDARY_IDS.sessionRun, + runtimeId: "openai-runtime", sessionId: API_DRIVER_BOUNDARY_IDS.session, + traceId: "trace-1", }); const terminalLink = { ...createRuntimeSessionLink(), @@ -834,42 +961,44 @@ describe("API to driver boundary", () => { ).not.toThrow(); }); - test("rejects canonical driver events whose source id disagrees with the envelope", () => { + test("rejects Driver transport source ids that disagree with their envelopes", () => { expect(() => - assertRuntimeEventMatchesDriverEnvelope( - createRuntimeEvent({ - id: API_DRIVER_BOUNDARY_IDS.runtimeEvent, - kind: "message.delta", - occurredAt: "1970-01-01T00:00:00.010Z", - payload: { - contentDelta: "wrong source", - messageId: "message-1", - }, - sessionId: API_DRIVER_BOUNDARY_IDS.session, - sourceEventId: "source-inner", - }), + canonicalizeDriverEventEnvelope( { + event: createRuntimeEvent({ + id: API_DRIVER_BOUNDARY_IDS.runtimeEvent, + kind: "message.delta", + occurredAt: "1970-01-01T00:00:00.010Z", + payload: { + contentDelta: "wrong source", + messageId: "message-1", + }, + sessionId: API_DRIVER_BOUNDARY_IDS.session, + sourceEventId: "source-inner", + }), eventId: "source-outer", }, + { traceId: null }, ), ).toThrow("Runtime driver event source id does not match the driver envelope."); expect(() => - assertRuntimeEventMatchesDriverEnvelope( - createRuntimeEvent({ - id: "01J0000000000000000000000H", - kind: "message.delta", - occurredAt: "1970-01-01T00:00:00.010Z", - payload: { - contentDelta: "ok", - messageId: "message-1", - }, - sessionId: API_DRIVER_BOUNDARY_IDS.session, - sourceEventId: "source-1", - }), + canonicalizeDriverEventEnvelope( { + event: createRuntimeEvent({ + id: "01J0000000000000000000000H", + kind: "message.delta", + occurredAt: "1970-01-01T00:00:00.010Z", + payload: { + contentDelta: "ok", + messageId: "message-1", + }, + sessionId: API_DRIVER_BOUNDARY_IDS.session, + sourceEventId: "source-1", + }), eventId: "source-1", }, + { traceId: null }, ), ).not.toThrow(); }); diff --git a/apps/api/tests/app-agent-bound-run-revocation.test.ts b/apps/api/tests/app-agent-bound-run-revocation.test.ts index ee480edb..ac8ed7ab 100644 --- a/apps/api/tests/app-agent-bound-run-revocation.test.ts +++ b/apps/api/tests/app-agent-bound-run-revocation.test.ts @@ -15,6 +15,12 @@ import { SessionRunCreationGuardRejectedError, } from "../src/modules/runtime/application/session-run.service"; import type { ApiBindings } from "../src/platform/cloudflare/worker-types"; +import { + BOUND_DEPLOYMENT_ID, + BOUND_DEPLOYMENT_RUN_ID, + deleteBoundDeployment, + insertBoundDeployment, +} from "./bound-capability-fixtures"; import { PUBLIC_API_TEST_IDS, createPublicHttpContractDatabase, @@ -24,9 +30,6 @@ import { } from "./helpers/public-api-http-test-fixture"; import type { SqliteD1Database } from "./helpers/public-api-http-test-fixture"; -const DEPLOYMENT_ID = "01J0000000000000000000000D"; -const DEPLOYMENT_RUN_ID = "01J0000000000000000000000R"; - const CLAIMS: AppAgentCapabilityClaims = { agentId: PUBLIC_API_TEST_IDS.agent, appId: PUBLIC_API_TEST_IDS.app, @@ -35,48 +38,11 @@ const CLAIMS: AppAgentCapabilityClaims = { expose: "public_thread", name: "Public API Agent", }, - deploymentId: DEPLOYMENT_ID, - deploymentRunId: DEPLOYMENT_RUN_ID, + deploymentId: BOUND_DEPLOYMENT_ID, + deploymentRunId: BOUND_DEPLOYMENT_RUN_ID, exp: Date.now() + 60_000, }; -async function insertDeploymentAuthority(database: SqliteD1Database): Promise { - database.execute(` - CREATE TABLE app_deployment ( - app_id text NOT NULL, - deleted_at integer, - id text PRIMARY KEY NOT NULL - ); - - CREATE TABLE app_deployment_run ( - app_id text NOT NULL, - deployment_id text NOT NULL, - id text PRIMARY KEY NOT NULL, - plan_json text, - status text NOT NULL - ); - - CREATE INDEX app_deployment_run_deployment_id_idx - ON app_deployment_run (deployment_id, id); - `); - - await database - .prepare("INSERT INTO app_deployment (app_id, deleted_at, id) VALUES (?, NULL, ?)") - .bind(PUBLIC_API_TEST_IDS.app, DEPLOYMENT_ID) - .run(); - await database - .prepare( - "INSERT INTO app_deployment_run (app_id, deployment_id, id, plan_json, status) VALUES (?, ?, ?, ?, 'success')", - ) - .bind( - PUBLIC_API_TEST_IDS.app, - DEPLOYMENT_ID, - DEPLOYMENT_RUN_ID, - JSON.stringify({ agentBindings: [CLAIMS.binding] }), - ) - .run(); -} - function revokeDeploymentWhenRunInsertStarts(database: SqliteD1Database): D1Database { let revoked = false; @@ -99,10 +65,7 @@ function revokeDeploymentWhenRunInsertStarts(database: SqliteD1Database): D1Data if (typeof method === "function") { return async (...args: unknown[]) => { revoked = true; - await database - .prepare("UPDATE app_deployment SET deleted_at = ? WHERE id = ?") - .bind(Date.now(), DEPLOYMENT_ID) - .run(); + await deleteBoundDeployment(database); return method.apply(target, args); }; } @@ -178,7 +141,7 @@ describe("bound Agent Run revocation boundary", () => { test("creates a Run while the claimed deployment authority remains current", async () => { const database = await createPublicHttpContractDatabase(); await insertOwnerSession(database); - await insertDeploymentAuthority(database); + await insertBoundDeployment(database, { agentBindings: [CLAIMS.binding] }); const viewer = await getAccountViewer(database, PUBLIC_API_TEST_IDS.ownerAccount); if (viewer === null) { @@ -258,7 +221,7 @@ describe("bound Agent Run revocation boundary", () => { test("does not expose accepted Run provenance to a non-owner", async () => { const database = await createPublicHttpContractDatabase(); await insertOwnerSession(database); - await insertDeploymentAuthority(database); + await insertBoundDeployment(database, { agentBindings: [CLAIMS.binding] }); const owner = await getAccountViewer(database, PUBLIC_API_TEST_IDS.ownerAccount); const nonOwner = await getAccountViewer(database, PUBLIC_API_TEST_IDS.nonOwnerAccount); @@ -358,7 +321,7 @@ describe("bound Agent Run revocation boundary", () => { test("does not insert a Run when deletion commits after preflight authorization", async () => { const database = await createPublicHttpContractDatabase(); await insertOwnerSession(database); - await insertDeploymentAuthority(database); + await insertBoundDeployment(database, { agentBindings: [CLAIMS.binding] }); const viewer = await getAccountViewer(database, PUBLIC_API_TEST_IDS.ownerAccount); if (viewer === null) { diff --git a/apps/api/tests/app-agent-capability-revocation-http.test.ts b/apps/api/tests/app-agent-capability-revocation-http.test.ts index 5e969a43..7b409734 100644 --- a/apps/api/tests/app-agent-capability-revocation-http.test.ts +++ b/apps/api/tests/app-agent-capability-revocation-http.test.ts @@ -6,6 +6,12 @@ import { registerPublicApiRoute } from "../src/adapters/http/routes/public-api-r import { mintAppAgentCapabilityToken } from "../src/modules/public-api/app-agent-capability"; import type { AppAgentCapabilityClaims } from "../src/modules/public-api/app-agent-capability"; import type { ApiBindings, ApiGatewayEnvironment } from "../src/platform/cloudflare/worker-types"; +import { + BOUND_DEPLOYMENT_ID, + BOUND_DEPLOYMENT_RUN_ID, + deleteBoundDeployment, + insertBoundDeployment, +} from "./bound-capability-fixtures"; import { PUBLIC_API_TEST_IDS, createPublicHttpContractDatabase, @@ -14,9 +20,6 @@ import { } from "./helpers/public-api-http-test-fixture"; import type { SqliteD1Database } from "./helpers/public-api-http-test-fixture"; -const DEPLOYMENT_ID = "01J0000000000000000000000D"; -const DEPLOYMENT_RUN_ID = "01J0000000000000000000000R"; - function createBoundAgentRouteTestApp(): Hono { const app = new Hono(); const publicApi = new Hono(); @@ -37,54 +40,13 @@ function capabilityClaims( expose: "public_thread", name: "Public API Agent", }, - deploymentId: DEPLOYMENT_ID, - deploymentRunId: DEPLOYMENT_RUN_ID, + deploymentId: BOUND_DEPLOYMENT_ID, + deploymentRunId: BOUND_DEPLOYMENT_RUN_ID, exp: Date.now() + 60_000, ...overrides, }; } -async function insertDeploymentAuthority( - database: SqliteD1Database, - input: { agentBindings: unknown[]; deletedAt: number | null }, -): Promise { - database.execute(` - CREATE TABLE app_deployment ( - app_id text NOT NULL, - deleted_at integer, - id text PRIMARY KEY NOT NULL - ); - - CREATE TABLE app_deployment_run ( - app_id text NOT NULL, - deployment_id text NOT NULL, - id text PRIMARY KEY NOT NULL, - plan_json text, - status text NOT NULL - ); - - CREATE INDEX app_deployment_run_deployment_id_idx - ON app_deployment_run (deployment_id, id); - `); - - await database - .prepare("INSERT INTO app_deployment (app_id, deleted_at, id) VALUES (?, ?, ?)") - .bind(PUBLIC_API_TEST_IDS.app, input.deletedAt, DEPLOYMENT_ID) - .run(); - await database - .prepare( - "INSERT INTO app_deployment_run (app_id, deployment_id, id, plan_json, status) VALUES (?, ?, ?, ?, ?)", - ) - .bind( - PUBLIC_API_TEST_IDS.app, - DEPLOYMENT_ID, - DEPLOYMENT_RUN_ID, - JSON.stringify({ agentBindings: input.agentBindings }), - "success", - ) - .run(); -} - async function requestBoundAgent( database: D1Database, claims: AppAgentCapabilityClaims, @@ -139,10 +101,7 @@ function revokeDeploymentWhenRunInsertStarts(database: SqliteD1Database): D1Data if (typeof method === "function") { return async (...args: unknown[]) => { revoked = true; - await database - .prepare("UPDATE app_deployment SET deleted_at = ? WHERE id = ?") - .bind(Date.now(), DEPLOYMENT_ID) - .run(); + await deleteBoundDeployment(database); return method.apply(target, args); }; } @@ -168,7 +127,7 @@ async function expectNoSessions(database: SqliteD1Database): Promise { describe("bound Agent capability revocation HTTP boundary", () => { test("rejects a deleted deployment capability before it can create a Session", async () => { const database = await createPublicHttpContractDatabase(); - await insertDeploymentAuthority(database, { + await insertBoundDeployment(database, { agentBindings: [capabilityClaims().binding], deletedAt: Date.now(), }); @@ -219,7 +178,7 @@ describe("bound Agent capability revocation HTTP boundary", () => { test("rejects a capability whose current successful revision removed its binding", async () => { const database = await createPublicHttpContractDatabase(); - await insertDeploymentAuthority(database, { + await insertBoundDeployment(database, { agentBindings: [], deletedAt: null, }); @@ -238,7 +197,7 @@ describe("bound Agent capability revocation HTTP boundary", () => { test("cleans up the new Session when deletion wins the final Run creation race", async () => { const database = await createPublicHttpContractDatabase(); - await insertDeploymentAuthority(database, { + await insertBoundDeployment(database, { agentBindings: [capabilityClaims().binding], deletedAt: null, }); diff --git a/apps/api/tests/app-deployment-cloudflare-client.test.ts b/apps/api/tests/app-deployment-cloudflare-client.test.ts index 9721ee3b..58a5b291 100644 --- a/apps/api/tests/app-deployment-cloudflare-client.test.ts +++ b/apps/api/tests/app-deployment-cloudflare-client.test.ts @@ -1,38 +1,81 @@ import { describe, expect, test } from "bun:test"; -import { createWorkerModuleUpload } from "../src/modules/apps/application/app-deployment-cloudflare-client"; +import { + createStaticAssetsUploadForm, + createWorkerModuleUpload, + validateStaticAssetsManifest, +} from "../src/modules/apps/application/app-deployment-cloudflare-client"; describe("app deployment Cloudflare client", () => { - test("uses the module name and metadata as multipart part names", async () => { + test("builds an atomic WfP module upload", async () => { const scriptContent = "export default { fetch() {} };"; + const tags = ["mosoo-managed", "d-01j0000000000000000000000d"]; const upload = createWorkerModuleUpload({ compatibilityDate: "2026-07-14", mainModuleName: "worker.js", scriptContent, scriptName: "example", + tags, vars: { MOSOO_AGENT_URL: "https://example.com/bound/token" }, }); - const modulePart = upload.get("worker.js"); + const modulePart = upload.files[0]; - expect(upload).toBeInstanceOf(FormData); expect(modulePart).toBeInstanceOf(File); - expect((modulePart as File).name).toBe("worker.js"); - expect((modulePart as File).type).toBe("application/javascript+module"); - expect(await (modulePart as File).text()).toBe(scriptContent); - expect(upload.get("files")).toBeNull(); - expect(upload.get("metadata")).toBe( - JSON.stringify({ - bindings: [ - { - name: "MOSOO_AGENT_URL", - text: "https://example.com/bound/token", - type: "plain_text", - }, - ], - compatibility_date: "2026-07-14", - main_module: "worker.js", - }), - ); + expect(modulePart.name).toBe("worker.js"); + expect(modulePart.type).toBe("application/javascript+module"); + expect(await modulePart.text()).toBe(scriptContent); + expect(upload.metadata).toEqual({ + bindings: [ + { + name: "MOSOO_AGENT_URL", + text: "https://example.com/bound/token", + type: "secret_text", + }, + ], + compatibility_date: "2026-07-14", + main_module: "worker.js", + tags, + }); + }); + + test("builds the official assets-only multipart upload without a wrapper module", () => { + const form = createStaticAssetsUploadForm({ + compatibilityDate: "2026-07-14", + completionToken: "completion-token", + headers: "/assets/*\n Cache-Control: public\n", + redirects: "/old /new 301\n", + tags: ["mosoo-managed"], + }); + + expect([...form.keys()]).toEqual(["metadata"]); + const metadata = form.get("metadata"); + + if (typeof metadata !== "string") { + throw new Error("Expected Static Assets upload metadata."); + } + + expect(JSON.parse(metadata)).toEqual({ + assets: { + config: { + _headers: "/assets/*\n Cache-Control: public\n", + _redirects: "/old /new 301\n", + }, + jwt: "completion-token", + }, + compatibility_date: "2026-07-14", + tags: ["mosoo-managed"], + }); }); + + test.each(["/../secret", "/assets//app.js", "/assets/./app.js", "/assets\\app.js"])( + "rejects an untrusted Static Assets manifest path %s", + (path) => { + expect(() => + validateStaticAssetsManifest({ + [path]: { hash: "0123456789abcdef0123456789abcdef", size: 1 }, + }), + ).toThrow("invalid path"); + }, + ); }); diff --git a/apps/api/tests/app-deployment-detector.test.ts b/apps/api/tests/app-deployment-detector.test.ts index e5b4c784..6a37bee8 100644 --- a/apps/api/tests/app-deployment-detector.test.ts +++ b/apps/api/tests/app-deployment-detector.test.ts @@ -5,10 +5,8 @@ import { detectAppDeploymentPlan, } from "../src/modules/apps/application/app-deployment-detector"; -const RESOURCE_NAME = "app-01j00000000000000000000054"; - function detect(files: Record) { - return detectAppDeploymentPlan({ files }, { resourceName: RESOURCE_NAME }); + return detectAppDeploymentPlan({ files }); } describe("app deployment detector", () => { @@ -19,17 +17,31 @@ describe("app deployment detector", () => { outputDir: ".", packageManager: "none", rootDir: ".", - targetKind: "cloudflare_pages", + targetKind: "cloudflare_static_assets", targetMode: "static_assets", }); }); - test("uses the caller-provided Cloudflare resource name", () => { - expect(detect({ "index.html": "
Hello
" }).generatedWranglerConfig).toContain( - `name = "${RESOURCE_NAME}"`, - ); + test("keeps the static plan physical-name independent", () => { + const config = detect({ "index.html": "
Hello
" }).generatedWranglerConfig; + + expect(config).toContain("[assets]"); + expect(config).toContain('directory = "artifact"'); + expect(config).not.toContain("name ="); }); + test.each(["_worker.js", "_routes.json", "functions/api.js"])( + "rejects unsupported static asset path %s", + (path) => { + expect(() => + detect({ + "index.html": "
Hello
", + [path]: "export default {};", + }), + ).toThrow(AppDeploymentDetectionError); + }, + ); + test("detects Vite static output", () => { expect( detect({ @@ -44,7 +56,7 @@ describe("app deployment detector", () => { installCommand: "pnpm install --frozen-lockfile", outputDir: "dist", packageManager: "pnpm", - targetKind: "cloudflare_pages", + targetKind: "cloudflare_static_assets", }); }); @@ -101,7 +113,7 @@ describe("app deployment detector", () => { installCommand: "npm ci", outputDir: "out", packageManager: "npm", - targetKind: "cloudflare_pages", + targetKind: "cloudflare_static_assets", }); }); @@ -129,7 +141,7 @@ fallback = "index.html" outputDir: "public", routesFallback: "index.html", rootDir: "site", - targetKind: "cloudflare_pages", + targetKind: "cloudflare_static_assets", }); }); diff --git a/apps/api/tests/app-deployment-gateway.test.ts b/apps/api/tests/app-deployment-gateway.test.ts new file mode 100644 index 00000000..482a6fd8 --- /dev/null +++ b/apps/api/tests/app-deployment-gateway.test.ts @@ -0,0 +1,402 @@ +import { describe, expect, test } from "bun:test"; + +import { + APP_DEPLOYMENT_PROBE_HOST_LABEL, + APP_DEPLOYMENT_PROBE_PATH_PREFIX, + APP_DEPLOYMENT_PROBE_SCRIPT_NAME, + dispatchAppDeploymentGatewayRequest, + parseAppDeploymentSubdomain, +} from "../src/modules/apps/application/app-deployment-gateway"; +import type { + AppDeploymentDispatchNamespace, + AppDeploymentGatewayOptions, +} from "../src/modules/apps/application/app-deployment-gateway"; +import { createApiWorker } from "../src/platform/cloudflare/create-api-worker"; +import type { ApiBindings } from "../src/platform/cloudflare/worker-types"; +import { SqliteD1Database } from "./helpers/sqlite-d1"; + +const APP_ID = "01J0000000000000000000000Q"; +const OTHER_APP_ID = "01J0000000000000000000000R"; +const MOSOO_SUBDOMAIN = `app-${APP_ID.toLowerCase()}`; +const OTHER_MOSOO_SUBDOMAIN = `app-${OTHER_APP_ID.toLowerCase()}`; +const DOMAIN = "apps.mosoo.ai"; + +function gatewayOptions( + overrides: Partial = {}, +): AppDeploymentGatewayOptions { + return { + appDeploymentDomain: DOMAIN, + dispatcher: { + get() { + throw new Error("Unexpected dispatch"); + }, + }, + resolveActiveScriptName: async () => null, + ...overrides, + }; +} + +function createGatewayDatabase(): SqliteD1Database { + const database = new SqliteD1Database(); + database.execute(` + CREATE TABLE app_deployment ( + active_script_name text, + deleted_at integer, + mosoo_subdomain text NOT NULL + ); + `); + return database; +} + +async function insertGatewayDeployment( + database: D1Database, + input: { activeScriptName: string | null; deletedAt: number | null; mosooSubdomain: string }, +): Promise { + await database + .prepare( + `INSERT INTO app_deployment (active_script_name, deleted_at, mosoo_subdomain) + VALUES (?, ?, ?)`, + ) + .bind(input.activeScriptName, input.deletedAt, input.mosooSubdomain) + .run(); +} + +function getApiWorkerFetch(): NonNullable["fetch"]> { + const fetch = createApiWorker().fetch; + + if (fetch === undefined || typeof fetch !== "function") { + throw new Error("API Worker fetch handler is unavailable."); + } + + return fetch; +} + +function createGatewayBindings( + database: D1Database, + dispatcher: AppDeploymentDispatchNamespace, +): ApiBindings { + return { + APP_DEPLOYMENT_DISPATCHER: dispatcher, + DB: database, + MOSOO_APP_DEPLOYMENT_DOMAIN: DOMAIN, + } as ApiBindings; +} + +describe("app deployment gateway", () => { + test("parses only the exact one-label app hostname", () => { + expect(parseAppDeploymentSubdomain(`${MOSOO_SUBDOMAIN}.${DOMAIN}`, DOMAIN)).toBe( + MOSOO_SUBDOMAIN, + ); + expect(parseAppDeploymentSubdomain(`${MOSOO_SUBDOMAIN.toUpperCase()}.${DOMAIN}.`, DOMAIN)).toBe( + MOSOO_SUBDOMAIN, + ); + + for (const hostname of [ + DOMAIN, + `foo.${DOMAIN}`, + `x.${MOSOO_SUBDOMAIN}.${DOMAIN}`, + `app-81j0000000000000000000000q.${DOMAIN}`, + `${MOSOO_SUBDOMAIN}.evil-${DOMAIN}`, + ]) { + expect(parseAppDeploymentSubdomain(hostname, DOMAIN)).toBeNull(); + } + }); + + test("leaves unrelated hosts for the API router", async () => { + let resolved = false; + const response = await dispatchAppDeploymentGatewayRequest( + new Request("https://cloud.mosoo.ai/api/graphql"), + gatewayOptions({ + resolveActiveScriptName: async () => { + resolved = true; + return null; + }, + }), + ); + + expect(response).toBeNull(); + expect(resolved).toBe(false); + }); + + test("fails closed for an invalid app-domain host", async () => { + let resolved = false; + const response = await dispatchAppDeploymentGatewayRequest( + new Request(`https://x.${MOSOO_SUBDOMAIN}.${DOMAIN}/private`), + gatewayOptions({ + resolveActiveScriptName: async () => { + resolved = true; + return null; + }, + }), + ); + + expect(response?.status).toBe(404); + expect(response?.headers.get("Cache-Control")).toBe("no-store"); + expect(resolved).toBe(false); + }); + + test("exposes the operational probe only on its exact hostname and path", async () => { + let dispatched = false; + let resolved = false; + const options = gatewayOptions({ + dispatcher: { + get() { + dispatched = true; + throw new Error("Unexpected dispatch"); + }, + }, + resolveActiveScriptName: async () => { + resolved = true; + return null; + }, + }); + + for (const url of [ + `https://${APP_DEPLOYMENT_PROBE_HOST_LABEL}.${DOMAIN}/private`, + `https://other.${DOMAIN}${APP_DEPLOYMENT_PROBE_PATH_PREFIX}http`, + ]) { + const response = await dispatchAppDeploymentGatewayRequest(new Request(url), options); + expect(response?.status).toBe(404); + expect(response?.headers.get("Cache-Control")).toBe("no-store"); + } + + expect(dispatched).toBe(false); + expect(resolved).toBe(false); + }); + + test("returns an uncached 404 when no script is active", async () => { + const resolvedSubdomains: string[] = []; + const response = await dispatchAppDeploymentGatewayRequest( + new Request(`https://${MOSOO_SUBDOMAIN}.${DOMAIN}/`), + gatewayOptions({ + resolveActiveScriptName: async (subdomain) => { + resolvedSubdomains.push(subdomain); + return null; + }, + }), + ); + + expect(resolvedSubdomains).toEqual([MOSOO_SUBDOMAIN]); + expect(response?.status).toBe(404); + expect(response?.headers.get("Cache-Control")).toBe("no-store"); + }); + + test("returns 503 for the app domain when the dispatch binding is missing", async () => { + let resolved = false; + const response = await dispatchAppDeploymentGatewayRequest( + new Request(`https://${MOSOO_SUBDOMAIN}.${DOMAIN}/`), + gatewayOptions({ + dispatcher: undefined, + resolveActiveScriptName: async () => { + resolved = true; + return "must-not-dispatch"; + }, + }), + ); + + expect(response?.status).toBe(503); + expect(response?.headers.get("Cache-Control")).toBe("no-store"); + expect(resolved).toBe(false); + }); + + test("dispatches the original request and returns the user Worker response unchanged", async () => { + const request = new Request(`https://${MOSOO_SUBDOMAIN}.${DOMAIN}/events?after=42`, { + body: "stream me", + headers: { "Content-Type": "text/plain" }, + method: "POST", + }); + const workerResponse = new Response("worker stream", { + headers: { "X-Worker": "attempt-7" }, + }); + const calls: Array<{ request: Request; scriptName: string }> = []; + const dispatcher: AppDeploymentDispatchNamespace = { + get(scriptName) { + return { + fetch(receivedRequest) { + calls.push({ request: receivedRequest, scriptName }); + return workerResponse; + }, + }; + }, + }; + + const response = await dispatchAppDeploymentGatewayRequest( + request, + gatewayOptions({ + dispatcher, + resolveActiveScriptName: async () => "mosoo-run-g2-a7", + }), + ); + + expect(calls).toEqual([{ request, scriptName: "mosoo-run-g2-a7" }]); + expect(request.bodyUsed).toBe(false); + expect(response).toBe(workerResponse); + }); + + test("returns 503 without dispatching when active-script resolution fails", async () => { + let dispatched = false; + const lookupError = new Error("D1 unavailable"); + + const response = await dispatchAppDeploymentGatewayRequest( + new Request(`https://${MOSOO_SUBDOMAIN}.${DOMAIN}/`), + gatewayOptions({ + dispatcher: { + get() { + dispatched = true; + throw new Error("Unexpected dispatch"); + }, + }, + resolveActiveScriptName: async () => { + throw lookupError; + }, + }), + ); + + expect(response?.status).toBe(503); + expect(response?.headers.get("Cache-Control")).toBe("no-store"); + expect(await response?.text()).toBe(""); + expect(dispatched).toBe(false); + }); + + test("returns 503 without leaking missing or failed user Worker details", async () => { + const dispatchers: AppDeploymentDispatchNamespace[] = [ + { + get() { + throw new Error("Worker not found"); + }, + }, + { + get() { + return { + fetch() { + throw new Error("User Worker failed"); + }, + }; + }, + }, + ]; + + for (const dispatcher of dispatchers) { + const response = await dispatchAppDeploymentGatewayRequest( + new Request(`https://${MOSOO_SUBDOMAIN}.${DOMAIN}/`), + gatewayOptions({ + dispatcher, + resolveActiveScriptName: async () => "missing-attempt-script", + }), + ); + + expect(response?.status).toBe(503); + expect(response?.headers.get("Cache-Control")).toBe("no-store"); + expect(await response?.text()).toBe(""); + } + }); + + test("routes the API Worker through the active D1 script before Hono", async () => { + const database = createGatewayDatabase(); + await insertGatewayDeployment(database, { + activeScriptName: "mosoo-run-g2-a7", + deletedAt: null, + mosooSubdomain: MOSOO_SUBDOMAIN, + }); + + const request = new Request(`https://${MOSOO_SUBDOMAIN}.${DOMAIN}/events?after=42`, { + body: "stream me", + method: "POST", + }); + const workerResponse = new Response("worker stream"); + let dispatchedRequest: Request | null = null; + const bindings = createGatewayBindings(database, { + get(scriptName) { + expect(scriptName).toBe("mosoo-run-g2-a7"); + return { + fetch(receivedRequest) { + dispatchedRequest = receivedRequest; + return workerResponse; + }, + }; + }, + }); + + const response = await getApiWorkerFetch()(request, bindings, {} as ExecutionContext); + + expect(dispatchedRequest).toBe(request); + expect(request.bodyUsed).toBe(false); + expect(response).toBe(workerResponse); + }); + + test("routes the pre-provisioned bootstrap probe through the API Worker without D1", async () => { + const request = new Request( + `https://${APP_DEPLOYMENT_PROBE_HOST_LABEL}.${DOMAIN}${APP_DEPLOYMENT_PROBE_PATH_PREFIX}http`, + { body: "probe", method: "POST" }, + ); + const workerResponse = new Response("probe-ok", { + headers: { "Cache-Control": "no-store" }, + }); + let dispatchedRequest: Request | null = null; + const bindings = createGatewayBindings(new SqliteD1Database(), { + get(scriptName) { + expect(scriptName).toBe(APP_DEPLOYMENT_PROBE_SCRIPT_NAME); + return { + fetch(receivedRequest) { + dispatchedRequest = receivedRequest; + return workerResponse; + }, + }; + }, + }); + + const response = await getApiWorkerFetch()(request, bindings, {} as ExecutionContext); + + expect(dispatchedRequest).toBe(request); + expect(request.bodyUsed).toBe(false); + expect(response).toBe(workerResponse); + }); + + test("the API Worker fails closed on an app host without the dispatch binding", async () => { + const response = await getApiWorkerFetch()( + new Request(`https://${MOSOO_SUBDOMAIN}.${DOMAIN}/`), + { + DB: createGatewayDatabase(), + MOSOO_APP_DEPLOYMENT_DOMAIN: DOMAIN, + } as ApiBindings, + {} as ExecutionContext, + ); + + expect(response.status).toBe(503); + expect(response.headers.get("Cache-Control")).toBe("no-store"); + }); + + test("does not dispatch unpublished or deleted D1 deployments", async () => { + const database = createGatewayDatabase(); + await insertGatewayDeployment(database, { + activeScriptName: null, + deletedAt: null, + mosooSubdomain: MOSOO_SUBDOMAIN, + }); + await insertGatewayDeployment(database, { + activeScriptName: "stale-deleted-script", + deletedAt: 1, + mosooSubdomain: OTHER_MOSOO_SUBDOMAIN, + }); + + let dispatchCount = 0; + const bindings = createGatewayBindings(database, { + get() { + dispatchCount += 1; + throw new Error("Unexpected dispatch"); + }, + }); + + for (const subdomain of [MOSOO_SUBDOMAIN, OTHER_MOSOO_SUBDOMAIN]) { + const response = await getApiWorkerFetch()( + new Request(`https://${subdomain}.${DOMAIN}/`), + bindings, + {} as ExecutionContext, + ); + + expect(response.status).toBe(404); + expect(response.headers.get("Cache-Control")).toBe("no-store"); + } + expect(dispatchCount).toBe(0); + }); +}); diff --git a/apps/api/tests/app-deployment-script-reconciliation.test.ts b/apps/api/tests/app-deployment-script-reconciliation.test.ts new file mode 100644 index 00000000..43b85ff5 --- /dev/null +++ b/apps/api/tests/app-deployment-script-reconciliation.test.ts @@ -0,0 +1,809 @@ +import { describe, expect, test } from "bun:test"; + +import type { ApiCommandId } from "@mosoo/db"; +import { createPlatformId } from "@mosoo/id"; +import type { AppDeploymentId, AppDeploymentRunId, AppId } from "@mosoo/id"; + +import { MANAGED_PROD_SCHEMA_TRIGGERS } from "../bin/prod-schema-guard"; +import { + APP_DEPLOYMENT_SCRIPT_GRACE_MS, + markAppDeploymentScriptUploadStarted, + reconcileAppDeploymentScriptPage, + registerAppDeploymentScriptCandidate, +} from "../src/modules/apps/application/app-deployment-script-reconciliation.service"; +import type { + AppDeploymentScriptCandidateAuthority, + AppDeploymentScriptCleanupAction, +} from "../src/modules/apps/application/app-deployment-script-reconciliation.service"; +import { applyDrizzleMigration, drizzleMigrations } from "./helpers/drizzle-migrations"; +import { SqliteD1Database } from "./helpers/sqlite-d1"; + +const ACTIVE_RUN_STATUSES = [ + "queued", + "preparing", + "building", + "submitting", + "submitted", + "activating", +] as const; + +interface AttemptFixture { + readonly appId: AppId; + readonly authority: AppDeploymentScriptCandidateAuthority; + readonly commandId: ApiCommandId; + readonly database: SqliteD1Database; + readonly deploymentId: AppDeploymentId; + readonly registeredAt: number; + readonly runId: AppDeploymentRunId; + readonly scriptName: string; +} + +function createDatabase(): SqliteD1Database { + const database = new SqliteD1Database(); + for (const migration of drizzleMigrations) { + applyDrizzleMigration(database, migration.tag); + } + return database; +} + +function candidateScriptName( + commandId: ApiCommandId, + deliveryGeneration: number, + attemptCount: number, +): string { + return `app-${commandId.toLowerCase()}-g${deliveryGeneration.toString(36)}-a${attemptCount.toString(36)}`; +} + +function authority( + commandId: ApiCommandId, + input: { attemptCount: number; claimOwner: string; deliveryGeneration?: number }, +): AppDeploymentScriptCandidateAuthority { + return { + attemptCount: input.attemptCount, + claimOwner: input.claimOwner, + commandId, + deliveryGeneration: input.deliveryGeneration ?? 1, + requireOwnership: async () => undefined, + }; +} + +async function insertAttemptFoundation( + database: SqliteD1Database, + input: { + readonly registeredAt: number; + readonly status?: (typeof ACTIVE_RUN_STATUSES)[number]; + }, +): Promise> { + const appId = createPlatformId(); + const commandId = createPlatformId(); + const deploymentId = createPlatformId(); + const runId = createPlatformId(); + const claimOwner = crypto.randomUUID(); + + await database + .prepare( + `INSERT INTO app_deployment ( + active_script_name, app_id, created_at, default_branch, deleted_at, id, + last_successful_url, latest_run_id, mosoo_subdomain, owner_account_id, + repo_name, repo_owner, repo_url, source_kind, updated_at + ) VALUES (NULL, ?, ?, 'main', NULL, ?, NULL, ?, ?, ?, 'repo', 'owner', + 'https://example.test/repo', 'github_public', ?)`, + ) + .bind( + appId, + input.registeredAt, + deploymentId, + runId, + `app-${deploymentId.toLowerCase()}`, + createPlatformId(), + input.registeredAt, + ) + .run(); + await database + .prepare( + `INSERT INTO app_deployment_run ( + app_id, created_at, deployment_id, id, source_branch, source_commit_sha, + status, updated_at + ) VALUES (?, ?, ?, ?, 'main', '0123456789abcdef', ?, ?)`, + ) + .bind( + appId, + input.registeredAt, + deploymentId, + runId, + input.status ?? "queued", + input.registeredAt, + ) + .run(); + await database + .prepare( + `INSERT INTO api_command ( + attempt_count, claim_expires_at, claim_owner, completed_at, created_at, + dedupe_key, delivery_generation, id, kind, last_error_code, + last_error_message, payload_json, status, updated_at + ) VALUES (1, ?, ?, NULL, ?, ?, 1, ?, 'app_deployment_run_dispatch', NULL, + NULL, ?, 'running', ?)`, + ) + .bind( + Date.now() + 7 * APP_DEPLOYMENT_SCRIPT_GRACE_MS, + claimOwner, + input.registeredAt, + `dispatch:${commandId}`, + commandId, + JSON.stringify({ appDeploymentRunId: runId }), + input.registeredAt, + ) + .run(); + + return { appId, commandId, database, deploymentId, registeredAt: input.registeredAt, runId }; +} + +async function createAttempt( + database: SqliteD1Database, + input: { + readonly registeredAt?: number; + readonly status?: (typeof ACTIVE_RUN_STATUSES)[number]; + } = {}, +): Promise { + const foundation = await insertAttemptFoundation(database, { + registeredAt: input.registeredAt ?? Date.now(), + status: input.status, + }); + const claimOwner = ( + await database + .prepare("SELECT claim_owner FROM api_command WHERE id = ?") + .bind(foundation.commandId) + .first<{ claim_owner: string }>() + )?.claim_owner; + if (claimOwner === undefined) { + throw new Error("App deployment command fixture is missing its claim owner."); + } + const attemptAuthority = authority(foundation.commandId, { attemptCount: 1, claimOwner }); + const scriptName = candidateScriptName(foundation.commandId, 1, 1); + await registerAppDeploymentScriptCandidate(database, { + authority: attemptAuthority, + deploymentId: foundation.deploymentId, + nowMs: foundation.registeredAt, + runId: foundation.runId, + scriptName, + }); + return { ...foundation, authority: attemptAuthority, scriptName }; +} + +async function createReconciliationAuthority( + database: SqliteD1Database, + nowMs: number, +): Promise { + const commandId = createPlatformId(); + const claimOwner = crypto.randomUUID(); + await database + .prepare( + `INSERT INTO api_command ( + attempt_count, claim_expires_at, claim_owner, completed_at, created_at, + dedupe_key, delivery_generation, id, kind, last_error_code, + last_error_message, payload_json, status, updated_at + ) VALUES (1, ?, ?, NULL, ?, ?, 1, ?, 'app_deployment_script_reconciliation', + NULL, NULL, '{}', 'running', ?)`, + ) + .bind( + nowMs + 7 * APP_DEPLOYMENT_SCRIPT_GRACE_MS, + claimOwner, + nowMs, + `script-reconciliation:${commandId}`, + commandId, + nowMs, + ) + .run(); + return authority(commandId, { attemptCount: 1, claimOwner }); +} + +async function expireAttemptAuthority(fixture: AttemptFixture): Promise { + await fixture.database + .prepare( + `UPDATE api_command + SET claim_expires_at = NULL, claim_owner = NULL, status = 'failed' + WHERE id = ?`, + ) + .bind(fixture.commandId) + .run(); +} + +async function retireScriptThroughTerminalRun(fixture: AttemptFixture): Promise { + await fixture.database + .prepare("UPDATE app_deployment_run SET status = 'failed' WHERE id = ?") + .bind(fixture.runId) + .run(); + const retireAfter = (await readScript(fixture)).retire_after; + if (typeof retireAfter !== "number") { + throw new Error("Terminal App deployment run did not retire its script candidate."); + } + return retireAfter; +} + +async function seedExpiredHistoricalScript(fixture: AttemptFixture, dueAt: number): Promise { + const retireAuthority = MANAGED_PROD_SCHEMA_TRIGGERS.find( + ({ name }) => name === "app_deployment_script_retire_authority", + ); + if (retireAuthority === undefined) { + throw new Error("App deployment script retirement trigger fixture is missing."); + } + fixture.database.execute("DROP TRIGGER app_deployment_script_retire_authority"); + await fixture.database + .prepare( + `UPDATE app_deployment_script + SET next_reconcile_at = ?, retire_after = ? + WHERE script_name = ?`, + ) + .bind(dueAt, fixture.registeredAt, fixture.scriptName) + .run(); + fixture.database.execute(retireAuthority.sql); +} + +async function readScript( + fixture: Pick, +): Promise> { + const row = await fixture.database + .prepare("SELECT * FROM app_deployment_script WHERE script_name = ?") + .bind(fixture.scriptName) + .first>(); + if (row === null) { + throw new Error("App deployment script fixture disappeared."); + } + return row; +} + +describe("App deployment script ledger", () => { + test("plain INSERT atomically resets any active run and retires the previous attempt", async () => { + const database = createDatabase(); + const first = await createAttempt(database, { status: "building" }); + + expect( + await database + .prepare("SELECT status, target_script_name FROM app_deployment_run WHERE id = ?") + .bind(first.runId) + .first(), + ).toEqual({ status: "preparing", target_script_name: first.scriptName }); + await expect( + registerAppDeploymentScriptCandidate(database, { + authority: first.authority, + deploymentId: first.deploymentId, + nowMs: first.registeredAt, + runId: first.runId, + scriptName: first.scriptName, + }), + ).rejects.toThrow(); + + const secondClaimOwner = crypto.randomUUID(); + await database + .prepare( + `UPDATE api_command + SET attempt_count = 2, claim_expires_at = ?, claim_owner = ?, status = 'running' + WHERE id = ?`, + ) + .bind(Date.now() + APP_DEPLOYMENT_SCRIPT_GRACE_MS, secondClaimOwner, first.commandId) + .run(); + await database + .prepare("UPDATE app_deployment_run SET status = 'submitting' WHERE id = ?") + .bind(first.runId) + .run(); + + const secondAuthority = authority(first.commandId, { + attemptCount: 2, + claimOwner: secondClaimOwner, + }); + const secondScriptName = candidateScriptName(first.commandId, 1, 2); + const beforeRegister = Date.now(); + await registerAppDeploymentScriptCandidate(database, { + authority: secondAuthority, + deploymentId: first.deploymentId, + nowMs: beforeRegister, + runId: first.runId, + scriptName: secondScriptName, + }); + const afterRegister = Date.now(); + + expect( + await database + .prepare("SELECT status, target_script_name FROM app_deployment_run WHERE id = ?") + .bind(first.runId) + .first(), + ).toEqual({ status: "preparing", target_script_name: secondScriptName }); + const old = await readScript(first); + expect(old.retire_after).toBeGreaterThanOrEqual( + beforeRegister + APP_DEPLOYMENT_SCRIPT_GRACE_MS, + ); + expect(old.retire_after).toBeLessThanOrEqual(afterRegister + APP_DEPLOYMENT_SCRIPT_GRACE_MS); + await database + .prepare("UPDATE app_deployment_run SET status = 'failed' WHERE id = ?") + .bind(first.runId) + .run(); + expect( + (await readScript({ database, scriptName: secondScriptName })).retire_after, + ).toBeNumber(); + expect( + await database.prepare("SELECT count(*) AS count FROM app_deployment_script").first(), + ).toEqual({ count: 2 }); + }); + + test("requires an upload fence before promotion and drains replaced or deleted active scripts", async () => { + const database = createDatabase(); + const first = await createAttempt(database); + await database + .prepare("UPDATE app_deployment_run SET status = 'activating' WHERE id = ?") + .bind(first.runId) + .run(); + await expect( + database + .prepare( + `UPDATE app_deployment + SET active_script_name = ?, last_successful_url = 'https://app.test', updated_at = ? + WHERE id = ?`, + ) + .bind(first.scriptName, first.registeredAt + 1, first.deploymentId) + .run(), + ).rejects.toThrow(); + + await expect( + database + .prepare( + `UPDATE app_deployment_script + SET retire_after = CAST(unixepoch('subsec') * 1000 AS INTEGER) + 86400000, + next_reconcile_at = CAST(unixepoch('subsec') * 1000 AS INTEGER) + 86400000 + WHERE script_name = ?`, + ) + .bind(first.scriptName) + .run(), + ).rejects.toThrow(); + expect((await readScript(first)).retire_after).toBeNull(); + + await database + .prepare("UPDATE api_command SET payload_json = '{}' WHERE id = ?") + .bind(first.commandId) + .run(); + await expect( + markAppDeploymentScriptUploadStarted(database, { + authority: first.authority, + deploymentId: first.deploymentId, + nowMs: first.registeredAt + 1, + runId: first.runId, + scriptName: first.scriptName, + }), + ).rejects.toThrow(); + await database + .prepare("UPDATE api_command SET payload_json = ? WHERE id = ?") + .bind(JSON.stringify({ appDeploymentRunId: first.runId }), first.commandId) + .run(); + await markAppDeploymentScriptUploadStarted(database, { + authority: first.authority, + deploymentId: first.deploymentId, + nowMs: first.registeredAt + 1, + runId: first.runId, + scriptName: first.scriptName, + }); + await database + .prepare( + `UPDATE app_deployment + SET active_script_name = ?, last_successful_url = 'https://app.test', updated_at = ? + WHERE id = ?`, + ) + .bind(first.scriptName, first.registeredAt + 2, first.deploymentId) + .run(); + expect(await readScript(first)).toMatchObject({ next_reconcile_at: null, retire_after: null }); + + const secondClaimOwner = crypto.randomUUID(); + await database + .prepare( + `UPDATE api_command + SET attempt_count = 2, claim_expires_at = ?, claim_owner = ?, status = 'running' + WHERE id = ?`, + ) + .bind(Date.now() + APP_DEPLOYMENT_SCRIPT_GRACE_MS, secondClaimOwner, first.commandId) + .run(); + const secondAuthority = authority(first.commandId, { + attemptCount: 2, + claimOwner: secondClaimOwner, + }); + const secondScriptName = candidateScriptName(first.commandId, 1, 2); + await registerAppDeploymentScriptCandidate(database, { + authority: secondAuthority, + deploymentId: first.deploymentId, + nowMs: Date.now(), + runId: first.runId, + scriptName: secondScriptName, + }); + await markAppDeploymentScriptUploadStarted(database, { + authority: secondAuthority, + deploymentId: first.deploymentId, + nowMs: Date.now(), + runId: first.runId, + scriptName: secondScriptName, + }); + await database + .prepare("UPDATE app_deployment_run SET status = 'activating' WHERE id = ?") + .bind(first.runId) + .run(); + await database + .prepare( + `UPDATE app_deployment + SET active_script_name = ?, last_successful_url = 'https://app.test', updated_at = ? + WHERE id = ?`, + ) + .bind(secondScriptName, Date.now(), first.deploymentId) + .run(); + + expect((await readScript(first)).retire_after).toBeNumber(); + await database + .prepare( + `UPDATE app_deployment + SET active_script_name = NULL, deleted_at = ?, last_successful_url = NULL, updated_at = ? + WHERE id = ?`, + ) + .bind(Date.now(), Date.now(), first.deploymentId) + .run(); + const second = { database, scriptName: secondScriptName }; + const retired = await readScript(second); + expect(retired.retire_after).toBeNumber(); + await expect( + database + .prepare( + `INSERT OR REPLACE INTO app_deployment_script + SELECT * FROM app_deployment_script WHERE script_name = ?`, + ) + .bind(secondScriptName) + .run(), + ).rejects.toThrow(); + expect(await readScript(second)).toEqual(retired); + await expect( + database + .prepare("UPDATE app_deployment_script SET retire_after = NULL WHERE script_name = ?") + .bind(secondScriptName) + .run(), + ).rejects.toThrow(); + await expect( + database + .prepare("DELETE FROM app_deployment_script WHERE script_name = ?") + .bind(secondScriptName) + .run(), + ).rejects.toThrow(); + }); + + test("linearizes promotion and orphan retirement in either commit order", async () => { + const nowMs = Date.now(); + const database = createDatabase(); + const gcAuthority = await createReconciliationAuthority(database, nowMs); + const gcWinner = await createAttempt(database, { registeredAt: nowMs - 1_000 }); + await markAppDeploymentScriptUploadStarted(database, { + authority: gcWinner.authority, + deploymentId: gcWinner.deploymentId, + nowMs, + runId: gcWinner.runId, + scriptName: gcWinner.scriptName, + }); + await expireAttemptAuthority(gcWinner); + await database + .prepare("UPDATE app_deployment_script SET next_reconcile_at = ? WHERE script_name = ?") + .bind(nowMs, gcWinner.scriptName) + .run(); + + let cleanupCalls = 0; + const armed = await reconcileAppDeploymentScriptPage( + database, + { authority: gcAuthority, cursor: null }, + async () => { + cleanupCalls += 1; + }, + ); + expect(armed.armed).toBe(1); + expect(cleanupCalls).toBe(0); + + await database + .prepare( + `UPDATE api_command + SET claim_expires_at = ?, claim_owner = ?, status = 'running' + WHERE id = ?`, + ) + .bind( + nowMs + APP_DEPLOYMENT_SCRIPT_GRACE_MS, + gcWinner.authority.claimOwner, + gcWinner.commandId, + ) + .run(); + await database + .prepare("UPDATE app_deployment_run SET status = 'activating' WHERE id = ?") + .bind(gcWinner.runId) + .run(); + await expect( + database + .prepare( + `UPDATE app_deployment + SET active_script_name = ?, last_successful_url = 'https://app.test', updated_at = ? + WHERE id = ?`, + ) + .bind(gcWinner.scriptName, nowMs, gcWinner.deploymentId) + .run(), + ).rejects.toThrow(); + + const promotionWinner = await createAttempt(database, { registeredAt: nowMs }); + await markAppDeploymentScriptUploadStarted(database, { + authority: promotionWinner.authority, + deploymentId: promotionWinner.deploymentId, + nowMs, + runId: promotionWinner.runId, + scriptName: promotionWinner.scriptName, + }); + await database + .prepare("UPDATE app_deployment_run SET status = 'activating' WHERE id = ?") + .bind(promotionWinner.runId) + .run(); + await database + .prepare( + `UPDATE app_deployment + SET active_script_name = ?, last_successful_url = 'https://app.test', updated_at = ? + WHERE id = ?`, + ) + .bind(promotionWinner.scriptName, nowMs, promotionWinner.deploymentId) + .run(); + await database + .prepare("UPDATE app_deployment_script SET next_reconcile_at = ? WHERE script_name = ?") + .bind(nowMs, promotionWinner.scriptName) + .run(); + const protectedResult = await reconcileAppDeploymentScriptPage( + database, + { authority: gcAuthority, cursor: null }, + async () => { + cleanupCalls += 1; + }, + ); + expect(protectedResult.deferred).toBeGreaterThanOrEqual(1); + expect(await readScript(promotionWinner)).toMatchObject({ + next_reconcile_at: null, + retire_after: null, + }); + expect(cleanupCalls).toBe(0); + }); + + test("protects every active candidate status only under the exact current command claim", async () => { + const nowMs = Date.now(); + const database = createDatabase(); + const fixture = await createAttempt(database, { registeredAt: nowMs }); + const gcAuthority = await createReconciliationAuthority(database, nowMs); + + for (const status of ACTIVE_RUN_STATUSES) { + await database + .prepare("UPDATE app_deployment_run SET status = ? WHERE id = ?") + .bind(status, fixture.runId) + .run(); + await database + .prepare("UPDATE app_deployment_script SET next_reconcile_at = ? WHERE script_name = ?") + .bind(nowMs, fixture.scriptName) + .run(); + const result = await reconcileAppDeploymentScriptPage( + database, + { authority: gcAuthority, cursor: null }, + async () => { + throw new Error("A live candidate must not be cleaned."); + }, + ); + expect(result.deferred).toBe(1); + expect((await readScript(fixture)).retire_after).toBeNull(); + } + + await expireAttemptAuthority(fixture); + await database + .prepare("UPDATE app_deployment_script SET next_reconcile_at = ? WHERE script_name = ?") + .bind(nowMs, fixture.scriptName) + .run(); + expect( + ( + await reconcileAppDeploymentScriptPage( + database, + { authority: gcAuthority, cursor: null }, + async () => undefined, + ) + ).armed, + ).toBe(1); + }); + + test("reconciles expired historical rows with permanent tombstones and recoverable leases", async () => { + const nowMs = Date.now(); + const database = createDatabase(); + const fixture = await createAttempt(database, { + registeredAt: nowMs - 2 * APP_DEPLOYMENT_SCRIPT_GRACE_MS, + }); + await markAppDeploymentScriptUploadStarted(database, { + authority: fixture.authority, + deploymentId: fixture.deploymentId, + nowMs: fixture.registeredAt + 1, + runId: fixture.runId, + scriptName: fixture.scriptName, + }); + await expireAttemptAuthority(fixture); + await seedExpiredHistoricalScript(fixture, nowMs); + const gcAuthority = await createReconciliationAuthority(database, nowMs); + const callerClockAhead = { + authority: gcAuthority, + cursor: null, + nowMs: Number.MAX_SAFE_INTEGER, + }; + const expectedBackoffDays = [1, 2, 4, 8, 16, 30, 30]; + let firstTombstone: number | null = null; + + for (const [index, days] of expectedBackoffDays.entries()) { + await database + .prepare("UPDATE app_deployment_script SET next_reconcile_at = ? WHERE script_name = ?") + .bind(fixture.registeredAt, fixture.scriptName) + .run(); + const actions: AppDeploymentScriptCleanupAction[] = []; + const result = await reconcileAppDeploymentScriptPage( + database, + callerClockAhead, + async (action) => { + actions.push(action); + }, + ); + expect(result.cleaned).toBe(1); + expect(actions[0]).toMatchObject({ + artifactPrefix: `app-deployments/${fixture.scriptName}/`, + buildSandboxId: `${fixture.runId}-g1-a1-build`, + deploySandboxId: `${fixture.runId}-g1-a1-deploy`, + scriptName: fixture.scriptName, + uploadStartedAt: fixture.registeredAt + 1, + }); + const row = await readScript(fixture); + firstTombstone ??= row.external_deleted_at as number; + expect(row.external_deleted_at).toBe(firstTombstone); + expect(row.next_reconcile_at).toBe( + (row.last_reconciled_at as number) + days * APP_DEPLOYMENT_SCRIPT_GRACE_MS, + ); + expect(row.reconcile_count).toBe(index + 1); + } + + await database + .prepare( + `UPDATE app_deployment_script + SET next_reconcile_at = ?, reconcile_count = ? + WHERE script_name = ?`, + ) + .bind(fixture.registeredAt, Number.MAX_SAFE_INTEGER, fixture.scriptName) + .run(); + expect( + (await reconcileAppDeploymentScriptPage(database, callerClockAhead, async () => undefined)) + .cleaned, + ).toBe(1); + const saturated = await readScript(fixture); + expect(saturated.reconcile_count).toBe(Number.MAX_SAFE_INTEGER); + expect(Number.isSafeInteger(saturated.next_reconcile_at)).toBeTrue(); + expect( + await database.prepare("SELECT count(*) AS count FROM app_deployment_script").first(), + ).toEqual({ count: 1 }); + + const noUpload = await createAttempt(database, { + registeredAt: nowMs - 2 * APP_DEPLOYMENT_SCRIPT_GRACE_MS, + }); + await expireAttemptAuthority(noUpload); + await seedExpiredHistoricalScript(noUpload, nowMs); + const failed = await reconcileAppDeploymentScriptPage(database, callerClockAhead, async () => { + throw new Error("transient cleanup failure"); + }); + expect(failed.failures).toHaveLength(1); + const failedRow = await readScript(noUpload); + expect(failedRow).toMatchObject({ + external_deleted_at: null, + reconcile_count: 0, + reconcile_expires_at: null, + reconcile_owner: null, + }); + expect(failedRow.next_reconcile_at).toBe( + (failedRow.last_reconciled_at as number) + APP_DEPLOYMENT_SCRIPT_GRACE_MS, + ); + + await database + .prepare( + `UPDATE app_deployment_script + SET next_reconcile_at = ?, reconcile_expires_at = ?, reconcile_owner = 'busy' + WHERE script_name = ?`, + ) + .bind(noUpload.registeredAt, Date.now() + 60_000, noUpload.scriptName) + .run(); + expect( + ( + await reconcileAppDeploymentScriptPage(database, callerClockAhead, async () => { + throw new Error("A live row lease must not be stolen."); + }) + ).deferred, + ).toBe(1); + await database + .prepare("UPDATE app_deployment_script SET reconcile_expires_at = ? WHERE script_name = ?") + .bind(0, noUpload.scriptName) + .run(); + let recoveredAction: AppDeploymentScriptCleanupAction | null = null; + await expect( + reconcileAppDeploymentScriptPage( + database, + { authority: gcAuthority, cursor: null }, + async (action) => { + recoveredAction = action; + await database + .prepare( + "UPDATE app_deployment_script SET reconcile_expires_at = 0 WHERE script_name = ?", + ) + .bind(noUpload.scriptName) + .run(); + }, + ), + ).rejects.toThrow("lost durable authority"); + expect(await readScript(noUpload)).toMatchObject({ + reconcile_count: 0, + reconcile_expires_at: 0, + }); + + const recovered = await reconcileAppDeploymentScriptPage( + database, + { authority: gcAuthority, cursor: null }, + async (action) => { + recoveredAction = action; + }, + ); + expect(recovered.cleaned).toBe(1); + expect(recoveredAction?.uploadStartedAt).toBeNull(); + expect((await readScript(noUpload)).external_deleted_at).toBeNull(); + }); + + test("uses D1 time rather than a caller clock to enforce the retirement grace", async () => { + const nowMs = Date.now(); + const database = createDatabase(); + const fixture = await createAttempt(database, { registeredAt: nowMs }); + await expireAttemptAuthority(fixture); + const retireAfter = await retireScriptThroughTerminalRun(fixture); + const gcAuthority = await createReconciliationAuthority(database, nowMs); + const callerClockAhead = { + authority: gcAuthority, + cursor: null, + nowMs: Number.MAX_SAFE_INTEGER, + }; + + let cleanupCalls = 0; + const result = await reconcileAppDeploymentScriptPage(database, callerClockAhead, async () => { + cleanupCalls += 1; + }); + + expect(result.processed).toBe(0); + expect(cleanupCalls).toBe(0); + expect((await readScript(fixture)).retire_after).toBe(retireAfter); + }); + + test("pages expired historical rows without skipping a permanent ledger entry", async () => { + const nowMs = Date.now(); + const database = createDatabase(); + for (let index = 0; index < 2; index += 1) { + const fixture = await createAttempt(database, { + registeredAt: nowMs - 2 * APP_DEPLOYMENT_SCRIPT_GRACE_MS - index, + }); + await expireAttemptAuthority(fixture); + await seedExpiredHistoricalScript(fixture, nowMs); + } + const gcAuthority = await createReconciliationAuthority(database, nowMs); + let cleaned = 0; + const first = await reconcileAppDeploymentScriptPage( + database, + { authority: gcAuthority, cursor: null }, + async () => { + cleaned += 1; + }, + ); + expect(first).toMatchObject({ hasMore: true, processed: 1 }); + expect(first.nextCursor).not.toBeNull(); + const second = await reconcileAppDeploymentScriptPage( + database, + { authority: gcAuthority, cursor: first.nextCursor }, + async () => { + cleaned += 1; + }, + ); + expect(second).toMatchObject({ hasMore: false, nextCursor: null, processed: 1 }); + expect(cleaned).toBe(2); + expect( + await database.prepare("SELECT count(*) AS count FROM app_deployment_script").first(), + ).toEqual({ count: 2 }); + }); +}); diff --git a/apps/api/tests/app-deployment-service.test.ts b/apps/api/tests/app-deployment-service.test.ts index 57c8e284..2ea679a1 100644 --- a/apps/api/tests/app-deployment-service.test.ts +++ b/apps/api/tests/app-deployment-service.test.ts @@ -1,19 +1,40 @@ import { describe, expect, test } from "bun:test"; +import { createHash } from "node:crypto"; +import { mkdtemp, mkdir, readFile, rm, symlink, writeFile } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; -import { apiCommandsTable, appDeploymentRunsTable, appDeploymentsTable } from "@mosoo/db"; +import { + apiCommandsTable, + appDeploymentRunsTable, + appDeploymentScriptsTable, + appDeploymentsTable, +} from "@mosoo/db"; +import { MANAGED_PROD_SCHEMA_TRIGGERS } from "@mosoo/db/deploy-schema-guard"; +import type { AppDeploymentRunId } from "@mosoo/id"; import { eq } from "drizzle-orm"; import { createAppDeploymentRunDispatchDedupeKey } from "../src/modules/api-command/application/api-command-enqueue"; import { API_COMMAND_QUEUE_SEND_FAILED_CODE } from "../src/modules/api-command/application/api-command-ledger"; -import { - APP_DEPLOYMENT_RUN_DISPATCH_MAX_ATTEMPTS, - APP_DEPLOYMENT_RUN_DISPATCH_RETRY_EXHAUSTED_CODE, -} from "../src/modules/api-command/application/api-command-policy"; -import { processApiCommandDeadLetterMessage } from "../src/modules/api-command/application/api-command-processor"; +import type { ApiCommandMessage } from "../src/modules/api-command/application/api-command-message"; +import { APP_DEPLOYMENT_RUN_DISPATCH_MAX_ATTEMPTS } from "../src/modules/api-command/application/api-command-policy"; import { getDeploymentAgentCapabilityAuthority } from "../src/modules/apps/application/app-deployment-capability-authority.service"; import type { CloudflareDeploymentClient } from "../src/modules/apps/application/app-deployment-cloudflare-client"; -import type { AppDeploymentBuildRunner } from "../src/modules/apps/application/app-deployment-executor.service"; -import { dispatchAppDeploymentRun } from "../src/modules/apps/application/app-deployment-executor.service"; +import type { + AppDeploymentAttemptAuthority, + AppDeploymentBuildRunner, + AppDeploymentDispatchOutcome, + DispatchAppDeploymentRunOptions, +} from "../src/modules/apps/application/app-deployment-executor.service"; +import { + AppDeploymentNonRetryableError, + APP_DEPLOYMENT_STATIC_ASSET_TOOL_SOURCE, + appDeploymentBuildSandboxId, + appDeploymentCandidateScriptName, + appDeploymentDeploySandboxId, + dispatchAppDeploymentRun, +} from "../src/modules/apps/application/app-deployment-executor.service"; +import { registerAppDeploymentScriptCandidate } from "../src/modules/apps/application/app-deployment-script-reconciliation.service"; import { deleteAppDeployment, deployApp, @@ -25,13 +46,8 @@ import type { AuthenticatedViewer } from "../src/modules/auth/application/viewer import type { SandboxHandle } from "../src/modules/runtime/infrastructure/sandbox-handles"; import { createApiWorker } from "../src/platform/cloudflare/create-api-worker"; import type { ApiBindings } from "../src/platform/cloudflare/worker-types"; -import { API_ERROR_CODE } from "../src/platform/errors"; -import type { ApiError } from "../src/platform/errors"; import { currentTimestampMs } from "../src/time"; -import { - createApiCommandQueueStub, - createRecordedQueueMessage, -} from "./helpers/api-command-queue-fixture"; +import { createApiCommandQueueStub } from "./helpers/api-command-queue-fixture"; import { SqliteD1Database } from "./helpers/sqlite-d1"; const OWNER_ID = "01J00000000000000000000001"; @@ -64,6 +80,7 @@ function createDatabase(): SqliteD1Database { ); CREATE TABLE app_deployment ( + active_script_name text, app_id text NOT NULL, created_at integer NOT NULL, default_branch text NOT NULL, @@ -77,7 +94,17 @@ function createDatabase(): SqliteD1Database { repo_owner text NOT NULL, repo_url text NOT NULL, source_kind text NOT NULL, - updated_at integer NOT NULL + updated_at integer NOT NULL, + CONSTRAINT app_deployment_traffic_authority_check CHECK ( + (active_script_name IS NULL AND last_successful_url IS NULL) + OR ( + deleted_at IS NULL + AND typeof(active_script_name) = 'text' + AND length(active_script_name) > 0 + AND typeof(last_successful_url) = 'text' + AND length(last_successful_url) > 0 + ) + ) ); CREATE UNIQUE INDEX app_deployment_active_app_idx @@ -111,6 +138,25 @@ function createDatabase(): SqliteD1Database { ON app_deployment_run (app_id) WHERE status IN ('queued', 'preparing', 'building', 'submitting', 'submitted', 'activating'); + CREATE TABLE app_deployment_script ( + attempt_count integer NOT NULL, + command_id text NOT NULL, + delivery_generation integer NOT NULL, + deployment_id text NOT NULL, + external_deleted_at integer, + last_reconciled_at integer, + next_reconcile_at integer, + reconcile_count integer DEFAULT 0 NOT NULL, + reconcile_expires_at integer, + reconcile_owner text, + registered_at integer NOT NULL, + registered_claim_owner text NOT NULL, + retire_after integer, + run_id text NOT NULL, + script_name text PRIMARY KEY NOT NULL, + upload_started_at integer + ); + CREATE TABLE api_command ( attempt_count integer DEFAULT 0 NOT NULL, claim_expires_at integer, @@ -118,6 +164,7 @@ function createDatabase(): SqliteD1Database { completed_at integer, created_at integer NOT NULL, dedupe_key text NOT NULL, + delivery_generation integer DEFAULT 1 NOT NULL, id text PRIMARY KEY NOT NULL, kind text NOT NULL, last_error_code text, @@ -140,30 +187,74 @@ function createDatabase(): SqliteD1Database { VALUES ('${APP_ID}', '01J00000000000000000000006', '${OWNER_ID}', 'App', 1, 1); `); + for (const trigger of MANAGED_PROD_SCHEMA_TRIGGERS.filter( + ({ name }) => + name.startsWith("app_deployment_script_") || + name.startsWith("app_deployment_run_target_script_") || + name === "app_deployment_run_terminal_script_retire", + )) { + database.execute(trigger.sql); + } + return database; } +function createAppDeploymentWorkflowStub( + options: { + failCreate?: () => boolean; + onCreate?: (message: ApiCommandMessage) => void; + } = {}, +) { + const created: ApiCommandMessage[] = []; + const instanceIds = new Set(); + const binding = { + async createBatch(batch: { id?: string; params?: ApiCommandMessage }[]) { + if (options.failCreate?.() === true) { + throw new Error("Workflow response timed out."); + } + return batch.map(({ id, params }) => { + if (id === undefined || params === undefined) { + throw new Error("Expected an exact Workflow instance ID and payload."); + } + created.push(params); + instanceIds.add(id); + options.onCreate?.(params); + return { id }; + }); + }, + async get(id: string) { + return { + id, + async status() { + return { status: instanceIds.has(id) ? "queued" : "unknown" }; + }, + }; + }, + } as unknown as Workflow; + return { binding, created }; +} + function createBindings(database: SqliteD1Database) { const queue = createApiCommandQueueStub(); + const workflow = createAppDeploymentWorkflowStub(); return { bindings: { + APP_DEPLOYMENT_DISPATCHER: { + get: () => ({ fetch: async () => new Response("candidate ready") }), + }, + APP_DEPLOYMENT_WORKFLOW: workflow.binding, API_COMMAND_QUEUE: queue, CLOUDFLARE_ACCOUNT_ID: "test-account", CLOUDFLARE_API_TOKEN: "test-token", CLOUDFLARE_ZONE_ID: "test-zone", DB: database, MOSOO_APP_DEPLOYMENT_DOMAIN: "apps.localhost", - } as Pick< - ApiBindings, - | "API_COMMAND_QUEUE" - | "CLOUDFLARE_ACCOUNT_ID" - | "CLOUDFLARE_API_TOKEN" - | "CLOUDFLARE_ZONE_ID" - | "DB" - | "MOSOO_APP_DEPLOYMENT_DOMAIN" - >, + MOSOO_APP_DISPATCH_NAMESPACE: "mosoo-app-deployments-stage", + SANDBOX_FILE_BUCKET_LOCAL: "true", + } as ApiBindings, queue, + workflow, }; } @@ -213,9 +304,10 @@ async function seedExpiredRunningDispatch( await setDeploymentRunUpdatedAt(database, runId, 1); } -async function seedExhaustedRunningDispatch( +async function seedThirdRunningDispatch( database: SqliteD1Database, runId: string, + claimExpiresAt: number, ): Promise { await database .prepare( @@ -229,7 +321,7 @@ async function seedExhaustedRunningDispatch( WHERE dedupe_key = ?`, ) .bind( - NOW_MS + 60_000, + claimExpiresAt, APP_DEPLOYMENT_RUN_DISPATCH_MAX_ATTEMPTS, createAppDeploymentRunDispatchDedupeKey(runId), ) @@ -273,6 +365,172 @@ async function seedQueuedDispatch(database: SqliteD1Database, runId: string): Pr .run(); } +async function claimDeploymentAttemptForTest( + database: SqliteD1Database, + runId: string, + input: { attemptCount?: number; claimOwner?: string; deliveryGeneration?: number } = {}, +): Promise { + const command = await database + .app() + .select({ + deliveryGeneration: apiCommandsTable.deliveryGeneration, + id: apiCommandsTable.id, + }) + .from(apiCommandsTable) + .where(eq(apiCommandsTable.dedupeKey, createAppDeploymentRunDispatchDedupeKey(runId))) + .limit(1) + .get(); + + if (command === undefined) { + throw new Error("Expected an App deployment dispatch command."); + } + + const attemptCount = input.attemptCount ?? 1; + const claimOwner = input.claimOwner ?? `test-claim-${attemptCount}`; + const deliveryGeneration = input.deliveryGeneration ?? command.deliveryGeneration; + await database + .prepare( + `UPDATE api_command + SET attempt_count = ?, + claim_expires_at = ?, + claim_owner = ?, + delivery_generation = ?, + status = 'running' + WHERE id = ?`, + ) + .bind(attemptCount, currentTimestampMs() + 60_000, claimOwner, deliveryGeneration, command.id) + .run(); + + const requireOwnership = async (): Promise => { + const owned = await database + .prepare( + `SELECT 1 + FROM api_command + WHERE id = ? + AND delivery_generation = ? + AND attempt_count = ? + AND claim_owner = ? + AND status = 'running' + AND claim_expires_at > ?`, + ) + .bind(command.id, deliveryGeneration, attemptCount, claimOwner, currentTimestampMs()) + .first(); + + if (owned === null) { + throw new Error("App deployment test attempt lost ownership."); + } + }; + + return { + attemptCount, + claimOwner, + commandId: command.id, + deliveryGeneration, + requireOwnership, + }; +} + +async function commitDeploymentOutcomeForTest( + database: SqliteD1Database, + authority: AppDeploymentAttemptAuthority, + outcome: AppDeploymentDispatchOutcome, +): Promise { + const nowMs = currentTimestampMs(); + const commandTerminal = database + .prepare( + `UPDATE api_command + SET claim_expires_at = NULL, + claim_owner = NULL, + completed_at = ?, + status = ?, + updated_at = ? + WHERE id = ? + AND delivery_generation = ? + AND attempt_count = ? + AND claim_owner = ? + AND status = 'running'`, + ) + .bind( + nowMs, + outcome.kind === "terminal_failure" ? "failed" : "succeeded", + nowMs, + authority.commandId, + authority.deliveryGeneration, + authority.attemptCount, + authority.claimOwner, + ); + + if (outcome.kind === "skipped") { + await commandTerminal.run(); + return; + } + + if (outcome.kind === "terminal_failure") { + await database.batch([ + database + .prepare( + `UPDATE app_deployment_run + SET error_code = ?, error_message = ?, status = 'failed', updated_at = ? + WHERE id = ? + AND status IN ('queued', 'preparing', 'building', 'submitting', 'submitted', 'activating')`, + ) + .bind(outcome.errorCode, outcome.errorMessage, nowMs, outcome.runId), + commandTerminal, + ]); + return; + } + + await database.batch([ + database + .prepare( + `UPDATE app_deployment + SET active_script_name = ?, last_successful_url = ?, updated_at = ? + WHERE id = ? AND latest_run_id = ? AND deleted_at IS NULL`, + ) + .bind(outcome.activeScriptName, outcome.url, nowMs, outcome.deploymentId, outcome.runId), + database + .prepare( + `UPDATE app_deployment_run + SET error_code = NULL, + error_message = NULL, + external_deployment_id = ?, + external_project_id = ?, + external_version_id = ?, + status = 'success', + updated_at = ?, + url = ? + WHERE id = ? AND status = 'activating' AND target_script_name = ?`, + ) + .bind( + outcome.externalDeploymentId, + outcome.externalProjectId, + outcome.externalVersionId, + nowMs, + outcome.url, + outcome.runId, + outcome.activeScriptName, + ), + commandTerminal, + ]); +} + +async function dispatchDeploymentForTest( + database: SqliteD1Database, + bindings: ApiBindings, + runId: string, + options: DispatchAppDeploymentRunOptions = {}, +): Promise { + const authority = await claimDeploymentAttemptForTest(database, runId); + const outcome = await dispatchAppDeploymentRun( + bindings, + { appDeploymentRunId: runId as AppDeploymentRunId }, + authority, + options, + ); + await commitDeploymentOutcomeForTest(database, authority, outcome); + return outcome; +} + async function seedDeployment( database: SqliteD1Database, input: { appId: string; deploymentId: string }, @@ -348,39 +606,18 @@ function createCloudflareDeleteRecorder( overrides: Partial = {}, ): CloudflareDeploymentClient { return { - async deletePagesDomain(input) { - deleted.push(`pages-domain:${input.hostname}`); - }, - async deletePagesProject(input) { - deleted.push(`pages:${input.projectName}`); - }, - async deleteWorkerDomain(input) { - deleted.push(`worker-domain:${input.hostname}`); - }, - async deleteWorkerRoute(input) { - deleted.push(`worker-route:${input.hostname}`); + async createStaticAssetsUploadSession() { + throw new Error("Unexpected Static Assets upload session."); }, async deleteWorkerScript(input) { deleted.push(`worker:${input.scriptName}`); }, + async deployStaticAssets() { + throw new Error("Unexpected Static Assets deploy."); + }, async deployWorkerModule() { throw new Error("Unexpected Worker deploy."); }, - async ensurePagesProject() { - throw new Error("Unexpected Pages project creation."); - }, - async ensurePagesDomain() { - throw new Error("Unexpected Pages domain creation."); - }, - async ensureWorkerDomain() { - throw new Error("Unexpected Worker domain creation."); - }, - async ensureWorkerRoute() { - throw new Error("Unexpected Worker route creation."); - }, - async getLatestPagesDeployment() { - throw new Error("Unexpected Pages deployment read."); - }, ...overrides, }; } @@ -416,16 +653,19 @@ function createTestSandboxHandle( readFile: unexpectedSandboxCall, setKeepAlive: async () => {}, writeFile: unexpectedSandboxCall, - } as SandboxHandle; + }; } const session = { - exec: async (command: string) => { - events.push(`${id}:session:${command}`); + exec: async (command: string, options?: { timeout?: number }) => { + events.push(`${id}:session:timeout=${String(options?.timeout)}:${command}`); return successfulCommandResult(); }, mkdir: unexpectedSandboxCall, - readFile: unexpectedSandboxCall, + readFile: async (path: string) => ({ + content: path.endsWith("completion-token") ? "completion-jwt" : "", + encoding: "utf8" as const, + }), startProcess: unexpectedSandboxCall, watch: unexpectedSandboxCall, writeFile: unexpectedSandboxCall, @@ -433,28 +673,163 @@ function createTestSandboxHandle( return { ...base, - createSession: async () => session, + createSession: async (options) => { + events.push( + `${id}:session-env=${Object.keys(options?.env ?? {}) + .toSorted() + .join(",")}`, + ); + return session; + }, destroy: async () => { events.push(`${id}:destroy`); }, - exec: async (command) => { - events.push(`${id}:${command}`); - return successfulCommandResult(command.includes("find . -type f") ? "./index.html\n" : ""); + exec: async (command, options) => { + events.push(`${id}:timeout=${String(options?.timeout)}:${command}`); + return successfulCommandResult( + command.includes("find . -type f") + ? "./.mosoo.toml\n./_headers\n./_redirects\n./index.html\n" + : "", + ); }, - readFile: async (_path, options) => ({ - content: options?.encoding === "base64" ? "YXJjaGl2ZQ==" : "
Hello
", + readFile: async (path, options) => ({ + content: path.endsWith("inventory.json") + ? JSON.stringify({ + assetsIgnore: "private/**\n", + headers: "/assets/*\n Cache-Control: public\n", + paths: [".assetsignore", "_headers", "_redirects", "index.html", "private/secret.txt"], + redirects: "/old /new 301\n\n/* /index.html 200\n", + }) + : path.endsWith("manifest.json") + ? JSON.stringify({ + "/index.html": { + hash: "0123456789abcdef0123456789abcdef", + size: 18, + }, + }) + : options?.encoding === "base64" + ? "YXJjaGl2ZQ==" + : path.endsWith(".mosoo.toml") + ? 'type = "static"\n\n[build]\noutput = "."\n\n[routes]\nfallback = "index.html"\n' + : "
Hello
", encoding: options?.encoding ?? "utf8", }), + mountBucket: async (bucket, mountPath, options) => { + events.push( + `${id}:mount:${bucket}:${mountPath}:${options.prefix}:${String(options.readOnly)}`, + ); + }, setKeepAlive: async (keepAlive) => { events.push(`${id}:keep-alive:${String(keepAlive)}`); }, - writeFile: async (path) => { - events.push(`${id}:write:${path}`); + unmountBucket: async (mountPath) => { + events.push(`${id}:unmount:${mountPath}`); + }, + writeFile: async (path, content) => { + events.push( + path.endsWith("selection.json") || path.endsWith("buckets.json") + ? `${id}:write:${path}:${content}` + : `${id}:write:${path}`, + ); + }, + }; +} + +function createLocalStaticDeploymentSandboxHandle( + id: string, + root: string, + populateRepository: (repoDir: string) => Promise, +): SandboxHandle { + const sandboxWorkDir = `/tmp/mosoo-app-deployment-${id}`; + const workDir = join(root, id); + const repoDir = join(workDir, "repo"); + const bucketDir = join(root, "bucket"); + const mapPath = (path: string) => + path + .replaceAll(sandboxWorkDir, workDir) + .replaceAll("/mnt/mosoo-app-deployment-artifact", bucketDir); + const execute = async (command: string) => { + const process = Bun.spawn(["sh", "-lc", mapPath(command)], { + stderr: "pipe", + stdout: "pipe", + }); + const [exitCode, stderr, stdout] = await Promise.all([ + process.exited, + new Response(process.stderr).text(), + new Response(process.stdout).text(), + ]); + + return { exitCode, stderr, stdout, success: exitCode === 0 }; + }; + const base = { + configureNetworkConstraints: async () => {}, + createBackup: unexpectedSandboxCall, + deleteSession: unexpectedSandboxCall, + getSession: unexpectedSandboxCall, + mkdir: unexpectedSandboxCall, + restoreBackup: unexpectedSandboxCall, + startProcess: unexpectedSandboxCall, + terminal: unexpectedSandboxCall, + watch: unexpectedSandboxCall, + wsConnect: unexpectedSandboxCall, + }; + + return { + ...base, + createSession: async () => ({ + exec: async (command) => + command.includes(" upload ") ? successfulCommandResult() : execute(command), + mkdir: unexpectedSandboxCall, + readFile: async (path, options) => ({ + content: path.endsWith("completion-token") + ? "completion-jwt" + : await readFile(mapPath(path), options?.encoding === "base64" ? "base64" : "utf8"), + encoding: options?.encoding ?? "utf8", + }), + startProcess: unexpectedSandboxCall, + watch: unexpectedSandboxCall, + writeFile: unexpectedSandboxCall, + }), + destroy: async () => {}, + exec: async (command) => { + if (command.includes("git clone --no-tags")) { + await rm(workDir, { force: true, recursive: true }); + await mkdir(repoDir, { recursive: true }); + await populateRepository(repoDir); + return successfulCommandResult(); + } + + return execute(command); + }, + mountBucket: async () => { + await mkdir(bucketDir, { recursive: true }); }, - } as SandboxHandle; + readFile: async (path, options) => ({ + content: path.endsWith("completion-token") + ? "completion-jwt" + : await readFile(mapPath(path), options?.encoding === "base64" ? "base64" : "utf8"), + encoding: options?.encoding ?? "utf8", + }), + setKeepAlive: async () => {}, + unmountBucket: async () => {}, + writeFile: async (path, content) => { + await writeFile(mapPath(path), content); + }, + }; } -function createWorkerDeploymentSandboxHandle(id: string, events: string[]): SandboxHandle { +function createWorkerDeploymentSandboxHandle( + id: string, + events: string[], + fixture: { + bundleContent?: string; + bundleError?: string; + bundleProjectRoot?: string; + rootDir?: string; + } = {}, +): SandboxHandle { + const rootDir = fixture.rootDir ?? "."; + let generatedBundleContent: string | undefined; const base = { configureNetworkConstraints: async () => {}, createBackup: unexpectedSandboxCall, @@ -476,44 +851,232 @@ function createWorkerDeploymentSandboxHandle(id: string, events: string[]): Sand destroy: async () => { events.push(`${id}:destroy`); }, - exec: async (command) => { - events.push(`${id}:${command}`); + exec: async (command, execOptions) => { + events.push(`${id}:timeout=${String(execOptions?.timeout)}:${command}`); + + if (command.includes("bun build") && fixture.bundleError !== undefined) { + return { + exitCode: 1, + stderr: fixture.bundleError, + stdout: "", + success: false as const, + }; + } + + if (command.includes("bun build") && fixture.bundleProjectRoot !== undefined) { + let result: Awaited>; + + try { + result = await Bun.build({ + entrypoints: [join(fixture.bundleProjectRoot, "src/index.js")], + external: ["cloudflare:*"], + format: "esm", + target: "browser", + }); + } catch (error) { + return { + exitCode: 1, + stderr: + error instanceof AggregateError + ? error.errors + .map((cause) => (cause instanceof Error ? cause.message : String(cause))) + .join("\n") + : error instanceof Error + ? error.message + : String(error), + stdout: "", + success: false as const, + }; + } + + if (!result.success) { + return { + exitCode: 1, + stderr: result.logs.map((log) => log.message).join("\n"), + stdout: "", + success: false as const, + }; + } + + generatedBundleContent = await result.outputs[0].text(); + } + return successfulCommandResult( command.includes("find . -type f -print") - ? "./.mosoo.toml\n./wrangler.toml\n./src/index.js\n" + ? `./.mosoo.toml\n./${rootDir === "." ? "" : `${rootDir}/`}src/index.js\n` : "", ); }, - readFile: async (path, options) => { - let content = "export default { fetch() { return new Response('ok'); } };\n"; + readFile: async (path, readOptions) => { + let content = + generatedBundleContent ?? + fixture.bundleContent ?? + "export default { fetch() { return new Response('ok'); } };\n"; if (path.endsWith(".mosoo.toml")) { content = [ "schema = 1", 'name = "worker-app"', + ...(rootDir === "." ? [] : [`root = "${rootDir}"`]), "", "[deploy]", 'adapter = "cloudflare-workers"', - 'wrangler = "wrangler.toml"', + "", + "[worker]", + 'entry = "src/index.js"', "", ].join("\n"); - } else if (path.endsWith("wrangler.toml")) { - content = 'name = "worker-app"\nmain = "src/index.js"\n'; } - return { content, encoding: options?.encoding ?? "utf8" }; + return { content, encoding: readOptions?.encoding ?? "utf8" }; }, setKeepAlive: async (keepAlive) => { events.push(`${id}:keep-alive:${String(keepAlive)}`); }, writeFile: unexpectedSandboxCall, - } as SandboxHandle; + }; } describe("app deployment service", () => { - test("creates a deployment run and queues dispatch", async () => { + test("salts Static Assets hashes by script and extension and remains deterministic", async () => { + const root = await mkdtemp(join(tmpdir(), "mosoo-static-assets-hash-")); + + try { + const artifactDir = join(root, "artifact"); + const selectionPath = join(root, "selection.json"); + const toolPath = join(root, "static-assets.mjs"); + const firstManifestPath = join(root, "manifest-a.json"); + const repeatedManifestPath = join(root, "manifest-a-repeated.json"); + const secondManifestPath = join(root, "manifest-b.json"); + const content = "same bytes"; + await mkdir(artifactDir); + await writeFile(join(artifactDir, "index.html"), content); + await writeFile(join(artifactDir, "same.css"), content); + await writeFile(join(artifactDir, "same.js"), content); + await writeFile(selectionPath, JSON.stringify(["index.html", "same.css", "same.js"])); + await writeFile(toolPath, APP_DEPLOYMENT_STATIC_ASSET_TOOL_SOURCE); + + for (const [scriptName, outputPath] of [ + ["app-command-g1-a1", firstManifestPath], + ["app-command-g1-a1", repeatedManifestPath], + ["app-command-g1-a2", secondManifestPath], + ] as const) { + const process = Bun.spawn([ + "node", + toolPath, + "manifest", + artifactDir, + selectionPath, + outputPath, + scriptName, + ]); + expect(await process.exited).toBe(0); + } + + const first = JSON.parse(await readFile(firstManifestPath, "utf8")); + const repeated = JSON.parse(await readFile(repeatedManifestPath, "utf8")); + const second = JSON.parse(await readFile(secondManifestPath, "utf8")); + const expectedHash = createHash("sha256") + .update("app-command-g1-a1") + .update(Buffer.from([0])) + .update("html") + .update(Buffer.from([0])) + .update(content) + .digest("hex") + .slice(0, 32); + + expect(first["/index.html"]).toEqual({ hash: expectedHash, size: content.length }); + expect(repeated).toEqual(first); + expect(second["/index.html"].hash).not.toBe(expectedHash); + expect(first["/same.css"].hash).not.toBe(first["/same.js"].hash); + } finally { + await rm(root, { force: true, recursive: true }); + } + }); + + test("prunes Git metadata from a root Static Assets artifact before manifest creation", async () => { + const root = await mkdtemp(join(tmpdir(), "mosoo-static-assets-root-")); + + try { + const database = createDatabase(); + const { bindings } = createBindings(database); + let manifest: Record = {}; + bindings.runtimeSubjectHandleFactory = (runtimeSubjectId) => + createLocalStaticDeploymentSandboxHandle(runtimeSubjectId, root, async (repoDir) => { + await mkdir(join(repoDir, ".git"), { recursive: true }); + await writeFile(join(repoDir, ".assetsignore"), "!/.git/**\n"); + await writeFile(join(repoDir, ".git", "config"), "credential = secret\n"); + await writeFile(join(repoDir, "index.html"), "
Root static
"); + }); + const cloudflareClient = createCloudflareDeleteRecorder([], { + async createStaticAssetsUploadSession(input) { + manifest = input.manifest; + return { + buckets: [Object.values(input.manifest).map(({ hash }) => hash)], + uploadToken: "eyJhbGciOiJub25lIn0.e30.signature", + }; + }, + async deployStaticAssets() { + return { deploymentId: null, versionId: null }; + }, + }); + const run = await deployApp( + bindings, + VIEWER, + { appId: APP_ID, repoUrl: "https://github.com/samzong/awire" }, + { fetch: githubFetch, nowMs: () => NOW_MS }, + ); + + const outcome = await dispatchDeploymentForTest(database, bindings, run.id, { + cloudflareClient, + }); + + expect(outcome).toMatchObject({ kind: "succeeded" }); + expect(Object.keys(manifest)).toEqual(["/index.html"]); + } finally { + await rm(root, { force: true, recursive: true }); + } + }); + + test("rejects a Static Assets output symlink outside the repository without retrying", async () => { + const root = await mkdtemp(join(tmpdir(), "mosoo-static-assets-escape-")); + const escapedDir = join(root, "escaped"); + + try { + await mkdir(escapedDir); + await writeFile(join(escapedDir, "index.html"), "
Escaped
"); + await writeFile(join(escapedDir, "_redirects"), "sentinel\n"); + const database = createDatabase(); + const { bindings } = createBindings(database); + bindings.runtimeSubjectHandleFactory = (runtimeSubjectId) => + createLocalStaticDeploymentSandboxHandle(runtimeSubjectId, root, async (repoDir) => { + await writeFile( + join(repoDir, ".mosoo.toml"), + 'type = "static"\n\n[build]\noutput = "public"\n\n[routes]\nfallback = "index.html"\n', + ); + await symlink(escapedDir, join(repoDir, "public"), "dir"); + }); + const run = await deployApp( + bindings, + VIEWER, + { appId: APP_ID, repoUrl: "https://github.com/samzong/awire" }, + { fetch: githubFetch, nowMs: () => NOW_MS }, + ); + + await expect(dispatchDeploymentForTest(database, bindings, run.id)).resolves.toMatchObject({ + errorCode: "AppDeploymentNonRetryableError", + errorMessage: "Static Assets output escapes repository root.", + kind: "terminal_failure", + }); + expect(await readFile(join(escapedDir, "_redirects"), "utf8")).toBe("sentinel\n"); + } finally { + await rm(root, { force: true, recursive: true }); + } + }); + + test("creates a deployment run and starts its durable Workflow", async () => { const database = createDatabase(); - const { bindings, queue } = createBindings(database); + const { bindings, workflow } = createBindings(database); const run = await deployApp( bindings, @@ -533,7 +1096,7 @@ describe("app deployment service", () => { sourceCommitSha: "abc123", status: "queued", }); - expect(queue.sent).toHaveLength(1); + expect(workflow.created).toHaveLength(1); const command = await database.app().select().from(apiCommandsTable).limit(1).get(); @@ -544,6 +1107,10 @@ describe("app deployment service", () => { expect(JSON.parse(command?.payloadJson ?? "{}")).toEqual({ appDeploymentRunId: run.id, }); + expect(workflow.created[0]).toEqual({ + commandId: command?.id, + deliveryGeneration: 1, + }); const deployment = await getAppDeployment(bindings, VIEWER, APP_ID); expect(deployment?.latestRun?.id).toBe(run.id); @@ -558,18 +1125,14 @@ describe("app deployment service", () => { const database = createDatabase(); const { bindings } = createBindings(database); const deliveredCommandIds: string[] = []; - let queueUnavailable = true; + let workflowUnavailable = true; + const workflow = createAppDeploymentWorkflowStub({ + failCreate: () => workflowUnavailable, + onCreate: (message) => deliveredCommandIds.push(message.commandId), + }); const deferredBindings = { ...bindings, - API_COMMAND_QUEUE: { - async send(input: { commandId: string }): Promise { - if (queueUnavailable) { - throw new Error("Queue response timed out."); - } - - deliveredCommandIds.push(input.commandId); - }, - }, + APP_DEPLOYMENT_WORKFLOW: workflow.binding, }; const run = await deployApp( @@ -603,7 +1166,7 @@ describe("app deployment service", () => { }); expect(runRow).toEqual({ errorCode: null, status: "queued" }); - queueUnavailable = false; + workflowUnavailable = false; await createApiWorker().scheduled( { scheduledTime: NOW_MS } as ScheduledController, deferredBindings as ApiBindings, @@ -623,8 +1186,8 @@ describe("app deployment service", () => { async build() {}, async deploy() { return { - externalDeploymentId: "pages-deployment-after-redrive", - externalProjectId: "pages-project-after-redrive", + externalDeploymentId: null, + externalProjectId: null, externalVersionId: null, url: `https://app-${APP_ID.toLowerCase()}.apps.localhost`, }; @@ -640,11 +1203,7 @@ describe("app deployment service", () => { expect(deliveredCommandIds).toContain(redrivenCommand?.id); expect(redrivenCommand).toMatchObject({ lastErrorCode: null, status: "queued" }); - await dispatchAppDeploymentRun( - deferredBindings as ApiBindings, - { appDeploymentRunId: run.id }, - { runner }, - ); + await dispatchDeploymentForTest(database, deferredBindings as ApiBindings, run.id, { runner }); const status = await getAppDeploymentStatus(deferredBindings, VIEWER, APP_ID); expect(status).toMatchObject({ status: "success" }); @@ -671,45 +1230,103 @@ describe("app deployment service", () => { ).rejects.toThrow("An App deployment run is already active."); }); - test("recovers an active deployment run without a dispatch command", async () => { + test("a losing concurrent deploy cannot overwrite the winner repository", async () => { const database = createDatabase(); const { bindings } = createBindings(database); - const firstRun = await deployApp( bindings, VIEWER, { appId: APP_ID, repoUrl: "https://github.com/samzong/awire" }, { fetch: githubFetch, nowMs: () => NOW_MS }, ); + await database + .prepare("UPDATE app_deployment_run SET status = 'failed' WHERE id = ?") + .bind(firstRun.id) + .run(); + await database.prepare("UPDATE api_command SET status = 'failed'").run(); + + const originalPrepare = database.prepare.bind(database); + let injected = false; + const wrapRunInsert = (statement: D1PreparedStatement): D1PreparedStatement => ({ + ...statement, + bind: (...values) => wrapRunInsert(statement.bind(...values)), + run: async () => { + if (!injected) { + injected = true; + database.execute(` + INSERT INTO app_deployment_run ( + app_id, created_at, deployment_id, id, source_branch, source_commit_sha, + status, updated_at + ) VALUES ( + '${APP_ID}', ${NOW_MS + 1}, '${firstRun.deploymentId}', + '${runListRunId(999)}', 'main', 'winner-sha', 'queued', ${NOW_MS + 1} + ) + `); + } + + return statement.run(); + }, + }); + database.prepare = ((query: string) => { + const statement = originalPrepare(query); + return /insert\s+into\s+"app_deployment_run"/iu.test(query) + ? wrapRunInsert(statement) + : statement; + }) as D1Database["prepare"]; + + const losingFetch: typeof fetch = async (input) => { + const url = input instanceof URL ? input.href : typeof input === "string" ? input : input.url; + + if (url === "https://api.github.com/repos/loser/repository") { + return Response.json({ + clone_url: "https://github.com/loser/repository.git", + default_branch: "trunk", + name: "repository", + owner: { login: "loser" }, + private: false, + }); + } + if (url === "https://api.github.com/repos/loser/repository/branches/trunk") { + return Response.json({ commit: { sha: "loser-sha" } }); + } - await database.prepare("DELETE FROM api_command").run(); - await setDeploymentRunUpdatedAt(database, firstRun.id, 1); - - const secondRun = await deployApp( - bindings, - VIEWER, - { appId: APP_ID, repoUrl: "https://github.com/samzong/awire" }, - { fetch: githubFetch, nowMs: () => NOW_MS + 1 }, - ); + throw new Error(`Unexpected GitHub request: ${url}`); + }; - expect(secondRun.id).not.toBe(firstRun.id); - expect(secondRun.status).toBe("queued"); + try { + await expect( + deployApp( + bindings, + VIEWER, + { appId: APP_ID, repoUrl: "https://github.com/loser/repository" }, + { fetch: losingFetch, nowMs: () => NOW_MS + 1 }, + ), + ).rejects.toThrow(); + } finally { + database.prepare = originalPrepare; + } - const firstRunRow = await database + const deployment = await database .app() - .select() - .from(appDeploymentRunsTable) - .where(eq(appDeploymentRunsTable.id, firstRun.id)) - .limit(1) + .select({ + latestRunId: appDeploymentsTable.latestRunId, + repoName: appDeploymentsTable.repoName, + repoOwner: appDeploymentsTable.repoOwner, + repoUrl: appDeploymentsTable.repoUrl, + }) + .from(appDeploymentsTable) + .where(eq(appDeploymentsTable.id, firstRun.deploymentId)) .get(); - expect(firstRunRow).toMatchObject({ - errorCode: "deployment_dispatch_missing", - status: "failed", + expect(deployment).toEqual({ + latestRunId: firstRun.id, + repoName: "awire", + repoOwner: "samzong", + repoUrl: "https://github.com/samzong/awire.git", }); }); - test("recovers an active deployment run with an expired running dispatch command", async () => { + test("recovers an active deployment run without a dispatch command", async () => { const database = createDatabase(); const { bindings } = createBindings(database); @@ -720,7 +1337,8 @@ describe("app deployment service", () => { { fetch: githubFetch, nowMs: () => NOW_MS }, ); - await seedExpiredRunningDispatch(database, firstRun.id); + await database.prepare("DELETE FROM api_command").run(); + await setDeploymentRunUpdatedAt(database, firstRun.id, 1); const secondRun = await deployApp( bindings, @@ -741,12 +1359,12 @@ describe("app deployment service", () => { .get(); expect(firstRunRow).toMatchObject({ - errorCode: "deployment_dispatch_expired", + errorCode: "deployment_dispatch_missing", status: "failed", }); }); - test("recovers an active deployment run with an expired running dispatch from reads", async () => { + test("keeps a Workflow deployment active after its command lease expires", async () => { const database = createDatabase(); const { bindings } = createBindings(database); @@ -760,27 +1378,36 @@ describe("app deployment service", () => { await seedExpiredRunningDispatch(database, run.id); await expect(getAppDeploymentStatus(bindings, VIEWER, APP_ID)).resolves.toMatchObject({ - errorCode: "deployment_dispatch_expired", + errorCode: null, id: run.id, - status: "failed", + status: "queued", }); await expect(getAppDeployment(bindings, VIEWER, APP_ID)).resolves.toMatchObject({ latestRun: { - errorCode: "deployment_dispatch_expired", + errorCode: null, id: run.id, - status: "failed", + status: "queued", }, }); await expect(listAppDeploymentRuns(bindings, VIEWER, APP_ID, 10)).resolves.toEqual([ expect.objectContaining({ - errorCode: "deployment_dispatch_expired", + errorCode: null, id: run.id, - status: "failed", + status: "queued", }), ]); + await expect( + deployApp( + bindings, + VIEWER, + { appId: APP_ID, repoUrl: "https://github.com/samzong/awire" }, + { fetch: githubFetch, nowMs: () => NOW_MS + 1 }, + ), + ).rejects.toThrow("An App deployment run is already active."); + const runRow = await database .app() .select() @@ -790,12 +1417,58 @@ describe("app deployment service", () => { .get(); expect(runRow).toMatchObject({ - errorCode: "deployment_dispatch_expired", - status: "failed", + errorCode: null, + status: "queued", }); }); - test("fails an active deployment run when dispatch retries are exhausted", async () => { + test("never terminalizes a third running Workflow attempt from a read path", async () => { + for (const claimExpiresAt of [currentTimestampMs() + 60_000, 1]) { + const database = createDatabase(); + const { bindings } = createBindings(database); + const run = await deployApp( + bindings, + VIEWER, + { appId: APP_ID, repoUrl: "https://github.com/samzong/awire" }, + { fetch: githubFetch, nowMs: () => NOW_MS }, + ); + + await seedThirdRunningDispatch(database, run.id, claimExpiresAt); + + await expect(getAppDeploymentStatus(bindings, VIEWER, APP_ID)).resolves.toMatchObject({ + errorCode: null, + id: run.id, + status: "queued", + }); + await expect( + deployApp( + bindings, + VIEWER, + { appId: APP_ID, repoUrl: "https://github.com/samzong/awire" }, + { fetch: githubFetch, nowMs: () => NOW_MS + 1 }, + ), + ).rejects.toThrow("An App deployment run is already active."); + + const command = await database + .app() + .select({ + attemptCount: apiCommandsTable.attemptCount, + lastErrorCode: apiCommandsTable.lastErrorCode, + status: apiCommandsTable.status, + }) + .from(apiCommandsTable) + .where(eq(apiCommandsTable.dedupeKey, createAppDeploymentRunDispatchDedupeKey(run.id))) + .limit(1) + .get(); + expect(command).toMatchObject({ + attemptCount: APP_DEPLOYMENT_RUN_DISPATCH_MAX_ATTEMPTS, + lastErrorCode: "SandboxError", + status: "running", + }); + } + }); + + test("keeps a fresh active deployment run without a dispatch command active", async () => { const database = createDatabase(); const { bindings } = createBindings(database); @@ -803,58 +1476,11 @@ describe("app deployment service", () => { bindings, VIEWER, { appId: APP_ID, repoUrl: "https://github.com/samzong/awire" }, - { fetch: githubFetch, nowMs: () => NOW_MS }, + { fetch: githubFetch, nowMs: currentTimestampMs }, ); - await seedExhaustedRunningDispatch(database, firstRun.id); - - await expect(getAppDeploymentStatus(bindings, VIEWER, APP_ID)).resolves.toMatchObject({ - errorCode: APP_DEPLOYMENT_RUN_DISPATCH_RETRY_EXHAUSTED_CODE, - errorMessage: expect.stringContaining("Container is starting"), - id: firstRun.id, - status: "failed", - }); - - const dispatchCommand = await database - .app() - .select({ - lastErrorCode: apiCommandsTable.lastErrorCode, - status: apiCommandsTable.status, - }) - .from(apiCommandsTable) - .where(eq(apiCommandsTable.dedupeKey, createAppDeploymentRunDispatchDedupeKey(firstRun.id))) - .limit(1) - .get(); - - expect(dispatchCommand).toMatchObject({ - lastErrorCode: APP_DEPLOYMENT_RUN_DISPATCH_RETRY_EXHAUSTED_CODE, - status: "failed", - }); - - const secondRun = await deployApp( - bindings, - VIEWER, - { appId: APP_ID, repoUrl: "https://github.com/samzong/awire" }, - { fetch: githubFetch, nowMs: () => NOW_MS + 1 }, - ); - - expect(secondRun.id).not.toBe(firstRun.id); - expect(secondRun.status).toBe("queued"); - }); - - test("keeps a fresh active deployment run without a dispatch command active", async () => { - const database = createDatabase(); - const { bindings } = createBindings(database); - - const firstRun = await deployApp( - bindings, - VIEWER, - { appId: APP_ID, repoUrl: "https://github.com/samzong/awire" }, - { fetch: githubFetch, nowMs: currentTimestampMs }, - ); - - await database.prepare("DELETE FROM api_command").run(); - await setDeploymentRunUpdatedAt(database, firstRun.id, currentTimestampMs()); + await database.prepare("DELETE FROM api_command").run(); + await setDeploymentRunUpdatedAt(database, firstRun.id, currentTimestampMs()); await expect( deployApp( @@ -884,16 +1510,24 @@ describe("app deployment service", () => { const { bindings } = createBindings(database); const targetUrl = `https://app-${APP_ID.toLowerCase()}.apps.localhost`; let buildPlanName: string | null = null; + let uploadWasFenced = false; const runner: AppDeploymentBuildRunner = { async build({ plan }) { buildPlanName = plan.generatedWranglerConfig; }, - async deploy() { + async deploy({ activeScriptName }) { + const script = await database + .app() + .select({ uploadStartedAt: appDeploymentScriptsTable.uploadStartedAt }) + .from(appDeploymentScriptsTable) + .where(eq(appDeploymentScriptsTable.scriptName, activeScriptName)) + .limit(1) + .get(); + uploadWasFenced = typeof script?.uploadStartedAt === "number"; return { - externalDeploymentId: "pages-deployment-1", - externalProjectId: "pages-project-1", + externalDeploymentId: null, + externalProjectId: null, externalVersionId: null, - url: targetUrl, }; }, async prepare() { @@ -911,34 +1545,384 @@ describe("app deployment service", () => { { fetch: githubFetch, nowMs: () => NOW_MS }, ); - await dispatchAppDeploymentRun( - bindings as ApiBindings, - { appDeploymentRunId: run.id }, - { - runner, - }, - ); + await dispatchDeploymentForTest(database, bindings, run.id, { runner }); const status = await getAppDeploymentStatus(bindings, VIEWER, APP_ID); const deployment = await getAppDeployment(bindings, VIEWER, APP_ID); const runRow = await database.app().select().from(appDeploymentRunsTable).limit(1).get(); - expect(buildPlanName).toContain(`name = "app-${APP_ID.toLowerCase()}"`); + expect(buildPlanName).toContain("[assets]"); + expect(uploadWasFenced).toBe(true); + expect(buildPlanName).not.toContain("name ="); expect(status).toMatchObject({ liveUrl: targetUrl, status: "success", }); expect(deployment?.liveUrl).toBe(targetUrl); expect(runRow).toMatchObject({ - externalDeploymentId: "pages-deployment-1", - externalProjectId: "pages-project-1", + externalDeploymentId: null, + externalProjectId: null, status: "success", - targetKind: "cloudflare_pages", - targetProjectName: `app-${APP_ID.toLowerCase()}`, + targetKind: "cloudflare_static_assets", + targetProjectName: null, + targetScriptName: expect.stringMatching(/^app-[0-9a-z]{26}-g1-a1$/u), url: targetUrl, }); }); + test("isolates deployment sandboxes by delivery generation and attempt", () => { + const runId = runListRunId(1) as AppDeploymentRunId; + const first = { attemptCount: 1, deliveryGeneration: 1 }; + const takeover = { attemptCount: 2, deliveryGeneration: 1 }; + const explicitRetry = { attemptCount: 1, deliveryGeneration: 2 }; + + expect( + new Set([ + appDeploymentBuildSandboxId(runId, first), + appDeploymentBuildSandboxId(runId, takeover), + appDeploymentBuildSandboxId(runId, explicitRetry), + appDeploymentDeploySandboxId(runId, first), + appDeploymentDeploySandboxId(runId, takeover), + appDeploymentDeploySandboxId(runId, explicitRetry), + ]).size, + ).toBe(6); + }); + + test.each(["queued", "preparing", "building", "submitting", "submitted", "activating"] as const)( + "restarts an interrupted %s run from preparing under a new attempt", + async (interruptedStatus) => { + const database = createDatabase(); + const { bindings } = createBindings(database); + const run = await deployApp( + bindings, + VIEWER, + { appId: APP_ID, repoUrl: "https://github.com/samzong/awire" }, + { fetch: githubFetch, nowMs: () => NOW_MS }, + ); + const firstAuthority = await claimDeploymentAttemptForTest(database, run.id); + const firstScriptName = appDeploymentCandidateScriptName(firstAuthority); + await registerAppDeploymentScriptCandidate(database, { + authority: firstAuthority, + deploymentId: run.deploymentId, + nowMs: currentTimestampMs(), + runId: run.id, + scriptName: firstScriptName, + }); + await database + .prepare("UPDATE app_deployment_run SET status = ? WHERE id = ?") + .bind(interruptedStatus, run.id) + .run(); + const authority = await claimDeploymentAttemptForTest(database, run.id, { + attemptCount: 2, + claimOwner: `resume-${interruptedStatus}`, + }); + const secondScriptName = appDeploymentCandidateScriptName(authority); + let statusAtPrepare: string | null = null; + const outcome = await dispatchAppDeploymentRun( + bindings, + { appDeploymentRunId: run.id }, + authority, + { + runner: { + async build() {}, + async deploy() { + return { + externalDeploymentId: null, + externalProjectId: null, + externalVersionId: null, + }; + }, + async prepare() { + statusAtPrepare = + ( + await database + .app() + .select({ status: appDeploymentRunsTable.status }) + .from(appDeploymentRunsTable) + .where(eq(appDeploymentRunsTable.id, run.id)) + .limit(1) + .get() + )?.status ?? null; + return { + repoDir: "/repo", + snapshot: { files: { "index.html": "
Recovered
" } }, + }; + }, + }, + }, + ); + await commitDeploymentOutcomeForTest(database, authority, outcome); + const scripts = await database + .app() + .select() + .from(appDeploymentScriptsTable) + .orderBy(appDeploymentScriptsTable.attemptCount); + const runRow = await database + .app() + .select({ + status: appDeploymentRunsTable.status, + targetScriptName: appDeploymentRunsTable.targetScriptName, + }) + .from(appDeploymentRunsTable) + .where(eq(appDeploymentRunsTable.id, run.id)) + .limit(1) + .get(); + + expect(statusAtPrepare).toBe("preparing"); + expect(secondScriptName).not.toBe(firstScriptName); + expect(outcome).toMatchObject({ kind: "succeeded", runId: run.id }); + expect(runRow).toEqual({ status: "success", targetScriptName: secondScriptName }); + expect(scripts).toHaveLength(2); + expect(scripts[0]).toMatchObject({ + attemptCount: 1, + retireAfter: expect.any(Number), + scriptName: firstScriptName, + }); + expect(scripts[1]).toMatchObject({ + attemptCount: 2, + retireAfter: null, + scriptName: secondScriptName, + uploadStartedAt: expect.any(Number), + }); + }, + ); + + test("bounds the whole dispatch by one deadline and lets the next attempt recover", async () => { + const database = createDatabase(); + const { bindings } = createBindings(database); + const run = await deployApp( + bindings, + VIEWER, + { appId: APP_ID, repoUrl: "https://github.com/samzong/awire" }, + { fetch: githubFetch, nowMs: () => NOW_MS }, + ); + const firstAuthority = await claimDeploymentAttemptForTest(database, run.id); + + await expect( + dispatchAppDeploymentRun(bindings, { appDeploymentRunId: run.id }, firstAuthority, { + deadlineMs: Date.now() + 5, + runner: { + async build() {}, + async deploy() { + throw new Error("Expired dispatch must not deploy."); + }, + async prepare() { + await new Promise((resolve) => setTimeout(resolve, 25)); + return { + repoDir: "/repo", + snapshot: { files: { "index.html": "
Late
" } }, + }; + }, + }, + }), + ).rejects.toThrow(/execution (?:budget|deadline)/u); + + const secondAuthority = await claimDeploymentAttemptForTest(database, run.id, { + attemptCount: 2, + claimOwner: "deadline-recovery", + }); + const recovered = await dispatchAppDeploymentRun( + bindings, + { appDeploymentRunId: run.id }, + secondAuthority, + { + runner: { + async build() {}, + async deploy() { + return { + externalDeploymentId: null, + externalProjectId: null, + externalVersionId: null, + }; + }, + async prepare() { + return { + repoDir: "/repo", + snapshot: { files: { "index.html": "
Recovered
" } }, + }; + }, + }, + }, + ); + + expect(recovered).toMatchObject({ kind: "succeeded", runId: run.id }); + }); + + test("returns deployment success without writing either terminal row", async () => { + const database = createDatabase(); + const { bindings } = createBindings(database); + const run = await deployApp( + bindings, + VIEWER, + { appId: APP_ID, repoUrl: "https://github.com/samzong/awire" }, + { fetch: githubFetch, nowMs: () => NOW_MS }, + ); + const authority = await claimDeploymentAttemptForTest(database, run.id); + const outcome = await dispatchAppDeploymentRun( + bindings, + { appDeploymentRunId: run.id }, + authority, + { + runner: { + async build() {}, + async deploy() { + return { + externalDeploymentId: null, + externalProjectId: null, + externalVersionId: null, + url: "https://owned.apps.localhost", + }; + }, + async prepare() { + return { + repoDir: "/repo", + snapshot: { files: { "index.html": "
Owned
" } }, + }; + }, + }, + }, + ); + const deployment = await database.app().select().from(appDeploymentsTable).limit(1).get(); + const runRow = await database.app().select().from(appDeploymentRunsTable).limit(1).get(); + const command = await database.app().select().from(apiCommandsTable).limit(1).get(); + + expect(outcome).toMatchObject({ kind: "succeeded", runId: run.id }); + expect(deployment?.lastSuccessfulUrl).toBeNull(); + expect(runRow).toMatchObject({ status: "activating", url: null }); + expect(command).toMatchObject({ claimOwner: authority.claimOwner, status: "running" }); + }); + + test("returns non-retryable failure without letting the executor fail the run", async () => { + const database = createDatabase(); + const { bindings } = createBindings(database); + const run = await deployApp( + bindings, + VIEWER, + { appId: APP_ID, repoUrl: "https://github.com/samzong/awire" }, + { fetch: githubFetch, nowMs: () => NOW_MS }, + ); + const authority = await claimDeploymentAttemptForTest(database, run.id); + const outcome = await dispatchAppDeploymentRun( + bindings, + { appDeploymentRunId: run.id }, + authority, + { + runner: { + async build() { + throw new AppDeploymentNonRetryableError("Unsupported build."); + }, + async deploy() { + throw new Error("Deploy must not run."); + }, + async prepare() { + return { + repoDir: "/repo", + snapshot: { files: { "index.html": "
Owned
" } }, + }; + }, + }, + }, + ); + const runRow = await database.app().select().from(appDeploymentRunsTable).limit(1).get(); + + expect(outcome).toEqual({ + errorCode: "AppDeploymentNonRetryableError", + errorMessage: "Unsupported build.", + kind: "terminal_failure", + runId: run.id, + }); + expect(runRow?.status).toBe("building"); + }); + + test("stops a stale attempt after takeover without moving state backward or deploying", async () => { + const database = createDatabase(); + const { bindings } = createBindings(database); + const run = await deployApp( + bindings, + VIEWER, + { appId: APP_ID, repoUrl: "https://github.com/samzong/awire" }, + { fetch: githubFetch, nowMs: () => NOW_MS }, + ); + const firstAuthority = await claimDeploymentAttemptForTest(database, run.id, { + attemptCount: 1, + claimOwner: "first-attempt", + }); + let unblockFirstBuild!: () => void; + let markFirstBuildStarted!: () => void; + const firstBuildStarted = new Promise((resolve) => { + markFirstBuildStarted = resolve; + }); + const firstBuildGate = new Promise((resolve) => { + unblockFirstBuild = resolve; + }); + let firstDeployCount = 0; + let takeoverDeployCount = 0; + const prepared = { + repoDir: "/repo", + snapshot: { files: { "index.html": "
Owned
" } }, + }; + const firstDispatch = dispatchAppDeploymentRun( + bindings, + { appDeploymentRunId: run.id }, + firstAuthority, + { + runner: { + async build() { + markFirstBuildStarted(); + await firstBuildGate; + }, + async deploy() { + firstDeployCount += 1; + throw new Error("Stale deployment must not run."); + }, + async prepare() { + return prepared; + }, + }, + }, + ); + + await firstBuildStarted; + const takeoverAuthority = await claimDeploymentAttemptForTest(database, run.id, { + attemptCount: 2, + claimOwner: "takeover-attempt", + }); + const takeoverOutcome = await dispatchAppDeploymentRun( + bindings, + { appDeploymentRunId: run.id }, + takeoverAuthority, + { + runner: { + async build() {}, + async deploy() { + takeoverDeployCount += 1; + return { + externalDeploymentId: "takeover-deployment", + externalProjectId: "takeover-project", + externalVersionId: null, + url: "https://takeover.apps.localhost", + }; + }, + async prepare() { + return prepared; + }, + }, + }, + ); + await commitDeploymentOutcomeForTest(database, takeoverAuthority, takeoverOutcome); + unblockFirstBuild(); + + await expect(firstDispatch).rejects.toThrow("lost ownership"); + const runRow = await database.app().select().from(appDeploymentRunsTable).limit(1).get(); + + expect(firstDeployCount).toBe(0); + expect(takeoverDeployCount).toBe(1); + expect(runRow).toMatchObject({ + externalDeploymentId: "takeover-deployment", + status: "success", + }); + }); + test("does not delete stable Cloudflare resources when an inactive run finishes", async () => { const database = createDatabase(); const { bindings } = createBindings(database); @@ -958,8 +1942,8 @@ describe("app deployment service", () => { .bind(run.id) .run(); return { - externalDeploymentId: "pages-deployment-1", - externalProjectId: "pages-project-1", + externalDeploymentId: null, + externalProjectId: null, externalVersionId: null, url: targetUrl, }; @@ -972,14 +1956,10 @@ describe("app deployment service", () => { }, }; - await dispatchAppDeploymentRun( - bindings as ApiBindings, - { appDeploymentRunId: run.id }, - { - cloudflareClient: createCloudflareDeleteRecorder(deleted), - runner, - }, - ); + await dispatchDeploymentForTest(database, bindings, run.id, { + cloudflareClient: createCloudflareDeleteRecorder(deleted), + runner, + }); const runRow = await database .app() @@ -993,7 +1973,7 @@ describe("app deployment service", () => { expect(runRow?.status).toBe("failed"); }); - test("compensates resources created after deployment deletion", async () => { + test("leaves a late WfP candidate retired for ledger GC after deployment deletion", async () => { const database = createDatabase(); const { bindings } = createBindings(database); const deleted: string[] = []; @@ -1008,12 +1988,12 @@ describe("app deployment service", () => { const runner: AppDeploymentBuildRunner = { async build() {}, async deploy() { - await deleteAppDeployment(bindings, VIEWER, { appId: APP_ID }, { cloudflareClient }); - externallyCreated.push(`pages:${APP_ID}`); + await deleteAppDeployment(bindings, VIEWER, { appId: APP_ID }); + externallyCreated.push(`wfp:${APP_ID}`); return { - externalDeploymentId: "pages-deployment-after-delete", - externalProjectId: "pages-project-after-delete", + externalDeploymentId: null, + externalProjectId: null, externalVersionId: null, url: `https://app-${APP_ID.toLowerCase()}.apps.localhost`, }; @@ -1026,11 +2006,12 @@ describe("app deployment service", () => { }, }; - await dispatchAppDeploymentRun( - bindings as ApiBindings, - { appDeploymentRunId: run.id }, - { cloudflareClient, runner }, - ); + await expect( + dispatchDeploymentForTest(database, bindings, run.id, { + cloudflareClient, + runner, + }), + ).rejects.toThrow("lost ownership"); const deployment = await database .app() @@ -1048,15 +2029,13 @@ describe("app deployment service", () => { .where(eq(appDeploymentRunsTable.id, run.id)) .get(); - expect(externallyCreated).toEqual([`pages:${APP_ID}`]); + expect(externallyCreated).toEqual([`wfp:${APP_ID}`]); expect(deployment?.deletedAt).toBeNumber(); expect(runRow).toEqual({ errorCode: "deployment_deleted", status: "failed" }); - expect(deleted).toHaveLength(10); - expect(deleted.filter((entry) => entry.startsWith("pages:"))).toHaveLength(2); - expect(deleted.filter((entry) => entry.startsWith("worker:"))).toHaveLength(2); + expect(deleted).toEqual([]); }); - test("does not compensate a deleted deployment after a replacement is active", async () => { + test("cannot let a late retired candidate disturb a replacement deployment", async () => { const database = createDatabase(); const { bindings } = createBindings(database); const deleted: string[] = []; @@ -1070,7 +2049,7 @@ describe("app deployment service", () => { const runner: AppDeploymentBuildRunner = { async build() {}, async deploy() { - await deleteAppDeployment(bindings, VIEWER, { appId: APP_ID }, { cloudflareClient }); + await deleteAppDeployment(bindings, VIEWER, { appId: APP_ID }); await deployApp( bindings, VIEWER, @@ -1079,8 +2058,8 @@ describe("app deployment service", () => { ); return { - externalDeploymentId: "pages-deployment-after-replacement", - externalProjectId: "pages-project-after-replacement", + externalDeploymentId: null, + externalProjectId: null, externalVersionId: null, url: `https://app-${APP_ID.toLowerCase()}.apps.localhost`, }; @@ -1093,24 +2072,132 @@ describe("app deployment service", () => { }, }; - await dispatchAppDeploymentRun( - bindings as ApiBindings, - { appDeploymentRunId: run.id }, - { cloudflareClient, runner }, - ); + await expect( + dispatchDeploymentForTest(database, bindings, run.id, { + cloudflareClient, + runner, + }), + ).rejects.toThrow("lost ownership"); const activeDeployment = await getAppDeployment(bindings, VIEWER, APP_ID); expect(activeDeployment).not.toBeNull(); - expect(deleted).toHaveLength(5); + expect(deleted).toEqual([]); + }); + + test("an old delete cannot terminate or destroy a replacement deployment", async () => { + const database = createDatabase(); + const { bindings } = createBindings(database); + const destroyed: string[] = []; + bindings.runtimeSubjectHandleFactory = (runtimeSubjectId) => + createTestSandboxHandle(runtimeSubjectId, destroyed, "destroy-only"); + await deployApp( + bindings, + VIEWER, + { appId: APP_ID, repoUrl: "https://github.com/samzong/awire" }, + { fetch: githubFetch, nowMs: () => NOW_MS }, + ); + + let replacement: Awaited> | null = null; + let raced = false; + const racingDatabase = new Proxy(database as D1Database, { + get(target, property, receiver) { + if (property !== "prepare") { + const value = Reflect.get(target, property, receiver) as unknown; + return typeof value === "function" ? value.bind(target) : value; + } + return (query: string) => { + const statement = target.prepare(query); + if (!query.includes("FROM app_deployment_run AS run")) { + return statement; + } + const interceptAll = (prepared: D1PreparedStatement) => + new Proxy(prepared, { + get(statementTarget, statementProperty, statementReceiver) { + if (statementProperty === "bind") { + return (...values: unknown[]) => interceptAll(statementTarget.bind(...values)); + } + if (statementProperty !== "all") { + const value = Reflect.get( + statementTarget, + statementProperty, + statementReceiver, + ) as unknown; + return typeof value === "function" ? value.bind(statementTarget) : value; + } + return async () => { + if (!raced) { + raced = true; + await deleteAppDeployment( + bindings, + VIEWER, + { appId: APP_ID }, + { nowMs: () => NOW_MS + 1 }, + ); + replacement = await deployApp( + bindings, + VIEWER, + { appId: APP_ID, repoUrl: "https://github.com/samzong/awire" }, + { fetch: githubFetch, nowMs: () => NOW_MS + 2 }, + ); + await database + .app() + .update(apiCommandsTable) + .set({ attemptCount: 1 }) + .where( + eq( + apiCommandsTable.dedupeKey, + createAppDeploymentRunDispatchDedupeKey(replacement.id), + ), + ) + .run(); + } + return statementTarget.all(); + }; + }, + }); + return interceptAll(statement); + }; + }, + }); + + await deleteAppDeployment( + { ...bindings, DB: racingDatabase }, + VIEWER, + { appId: APP_ID }, + { nowMs: () => NOW_MS + 3 }, + ); + + if (replacement === null) { + throw new Error("Expected a replacement deployment."); + } + const replacementRun = await database + .app() + .select({ status: appDeploymentRunsTable.status }) + .from(appDeploymentRunsTable) + .where(eq(appDeploymentRunsTable.id, replacement.id)) + .get(); + const replacementCommand = await database + .app() + .select({ status: apiCommandsTable.status }) + .from(apiCommandsTable) + .where( + eq(apiCommandsTable.dedupeKey, createAppDeploymentRunDispatchDedupeKey(replacement.id)), + ) + .get(); + + await expect(getAppDeployment(bindings, VIEWER, APP_ID)).resolves.not.toBeNull(); + expect(replacementRun).toEqual({ status: "queued" }); + expect(replacementCommand).toEqual({ status: "queued" }); + expect(destroyed).not.toContain(`${replacement.id}-g1-a1-build`); + expect(destroyed).not.toContain(`${replacement.id}-g1-a1-deploy`); }); test("deletes the active deployment and fails the active run", async () => { const database = createDatabase(); const { bindings } = createBindings(database); - const deleted: string[] = []; const destroyed: string[] = []; - (bindings as ApiBindings).runtimeSubjectHandleFactory = (runtimeSubjectId) => + bindings.runtimeSubjectHandleFactory = (runtimeSubjectId) => createTestSandboxHandle(runtimeSubjectId, destroyed, "destroy-only"); const run = await deployApp( @@ -1119,14 +2206,34 @@ describe("app deployment service", () => { { appId: APP_ID, repoUrl: "https://github.com/samzong/awire" }, { fetch: githubFetch, nowMs: () => NOW_MS }, ); - + const activeScriptName = "app-active-g1-a1"; + const liveUrl = `https://app-${APP_ID.toLowerCase()}.apps.localhost`; + await database + .prepare( + `UPDATE app_deployment + SET active_script_name = ?, last_successful_url = ? + WHERE id = ?`, + ) + .bind(activeScriptName, liveUrl, run.deploymentId) + .run(); + await database + .prepare( + `UPDATE api_command + SET attempt_count = 1, + claim_expires_at = ?, + claim_owner = 'active-delete-worker', + status = 'running' + WHERE dedupe_key = ?`, + ) + .bind(NOW_MS + 60_000, createAppDeploymentRunDispatchDedupeKey(run.id)) + .run(); await expect( deleteAppDeployment( bindings, VIEWER, { appId: APP_ID }, { - cloudflareClient: createCloudflareDeleteRecorder(deleted), + nowMs: () => NOW_MS + 1, }, ), ).resolves.toEqual({ ok: true }); @@ -1142,42 +2249,38 @@ describe("app deployment service", () => { id: run.id, status: "failed", }); - expect(deploymentRow?.deletedAt).toBeNumber(); + expect(deploymentRow).toMatchObject({ + activeScriptName: null, + deletedAt: NOW_MS + 1, + lastSuccessfulUrl: null, + }); expect(runRow?.status).toBe("failed"); - expect(deleted).toContain(`pages-domain:app-${APP_ID.toLowerCase()}.apps.localhost`); - expect(deleted).toContain(`pages:app-${APP_ID.toLowerCase()}`); - expect(deleted).toContain(`worker-domain:app-${APP_ID.toLowerCase()}.apps.localhost`); - expect(deleted).toContain(`worker-route:app-${APP_ID.toLowerCase()}.apps.localhost`); - expect(deleted).toContain(`worker:app-${APP_ID.toLowerCase()}`); - expect(destroyed).toContain(`${run.id}-build`); - expect(destroyed).toContain(`${run.id}-deploy`); + expect(destroyed).toContain(`${run.id}-g1-a1-build`); + expect(destroyed).toContain(`${run.id}-g1-a1-deploy`); }); - test("keeps a deployment retryable when Cloudflare cleanup fails", async () => { + test("revokes traffic without deleting the retired WfP script inline", async () => { const database = createDatabase(); const { bindings } = createBindings(database); - const failedDeletes: string[] = []; - const successfulDeletes: string[] = []; const run = await deployApp( bindings, VIEWER, { appId: APP_ID, repoUrl: "https://github.com/samzong/awire" }, { fetch: githubFetch, nowMs: () => NOW_MS }, ); - const failingClient = createCloudflareDeleteRecorder(failedDeletes, { - async deletePagesProject(input) { - failedDeletes.push(`pages:${input.projectName}`); - throw new Error("Cloudflare API unavailable."); - }, + const activeScriptName = "app-deferred-g1-a1"; + await database + .prepare( + `UPDATE app_deployment + SET active_script_name = ?, last_successful_url = ? + WHERE id = ?`, + ) + .bind(activeScriptName, "https://app.example", run.deploymentId) + .run(); + await expect(deleteAppDeployment(bindings, VIEWER, { appId: APP_ID })).resolves.toEqual({ + ok: true, }); - await expect( - deleteAppDeployment(bindings, VIEWER, { appId: APP_ID }, { cloudflareClient: failingClient }), - ).rejects.toMatchObject({ - code: API_ERROR_CODE.appDeploymentCleanupFailed, - name: "ApiError", - } satisfies Partial); - const deploymentAfterFailure = await database .app() .select() @@ -1191,31 +2294,18 @@ describe("app deployment service", () => { .where(eq(appDeploymentRunsTable.id, run.id)) .get(); - expect(deploymentAfterFailure).toMatchObject({ deletedAt: null }); + expect(deploymentAfterFailure).toMatchObject({ + activeScriptName: null, + lastSuccessfulUrl: null, + }); + expect(deploymentAfterFailure?.deletedAt).toBeNumber(); expect(runAfterFailure).toMatchObject({ errorCode: "deployment_deleted", status: "failed", }); - expect(failedDeletes).toHaveLength(5); - - await expect( - deleteAppDeployment( - bindings, - VIEWER, - { appId: APP_ID }, - { cloudflareClient: createCloudflareDeleteRecorder(successfulDeletes) }, - ), - ).resolves.toEqual({ ok: true }); - - const deploymentAfterRetry = await database - .app() - .select({ deletedAt: appDeploymentsTable.deletedAt }) - .from(appDeploymentsTable) - .where(eq(appDeploymentsTable.id, run.deploymentId)) - .get(); - - expect(deploymentAfterRetry?.deletedAt).toBeNumber(); - expect(successfulDeletes).toHaveLength(5); + await expect(deleteAppDeployment(bindings, VIEWER, { appId: APP_ID })).resolves.toEqual({ + ok: true, + }); }); test("does not expose a live URL after deleting a successful deployment", async () => { @@ -1226,8 +2316,8 @@ describe("app deployment service", () => { async build() {}, async deploy() { return { - externalDeploymentId: "pages-deployment-1", - externalProjectId: "pages-project-1", + externalDeploymentId: null, + externalProjectId: null, externalVersionId: null, url: targetUrl, }; @@ -1246,21 +2336,8 @@ describe("app deployment service", () => { { appId: APP_ID, repoUrl: "https://github.com/samzong/awire" }, { fetch: githubFetch, nowMs: () => NOW_MS }, ); - await dispatchAppDeploymentRun( - bindings as ApiBindings, - { appDeploymentRunId: run.id }, - { - runner, - }, - ); - await deleteAppDeployment( - bindings, - VIEWER, - { appId: APP_ID }, - { - cloudflareClient: createCloudflareDeleteRecorder([]), - }, - ); + await dispatchDeploymentForTest(database, bindings, run.id, { runner }); + await deleteAppDeployment(bindings, VIEWER, { appId: APP_ID }); await expect(getAppDeploymentStatus(bindings, VIEWER, APP_ID)).resolves.toMatchObject({ liveUrl: null, @@ -1276,8 +2353,8 @@ describe("app deployment service", () => { async build() {}, async deploy() { return { - externalDeploymentId: "pages-deployment-1", - externalProjectId: "pages-project-1", + externalDeploymentId: null, + externalProjectId: null, externalVersionId: null, url: targetUrl, }; @@ -1296,11 +2373,7 @@ describe("app deployment service", () => { { fetch: githubFetch, nowMs: () => NOW_MS }, ); - await dispatchAppDeploymentRun( - bindings as ApiBindings, - { appDeploymentRunId: run.id }, - { runner }, - ); + await dispatchDeploymentForTest(database, bindings, run.id, { runner }); await database .prepare("UPDATE app_deployment_run SET plan_json = ? WHERE id = ?") .bind( @@ -1322,14 +2395,7 @@ describe("app deployment service", () => { authorized: true, }); - await deleteAppDeployment( - bindings, - VIEWER, - { appId: APP_ID }, - { - cloudflareClient: createCloudflareDeleteRecorder([]), - }, - ); + await deleteAppDeployment(bindings, VIEWER, { appId: APP_ID }); await expect(getDeploymentAgentCapabilityAuthority(database, authority)).resolves.toEqual({ authorized: false, @@ -1337,20 +2403,29 @@ describe("app deployment service", () => { }); }); - test("uses the Pages deployment URL while the custom domain is pending", async () => { + test("deploys native Static Assets to an attempt-specific WfP script", async () => { const database = createDatabase(); const { bindings } = createBindings(database); const calls: string[] = []; - const pagesUrl = "https://app-example.pages.dev"; + const cloudflareCalls: string[] = []; const cloudflareClient = createCloudflareDeleteRecorder([], { - ensurePagesDomain: async () => ({ status: "initializing" }), - ensurePagesProject: async () => ({ projectId: "pages-project-1" }), - getLatestPagesDeployment: async () => ({ - deploymentId: "pages-deployment-1", - url: pagesUrl, - }), + async createStaticAssetsUploadSession(input) { + cloudflareCalls.push( + `session:${input.scriptName}:${Object.keys(input.manifest).join(",")}`, + ); + return { + buckets: [["0123456789abcdef0123456789abcdef"]], + uploadToken: "eyJhbGciOiJub25lIn0.e30.signature", + }; + }, + async deployStaticAssets(input) { + cloudflareCalls.push( + `deploy:${input.scriptName}:${input.completionToken}:${input.tags.join(",")}:${input.headers}:${input.redirects}`, + ); + return { deploymentId: null, versionId: null }; + }, }); - (bindings as ApiBindings).runtimeSubjectHandleFactory = (runtimeSubjectId) => + bindings.runtimeSubjectHandleFactory = (runtimeSubjectId) => createTestSandboxHandle(runtimeSubjectId, calls, "deployment"); const run = await deployApp( @@ -1360,49 +2435,83 @@ describe("app deployment service", () => { { fetch: githubFetch, nowMs: () => NOW_MS }, ); - await dispatchAppDeploymentRun( - bindings as ApiBindings, - { appDeploymentRunId: run.id }, - { cloudflareClient }, - ); + await dispatchDeploymentForTest(database, bindings, run.id, { + cloudflareClient, + }); const status = await getAppDeploymentStatus(bindings, VIEWER, APP_ID); const runRow = await database.app().select().from(appDeploymentRunsTable).limit(1).get(); + const scriptRow = await database.app().select().from(appDeploymentScriptsTable).limit(1).get(); + const targetUrl = `https://app-${APP_ID.toLowerCase()}.apps.localhost`; + const scriptName = runRow?.targetScriptName; expect(status).toMatchObject({ - liveUrl: pagesUrl, + liveUrl: targetUrl, status: "success", }); expect(runRow).toMatchObject({ - externalDeploymentId: "pages-deployment-1", - externalProjectId: "pages-project-1", + externalDeploymentId: null, + externalProjectId: null, status: "success", - url: pagesUrl, + targetKind: "cloudflare_static_assets", + targetProjectName: null, + targetScriptName: expect.stringMatching(/^app-[0-9a-z]{26}-g1-a1$/u), + url: targetUrl, }); - expect(calls.some((call) => call.includes("wrangler pages deploy"))).toBe(true); + expect(calls.every((call) => !call.includes("wrangler"))).toBe(true); + const fallbackArchive = calls.find((call) => call.includes(">>")); + const commandCalls = calls.filter((call) => call.includes("sh -lc")); + + expect(fallbackArchive).toContain(">>"); + expect(fallbackArchive).toContain("find . -name .git -prune -o -type f -print0"); + expect(fallbackArchive).toContain("output_root=$(pwd -P)"); + expect(fallbackArchive).not.toContain("/_headers"); + expect(commandCalls.length).toBeGreaterThan(0); + expect( + commandCalls.every((call) => { + const timeout = Number(/timeout=(\d+)/u.exec(call)?.[1]); + return timeout > 0 && timeout <= 600_000; + }), + ).toBe(true); + expect( + calls + .filter((call) => call.includes(":mount:FILE_BUCKET:")) + .map((call) => call.slice(call.lastIndexOf(":"))), + ).toEqual([":false", ":true"]); + expect( + calls + .filter((call) => call.includes(":mount:FILE_BUCKET:")) + .every((call) => call.includes(`/app-deployments/${scriptName}/`)), + ).toBe(true); + expect(calls.some((call) => call.endsWith('/selection.json:["index.html"]'))).toBe(true); + expect( + calls.some((call) => + call.includes("session-env=CLOUDFLARE_ACCOUNT_ID,CLOUDFLARE_ASSET_UPLOAD_JWT"), + ), + ).toBe(true); + expect(calls.every((call) => !call.includes("CLOUDFLARE_API_TOKEN="))).toBe(true); + expect(scriptRow?.uploadStartedAt).toBeNumber(); + expect(cloudflareCalls).toEqual([ + `session:${scriptName}:/index.html`, + `deploy:${scriptName}:completion-jwt:mosoo-managed,d-${run.deploymentId.toLowerCase()},r-${run.id.toLowerCase()}:/assets/*\n Cache-Control: public\n:/old /new 301\n\n/* /index.html 200\n`, + ]); }); - test("deploys worker modules with a Worker route and custom domain", async () => { + test("deploys a Worker module as an attempt-specific WfP script", async () => { const database = createDatabase(); const { bindings } = createBindings(database); const calls: string[] = []; const cloudflareCalls: string[] = []; - (bindings as ApiBindings).runtimeSubjectHandleFactory = (runtimeSubjectId) => + bindings.runtimeSubjectHandleFactory = (runtimeSubjectId) => createWorkerDeploymentSandboxHandle(runtimeSubjectId, calls); const cloudflareClient = createCloudflareDeleteRecorder([], { async deployWorkerModule(input) { cloudflareCalls.push( - `worker:${input.scriptName}:${input.mainModuleName}:${input.scriptContent.trim()}`, + `worker:${input.scriptName}:${input.mainModuleName}:${input.tags.join(",")}:${input.scriptContent.trim()}`, ); return { deploymentId: "worker-deployment-1", versionId: "worker-version-1" }; }, - async ensureWorkerDomain(input) { - cloudflareCalls.push(`worker-domain:${input.hostname}:${input.scriptName}`); - }, - async ensureWorkerRoute(input) { - cloudflareCalls.push(`worker-route:${input.hostname}:${input.scriptName}`); - }, }); const run = await deployApp( @@ -1412,17 +2521,14 @@ describe("app deployment service", () => { { fetch: githubFetch, nowMs: () => NOW_MS }, ); - await dispatchAppDeploymentRun( - bindings as ApiBindings, - { appDeploymentRunId: run.id }, - { cloudflareClient }, - ); + await dispatchDeploymentForTest(database, bindings, run.id, { + cloudflareClient, + }); - const targetName = `app-${APP_ID.toLowerCase()}`; - const hostname = `${targetName}.apps.localhost`; - const targetUrl = `https://${hostname}`; + const targetUrl = `https://app-${APP_ID.toLowerCase()}.apps.localhost`; const status = await getAppDeploymentStatus(bindings, VIEWER, APP_ID); const runRow = await database.app().select().from(appDeploymentRunsTable).limit(1).get(); + const scriptName = runRow?.targetScriptName; expect(status).toMatchObject({ liveUrl: targetUrl, @@ -1434,65 +2540,147 @@ describe("app deployment service", () => { externalVersionId: "worker-version-1", status: "success", targetKind: "cloudflare_worker", - targetScriptName: targetName, + targetProjectName: null, + targetScriptName: expect.stringMatching(/^app-[0-9a-z]{26}-g1-a1$/u), url: targetUrl, }); expect(cloudflareCalls).toEqual([ - `worker:${targetName}:index.js:export default { fetch() { return new Response('ok'); } };`, - `worker-route:${hostname}:${targetName}`, - `worker-domain:${hostname}:${targetName}`, + `worker:${scriptName}:worker.mjs:mosoo-managed,d-${run.deploymentId.toLowerCase()},r-${run.id.toLowerCase()}:export default { fetch() { return new Response('ok'); } };`, ]); + expect( + calls.some( + (call) => + call.includes("/repo/.") && call.includes("bun build") && call.includes("src/index.js"), + ), + ).toBe(true); }); - test("dead letters active deployment runs without overwriting terminal runs", async () => { - const database = createDatabase(); - const { bindings, queue } = createBindings(database); + test("bundles relative and package Worker imports from the configured root", async () => { + const projectRoot = await mkdtemp(join(tmpdir(), "mosoo-worker-bundle-")); - const run = await deployApp( - bindings, - VIEWER, - { appId: APP_ID, repoUrl: "https://github.com/samzong/awire" }, - { fetch: githubFetch, nowMs: () => NOW_MS }, - ); - const queued = queue.sent[0]; + try { + await mkdir(join(projectRoot, "node_modules", "fixture-package"), { recursive: true }); + await mkdir(join(projectRoot, "src"), { recursive: true }); + await writeFile( + join(projectRoot, "node_modules", "fixture-package", "package.json"), + JSON.stringify({ exports: "./index.js", name: "fixture-package", type: "module" }), + ); + await writeFile( + join(projectRoot, "node_modules", "fixture-package", "index.js"), + 'export const packageValue = "package-value";\n', + ); + await writeFile( + join(projectRoot, "src", "relative.js"), + 'export const relativeValue = "relative-value";\n', + ); + await writeFile( + join(projectRoot, "src", "index.js"), + [ + 'import { relativeValue } from "./relative.js";', + 'import { packageValue } from "fixture-package";', + "export default {", + " fetch() { return new Response(`${relativeValue}:${packageValue}`); },", + "};", + "", + ].join("\n"), + ); + + const database = createDatabase(); + const { bindings } = createBindings(database); + const calls: string[] = []; + let uploaded = ""; + bindings.runtimeSubjectHandleFactory = (runtimeSubjectId) => + createWorkerDeploymentSandboxHandle(runtimeSubjectId, calls, { + bundleProjectRoot: projectRoot, + rootDir: "packages/worker", + }); + const cloudflareClient = createCloudflareDeleteRecorder([], { + async deployWorkerModule(input) { + uploaded = input.scriptContent; + return { deploymentId: "worker-deployment-1", versionId: "worker-version-1" }; + }, + }); + const run = await deployApp( + bindings, + VIEWER, + { appId: APP_ID, configPath: ".mosoo.toml", repoUrl: "https://github.com/samzong/awire" }, + { fetch: githubFetch, nowMs: () => NOW_MS }, + ); - if (queued === undefined) { - throw new Error("Expected deployment dispatch queue message."); + await expect( + dispatchDeploymentForTest(database, bindings, run.id, { + cloudflareClient, + }), + ).resolves.toMatchObject({ kind: "succeeded" }); + + expect(uploaded).toContain("relative-value"); + expect(uploaded).toContain("package-value"); + expect(uploaded).not.toContain('from "./relative.js"'); + expect(uploaded).not.toContain('from "fixture-package"'); + const bundleCall = calls.find((call) => call.includes("bun build")); + + expect(bundleCall).toContain("/repo/packages/worker"); + expect(bundleCall?.indexOf("/repo/packages/worker")).toBeLessThan( + bundleCall?.indexOf("bun build") ?? -1, + ); + } finally { + await rm(projectRoot, { force: true, recursive: true }); } + }); - await database.app().update(apiCommandsTable).set({ payloadJson: "{}" }).run(); + test("fails closed when a Worker dependency cannot be bundled", async () => { + const projectRoot = await mkdtemp(join(tmpdir(), "mosoo-worker-missing-dependency-")); - await processApiCommandDeadLetterMessage( - bindings as ApiBindings, - createRecordedQueueMessage({ body: queued.body }).message, - () => NOW_MS + 1, - ); + try { + await mkdir(join(projectRoot, "src"), { recursive: true }); + await writeFile( + join(projectRoot, "src", "index.js"), + 'import "missing-worker-dependency";\nexport default { fetch() {} };\n', + ); - await expect(getAppDeploymentStatus(bindings, VIEWER, APP_ID)).resolves.toMatchObject({ - errorCode: "queue_dead_lettered", - id: run.id, - status: "failed", - }); + const database = createDatabase(); + const { bindings } = createBindings(database); + const calls: string[] = []; + let deployCalls = 0; + bindings.runtimeSubjectHandleFactory = (runtimeSubjectId) => + createWorkerDeploymentSandboxHandle(runtimeSubjectId, calls, { + bundleProjectRoot: projectRoot, + rootDir: "packages/worker", + }); + const cloudflareClient = createCloudflareDeleteRecorder([], { + async deployWorkerModule() { + deployCalls += 1; + return { deploymentId: "unexpected", versionId: "unexpected" }; + }, + }); + const run = await deployApp( + bindings, + VIEWER, + { appId: APP_ID, configPath: ".mosoo.toml", repoUrl: "https://github.com/samzong/awire" }, + { fetch: githubFetch, nowMs: () => NOW_MS }, + ); - await deleteAppDeployment( - bindings, - VIEWER, - { appId: APP_ID }, - { - cloudflareClient: createCloudflareDeleteRecorder([]), - }, - ); - await processApiCommandDeadLetterMessage( - bindings as ApiBindings, - createRecordedQueueMessage({ body: queued.body }).message, - () => NOW_MS + 2, - ); + const outcome = await dispatchDeploymentForTest(database, bindings, run.id, { + cloudflareClient, + }); - await expect(getAppDeploymentStatus(bindings, VIEWER, APP_ID)).resolves.toMatchObject({ - errorCode: "queue_dead_lettered", - id: run.id, - status: "failed", - }); + expect(outcome).toMatchObject({ + errorCode: "AppDeploymentNonRetryableError", + errorMessage: expect.stringContaining('Could not resolve: "missing-worker-dependency"'), + kind: "terminal_failure", + }); + expect(deployCalls).toBe(0); + await expect( + database + .app() + .select({ status: appDeploymentRunsTable.status }) + .from(appDeploymentRunsTable) + .where(eq(appDeploymentRunsTable.id, run.id)) + .get(), + ).resolves.toEqual({ status: "failed" }); + } finally { + await rm(projectRoot, { force: true, recursive: true }); + } }); test("lists deployment runs newest-first", async () => { @@ -1613,8 +2801,8 @@ describe("app deployment service", () => { async build() {}, async deploy() { return { - externalDeploymentId: "pages-deployment-1", - externalProjectId: "pages-project-1", + externalDeploymentId: null, + externalProjectId: null, externalVersionId: null, url: targetUrl, }; @@ -1633,21 +2821,8 @@ describe("app deployment service", () => { { appId: APP_ID, repoUrl: "https://github.com/samzong/awire" }, { fetch: githubFetch, nowMs: () => NOW_MS }, ); - await dispatchAppDeploymentRun( - bindings as ApiBindings, - { appDeploymentRunId: run.id }, - { - runner, - }, - ); - await deleteAppDeployment( - bindings, - VIEWER, - { appId: APP_ID }, - { - cloudflareClient: createCloudflareDeleteRecorder([]), - }, - ); + await dispatchDeploymentForTest(database, bindings, run.id, { runner }); + await deleteAppDeployment(bindings, VIEWER, { appId: APP_ID }); // The deployment is soft-deleted: run history stays listed, but liveUrl is // suppressed because the deployment row carries deletedAt. diff --git a/apps/api/tests/app-overview.test.ts b/apps/api/tests/app-overview.test.ts index b1e83887..f9fde426 100644 --- a/apps/api/tests/app-overview.test.ts +++ b/apps/api/tests/app-overview.test.ts @@ -1,9 +1,16 @@ import { describe, expect, test } from "bun:test"; -import { isInputObjectType, isObjectType } from "graphql"; +import { isEnumType, isInputObjectType, isObjectType } from "graphql"; import { createGraphQLSchema } from "../src/adapters/graphql/create-graphql-schema"; import { createAppDeploymentRunDispatchDedupeKey } from "../src/modules/api-command/application/api-command-enqueue"; +import { admitApiCommand } from "../src/modules/api-command/application/api-command-ledger"; +import { processApiCommandDelivery } from "../src/modules/api-command/application/api-command-processor"; +import { appDeploymentCandidateScriptName } from "../src/modules/apps/application/app-deployment-executor.service"; +import { + markAppDeploymentScriptUploadStarted, + registerAppDeploymentScriptCandidate, +} from "../src/modules/apps/application/app-deployment-script-reconciliation.service"; import { getAppOverview, getControlPlaneOverview, @@ -13,6 +20,7 @@ import { createApiTestFixture } from "./helpers/api-test-fixture"; const OVERVIEW_DEPLOYMENT_ID = "01J000000000000000000000D1"; const OVERVIEW_DEPLOYMENT_RUN_ID = "01J000000000000000000000D2"; +const OVERVIEW_AGENT_DEPLOYMENT_VERSION_ID = "01J000000000000000000000D3"; function createOverviewDeploymentUrl(appId: string, domain: string): string { return `https://app-${appId.toLowerCase()}.${domain}`; @@ -36,6 +44,12 @@ async function insertOverviewAgent( updatedAt: number; }, ): Promise { + const configJson = JSON.stringify({ + packageMcpServers: [], + packageResolution: null, + packageSkills: [], + providerOptions: {}, + }); await fixture.database .prepare( `INSERT INTO agent ( @@ -57,12 +71,7 @@ async function insertOverviewAgent( ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, ) .bind( - JSON.stringify({ - packageMcpServers: [], - packageResolution: null, - packageSkills: [], - providerOptions: {}, - }), + configJson, 1, "Extra overview fixture.", input.id, @@ -74,11 +83,46 @@ async function insertOverviewAgent( "Help with overview tests.", "openai", "openai-runtime", - "published", + "draft", input.updatedAt, "private", ) .run(); + + await fixture.database.batch([ + fixture.database + .prepare( + `INSERT INTO agent_deployment_version ( + agent_id, config_json, created_at, created_by_account_id, environment_id, + id, kind, mcp_bindings_json, model, prompt, provider, runtime_id, + skills_json, summary, version_number + ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, + ) + .bind( + input.id, + configJson, + input.updatedAt, + fixture.viewer.id, + null, + OVERVIEW_AGENT_DEPLOYMENT_VERSION_ID, + "cattle", + "[]", + "gpt-5.4", + "Help with overview tests.", + "openai", + "openai-runtime", + "[]", + "Initial publish", + 1, + ), + fixture.database + .prepare( + `UPDATE agent + SET live_deployment_version_id = ?, status = 'published', updated_at = ? + WHERE id = ? AND status = 'draft'`, + ) + .bind(OVERVIEW_AGENT_DEPLOYMENT_VERSION_ID, input.updatedAt, input.id), + ]); } async function insertOverviewCredentialMetadata( @@ -131,6 +175,7 @@ async function insertOverviewDeploymentMetadata( deleted_at, id, last_successful_url, + latest_run_id, mosoo_subdomain, owner_account_id, repo_name, @@ -138,7 +183,7 @@ async function insertOverviewDeploymentMetadata( repo_url, source_kind, updated_at - ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, + ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, ) .bind( fixture.ids.appId, @@ -146,7 +191,8 @@ async function insertOverviewDeploymentMetadata( "main", null, OVERVIEW_DEPLOYMENT_ID, - liveUrl, + null, + OVERVIEW_DEPLOYMENT_RUN_ID, `app-${fixture.ids.appId.toLowerCase()}`, fixture.viewer.id, "awire", @@ -183,13 +229,56 @@ async function insertOverviewDeploymentMetadata( OVERVIEW_DEPLOYMENT_RUN_ID, "main", "abc123", - "success", - "cloudflare_pages", + "preparing", + "cloudflare_static_assets", 2, - liveUrl, + null, ) .run(); + const nowMs = Date.now(); + const admission = await admitApiCommand(fixture.bindings, { + dedupeKey: createAppDeploymentRunDispatchDedupeKey(OVERVIEW_DEPLOYMENT_RUN_ID), + kind: "app_deployment_run_dispatch", + payload: { appDeploymentRunId: OVERVIEW_DEPLOYMENT_RUN_ID }, + }); + const disposition = await processApiCommandDelivery( + fixture.bindings, + { commandId: admission.commandId, deliveryGeneration: 1 }, + () => nowMs, + { + dispatchAppDeploymentRun: async (_bindings, _input, authority) => { + const activeScriptName = appDeploymentCandidateScriptName(authority); + const candidate = { + authority, + deploymentId: OVERVIEW_DEPLOYMENT_ID, + nowMs, + runId: OVERVIEW_DEPLOYMENT_RUN_ID, + scriptName: activeScriptName, + } as const; + await registerAppDeploymentScriptCandidate(fixture.database, candidate); + await markAppDeploymentScriptUploadStarted(fixture.database, candidate); + await fixture.database + .prepare("UPDATE app_deployment_run SET status = 'activating' WHERE id = ?") + .bind(OVERVIEW_DEPLOYMENT_RUN_ID) + .run(); + return { + activeScriptName, + deploymentId: OVERVIEW_DEPLOYMENT_ID, + externalDeploymentId: null, + externalProjectId: null, + externalVersionId: null, + kind: "succeeded" as const, + runId: OVERVIEW_DEPLOYMENT_RUN_ID, + url: liveUrl, + }; + }, + }, + ); + if (disposition.kind !== "finished") { + throw new Error("Overview deployment fixture did not reach its durable success state."); + } + return { liveUrl }; } @@ -202,6 +291,7 @@ describe("App overview", () => { const credential = schema.getType("AppOverviewProviderCredential"); const deployment = schema.getType("AppDeployment"); const deploymentRun = schema.getType("AppDeploymentRun"); + const deploymentTargetKind = schema.getType("AppDeploymentTargetKind"); const deployInput = schema.getType("DeployAppInput"); if ( @@ -211,6 +301,7 @@ describe("App overview", () => { !isObjectType(credential) || !isObjectType(deployment) || !isObjectType(deploymentRun) || + !isEnumType(deploymentTargetKind) || !isInputObjectType(deployInput) ) { throw new Error("Expected App overview GraphQL types."); @@ -229,6 +320,10 @@ describe("App overview", () => { expect(String(appOverview.getFields().deployment.type)).toBe("AppDeployment"); expect(String(deployment.getFields().latestRun.type)).toBe("AppDeploymentRun"); expect(String(deploymentRun.getFields().status.type)).toBe("AppDeploymentRunStatus!"); + expect(deploymentTargetKind.getValues().map(({ name }) => name)).toEqual([ + "cloudflare_static_assets", + "cloudflare_worker", + ]); expect(String(deploymentStatus.type)).toBe("AppDeploymentRun"); expect(String(deploy.type)).toBe("AppDeploymentRun!"); expect(String(deleteDeployment.type)).toBe("OperationResult!"); @@ -266,7 +361,7 @@ describe("App overview", () => { latestRun: { liveUrl: deploymentFixture.liveUrl, status: "success", - targetKind: "cloudflare_pages", + targetKind: "cloudflare_static_assets", }, liveUrl: deploymentFixture.liveUrl, plannedUrl: deploymentFixture.liveUrl, diff --git a/apps/api/tests/bound-agent-idempotency.e2e.test.ts b/apps/api/tests/bound-agent-idempotency.e2e.test.ts index 10ca1712..a1c5964a 100644 --- a/apps/api/tests/bound-agent-idempotency.e2e.test.ts +++ b/apps/api/tests/bound-agent-idempotency.e2e.test.ts @@ -1,7 +1,12 @@ import { describe, expect, test } from "bun:test"; import { createPlatformId, parsePlatformId } from "@mosoo/id"; -import type { AgentDeploymentVersionId, SessionId, SessionMessageId } from "@mosoo/id"; +import type { + AgentDeploymentVersionId, + SessionId, + SessionMessageId, + SessionRunId, +} from "@mosoo/id"; import { Hono } from "hono"; import { registerPublicApiRoute } from "../src/adapters/http/routes/public-api-route"; @@ -16,20 +21,28 @@ import { import { mintAppAgentCapabilityToken } from "../src/modules/public-api/app-agent-capability"; import type { AppAgentCapabilityClaims } from "../src/modules/public-api/app-agent-capability"; import { queueSessionRun } from "../src/modules/runtime/application/session-run.service"; +import { recordCanonicalSessionRunTerminal } from "../src/modules/runtime/application/session-runs/session-run-terminal-failure.service"; +import { prepareAssistantMessageProjection } from "../src/modules/runtime/infrastructure/driver-instance/assistant-message-projection"; +import { + appendSessionRuntimeEvents, + createSessionRuntimeEvent, +} from "../src/modules/sessions/application/session-event-write.service"; import type { ApiBindings, ApiGatewayEnvironment } from "../src/platform/cloudflare/worker-types"; +import { + BOUND_DEPLOYMENT_ID, + BOUND_DEPLOYMENT_RUN_ID, + deleteBoundDeployment, + insertBoundDeployment, +} from "./bound-capability-fixtures"; import { PUBLIC_API_TEST_IDS, createApiCommandQueueStub, createPublicHttpContractDatabase, createPublicHttpTestBindings, createTestExecutionContext, - nowMsForTest, } from "./helpers/public-api-http-test-fixture"; import type { ApiCommandQueueStub, SqliteD1Database } from "./helpers/public-api-http-test-fixture"; -const DEPLOYMENT_ID = "01J0000000000000000000000D"; -const DEPLOYMENT_RUN_ID = "01J0000000000000000000000R"; - interface DispatchCommandPayload { session: { id: string }; sessionRunId: string; @@ -62,57 +75,17 @@ function capabilityClaims( expose: "public_thread", name: "Public API Agent", }, - deploymentId: DEPLOYMENT_ID, - deploymentRunId: DEPLOYMENT_RUN_ID, + deploymentId: BOUND_DEPLOYMENT_ID, + deploymentRunId: BOUND_DEPLOYMENT_RUN_ID, exp: Date.now() + 60_000, ...overrides, }; } -async function insertDeploymentAuthority( - database: SqliteD1Database, - bindings: AppAgentCapabilityClaims["binding"][], -): Promise { - database.execute(` - CREATE TABLE app_deployment ( - app_id text NOT NULL, - deleted_at integer, - id text PRIMARY KEY NOT NULL - ); - - CREATE TABLE app_deployment_run ( - app_id text NOT NULL, - deployment_id text NOT NULL, - id text PRIMARY KEY NOT NULL, - plan_json text, - status text NOT NULL - ); - - CREATE INDEX app_deployment_run_deployment_id_idx - ON app_deployment_run (deployment_id, id); - `); - - await database - .prepare("INSERT INTO app_deployment (app_id, deleted_at, id) VALUES (?, NULL, ?)") - .bind(PUBLIC_API_TEST_IDS.app, DEPLOYMENT_ID) - .run(); - await database - .prepare( - "INSERT INTO app_deployment_run (app_id, deployment_id, id, plan_json, status) VALUES (?, ?, ?, ?, 'success')", - ) - .bind( - PUBLIC_API_TEST_IDS.app, - DEPLOYMENT_ID, - DEPLOYMENT_RUN_ID, - JSON.stringify({ agentBindings: bindings }), - ) - .run(); -} - function createCompletingApiCommandQueue(database: SqliteD1Database): ApiCommandQueueStub { const sent: ApiCommandQueueStub["sent"] = []; - return { + const queue: ApiCommandQueueStub = { sent, async send(body: ApiCommandMessage, options): Promise { sent.push({ @@ -132,69 +105,65 @@ function createCompletingApiCommandQueue(database: SqliteD1Database): ApiCommand } const payload = JSON.parse(command.payloadJson) as DispatchCommandPayload; - const timestampMs = nowMsForTest() + sent.length; - - await database - .prepare( - `UPDATE session_run - SET status = 'completed', - completed_at = ?, - status_changed_at = ?, - status_event = 'run.complete', - status_seq = status_seq + 1, - status_source = 'driver', - updated_at = ? - WHERE id = ?`, - ) - .bind(timestampMs, timestampMs, timestampMs, payload.sessionRunId) - .run(); - await database - .prepare( - `UPDATE session - SET status = 'IDLE', - status_seq = status_seq + 1, - message_seq_cursor = message_seq_cursor + 1, - last_message_at = ?, - updated_at = ? - WHERE id = ?`, - ) - .bind(timestampMs, timestampMs, payload.session.id) - .run(); - - const session = await database - .prepare("SELECT message_seq_cursor AS seq FROM session WHERE id = ?") - .bind(payload.session.id) - .first<{ seq: number }>(); - - if (session === null) { - throw new Error("Queued Session is missing."); - } - - await database - .prepare( - `INSERT INTO session_message ( - content_text, - created_at, - created_by_account_id, - id, - plan_json, - role, - segments_json, - seq, - session_id, - session_run_id - ) VALUES (?, ?, ?, ?, NULL, 'assistant', NULL, ?, ?, ?)`, - ) - .bind( - "The original bound request completed.", - timestampMs, - PUBLIC_API_TEST_IDS.ownerAccount, - createPlatformId(), - session.seq, - payload.session.id, - payload.sessionRunId, - ) - .run(); + const sessionId = parsePlatformId(payload.session.id, "queued Session ID"); + const sessionRunId = parsePlatformId( + payload.sessionRunId, + "queued Session Run ID", + ); + const messageId = createPlatformId(); + const timestampMs = Date.now(); + const bindings = createPublicHttpTestBindings(database, { + apiCommandQueue: queue, + }) as ApiBindings; + + await appendSessionRuntimeEvents({ + bindings, + deliver: false, + events: [ + createSessionRuntimeEvent({ + actor: "driver", + kind: "message.added", + occurredAtMs: timestampMs, + origin: "driver", + payload: { + content: "The original bound request completed.", + messageId, + role: "agent", + }, + runId: sessionRunId, + sessionId, + sourceEventId: createPlatformId(), + visibility: "participant", + }), + createSessionRuntimeEvent({ + actor: "driver", + kind: "message.completed", + occurredAtMs: timestampMs, + origin: "driver", + payload: { messageId, role: "agent" }, + runId: sessionRunId, + sessionId, + sourceEventId: createPlatformId(), + visibility: "participant", + }), + ], + sessionId, + }); + await recordCanonicalSessionRunTerminal(bindings, { + assistantMessage: prepareAssistantMessageProjection({ + createdByAccountId: PUBLIC_API_TEST_IDS.ownerAccount, + messageId, + sessionId, + sessionRunId, + }), + deliver: false, + error: null, + runId: sessionRunId, + sessionId, + source: "api", + status: "completed", + timestampMs, + }); await database .prepare( `UPDATE api_command @@ -209,6 +178,8 @@ function createCompletingApiCommandQueue(database: SqliteD1Database): ApiCommand .run(); }, }; + + return queue; } function failFirstMatchingStatement(database: D1Database, pattern: RegExp): D1Database { @@ -324,7 +295,7 @@ async function withAcceleratedClock(operation: () => Promise): Promise async function createFixture(bindings = [capabilityClaims().binding]) { const database = await createPublicHttpContractDatabase(); - await insertDeploymentAuthority(database, bindings); + await insertBoundDeployment(database, { agentBindings: bindings }); const queue = createCompletingApiCommandQueue(database); return { database, queue }; @@ -496,10 +467,10 @@ describe("bound Agent HTTP idempotency", () => { const reservation = await database .prepare( - `SELECT run_id AS runId, session_id AS sessionId + `SELECT id, run_id AS runId, session_id AS sessionId FROM bound_agent_call_idempotency_key`, ) - .first<{ runId: string; sessionId: string }>(); + .first<{ id: string; runId: string; sessionId: string }>(); const viewer = await getAccountViewer(database, PUBLIC_API_TEST_IDS.ownerAccount); if (reservation === null || viewer === null) { @@ -510,7 +481,10 @@ describe("bound Agent HTTP idempotency", () => { // reserved Session itself must still prevent a later Run from becoming // the replay target for the original key. await database.prepare("UPDATE bound_agent_call_idempotency_key SET run_id = NULL").run(); - await database.prepare("UPDATE session_event SET source_event_id = id").run(); + await database + .prepare("UPDATE session_event SET source_event_id = id WHERE source_event_id = ?") + .bind(reservation.id) + .run(); const later = await queueSessionRun({ bindings: createPublicHttpTestBindings(database, { @@ -797,10 +771,7 @@ describe("bound Agent HTTP idempotency", () => { }); expect(first.status).toBe(200); - await database - .prepare("UPDATE app_deployment SET deleted_at = ? WHERE id = ?") - .bind(Date.now(), DEPLOYMENT_ID) - .run(); + await deleteBoundDeployment(database); const revoked = await requestBoundAgent({ claims: capabilityClaims(), diff --git a/apps/api/tests/bound-capability-fixtures.ts b/apps/api/tests/bound-capability-fixtures.ts index b4bf29e4..dba5d7e0 100644 --- a/apps/api/tests/bound-capability-fixtures.ts +++ b/apps/api/tests/bound-capability-fixtures.ts @@ -1,3 +1,7 @@ +import { appDeploymentRunsTable, appDeploymentsTable } from "@mosoo/db"; +import { parsePlatformId } from "@mosoo/id"; +import type { AppDeploymentId, AppDeploymentRunId } from "@mosoo/id"; +import { eq } from "drizzle-orm"; import type { Hono } from "hono"; import { mintAppAgentCapabilityToken } from "../src/modules/public-api/app-agent-capability"; @@ -6,15 +10,24 @@ import type { ApiBindings } from "../src/platform/cloudflare/worker-types"; import { PUBLIC_API_TEST_IDS, createPublicHttpTestBindings, + nowMsForTest, } from "./helpers/public-api-http-test-fixture"; import type { SqliteD1Database } from "./helpers/public-api-http-test-fixture"; import { requestPublicApiWithBindings } from "./public-thread-api-fixtures"; -export const BOUND_DEPLOYMENT_ID = "01J0000000000000000000000D"; -export const BOUND_DEPLOYMENT_RUN_ID = "01J0000000000000000000000R"; -export const BOUND_REPLACEMENT_DEPLOYMENT_RUN_ID = "01J0000000000000000000000S"; -export const BOUND_OTHER_DEPLOYMENT_ID = "01J0000000000000000000000E"; -export const BOUND_OTHER_DEPLOYMENT_RUN_ID = "01J0000000000000000000000T"; +export const BOUND_DEPLOYMENT_ID = parsePlatformId("01J0000000000000000000000D"); +export const BOUND_DEPLOYMENT_RUN_ID = parsePlatformId( + "01J0000000000000000000000R", +); +export const BOUND_REPLACEMENT_DEPLOYMENT_RUN_ID = parsePlatformId( + "01J0000000000000000000000S", +); +export const BOUND_OTHER_DEPLOYMENT_ID = parsePlatformId( + "01J0000000000000000000000E", +); +export const BOUND_OTHER_DEPLOYMENT_RUN_ID = parsePlatformId( + "01J0000000000000000000000T", +); export const BOUND_BINDING = { env: "MOSOO_AGENT_URL", @@ -36,51 +49,42 @@ export function boundCapabilityClaims( }; } -/** - * The minimal Deployment authority tables the capability checks read. Mirrors - * the production columns the authority service touches; the public HTTP core - * schema does not include them. - */ -export function createBoundDeploymentAuthoritySchema(database: SqliteD1Database): void { - database.execute(` - CREATE TABLE app_deployment ( - app_id text NOT NULL, - deleted_at integer, - id text PRIMARY KEY NOT NULL - ); - - CREATE TABLE app_deployment_run ( - app_id text NOT NULL, - deployment_id text NOT NULL, - id text PRIMARY KEY NOT NULL, - plan_json text, - status text NOT NULL - ); - - CREATE INDEX app_deployment_run_deployment_id_idx - ON app_deployment_run (deployment_id, id); - `); -} - export async function insertBoundDeployment( database: SqliteD1Database, input: { agentBindings?: unknown[]; deletedAt?: number | null; - deploymentId?: string; - deploymentRunId?: string; + deploymentId?: AppDeploymentId; + deploymentRunId?: AppDeploymentRunId; } = {}, ): Promise { const deploymentId = input.deploymentId ?? BOUND_DEPLOYMENT_ID; + const deploymentRunId = input.deploymentRunId ?? BOUND_DEPLOYMENT_RUN_ID; + const nowMs = nowMsForTest(); await database - .prepare("INSERT INTO app_deployment (app_id, deleted_at, id) VALUES (?, ?, ?)") - .bind(PUBLIC_API_TEST_IDS.app, input.deletedAt ?? null, deploymentId) + .app() + .insert(appDeploymentsTable) + .values({ + appId: PUBLIC_API_TEST_IDS.app, + createdAt: nowMs, + defaultBranch: "main", + deletedAt: input.deletedAt ?? null, + id: deploymentId, + latestRunId: deploymentRunId, + mosooSubdomain: `bound-${deploymentId.toLowerCase()}`, + ownerAccountId: PUBLIC_API_TEST_IDS.ownerAccount, + repoName: "bound-capability", + repoOwner: "mosoo", + repoUrl: "https://github.com/mosoo/bound-capability", + sourceKind: "github_public", + updatedAt: nowMs, + }) .run(); await insertBoundDeploymentRun(database, { agentBindings: input.agentBindings ?? [BOUND_BINDING], deploymentId, - deploymentRunId: input.deploymentRunId ?? BOUND_DEPLOYMENT_RUN_ID, + deploymentRunId, }); } @@ -88,22 +92,33 @@ export async function insertBoundDeploymentRun( database: SqliteD1Database, input: { agentBindings: unknown[]; - deploymentId: string; - deploymentRunId: string; - status?: string; + deploymentId: AppDeploymentId; + deploymentRunId: AppDeploymentRunId; + status?: "failed" | "success"; }, ): Promise { + const nowMs = nowMsForTest(); + await database + .app() + .insert(appDeploymentRunsTable) + .values({ + appId: PUBLIC_API_TEST_IDS.app, + createdAt: nowMs, + deploymentId: input.deploymentId, + id: input.deploymentRunId, + planJson: JSON.stringify({ agentBindings: input.agentBindings }), + sourceBranch: "main", + sourceCommitSha: "a".repeat(40), + status: input.status ?? "success", + updatedAt: nowMs, + }) + .run(); + await database - .prepare( - "INSERT INTO app_deployment_run (app_id, deployment_id, id, plan_json, status) VALUES (?, ?, ?, ?, ?)", - ) - .bind( - PUBLIC_API_TEST_IDS.app, - input.deploymentId, - input.deploymentRunId, - JSON.stringify({ agentBindings: input.agentBindings }), - input.status ?? "success", - ) + .app() + .update(appDeploymentsTable) + .set({ latestRunId: input.deploymentRunId, updatedAt: nowMs }) + .where(eq(appDeploymentsTable.id, input.deploymentId)) .run(); } @@ -112,19 +127,21 @@ export async function deleteBoundDeployment( deploymentId = BOUND_DEPLOYMENT_ID, ): Promise { await database - .prepare("UPDATE app_deployment SET deleted_at = ? WHERE id = ?") - .bind(Date.now(), deploymentId) + .app() + .update(appDeploymentsTable) + .set({ deletedAt: Date.now() }) + .where(eq(appDeploymentsTable.id, deploymentId)) .run(); } -export async function mintBoundCapabilityToken( +async function mintBoundCapabilityToken( bindings: ApiBindings, claims: AppAgentCapabilityClaims = boundCapabilityClaims(), ): Promise { return mintAppAgentCapabilityToken(bindings.RUNTIME_ACTION_TOKEN_SECRET, claims); } -export function boundCapabilityUrl(token: string, path = ""): string { +function boundCapabilityUrl(token: string, path = ""): string { return `https://api.example.com/api/v1/bound/${token}${path}`; } diff --git a/apps/api/tests/bound-capability-public-thread-api.e2e.test.ts b/apps/api/tests/bound-capability-public-thread-api.e2e.test.ts index 22b9c7cb..7ca60d88 100644 --- a/apps/api/tests/bound-capability-public-thread-api.e2e.test.ts +++ b/apps/api/tests/bound-capability-public-thread-api.e2e.test.ts @@ -1,9 +1,13 @@ import { describe, expect, test } from "bun:test"; -import { sessionRunsTable, sessionsTable } from "@mosoo/db"; -import { eq } from "drizzle-orm"; +import { createPlatformId } from "@mosoo/id"; -import { insertSessionMessage } from "../src/modules/sessions/infrastructure/session-message-store.repository"; +import { recordCanonicalSessionRunTerminal } from "../src/modules/runtime/application/session-runs/session-run-terminal-failure.service"; +import { prepareAssistantMessageProjection } from "../src/modules/runtime/infrastructure/driver-instance/assistant-message-projection"; +import { + appendSessionRuntimeEvents, + createSessionRuntimeEvent, +} from "../src/modules/sessions/application/session-event-write.service"; import { BOUND_DEPLOYMENT_ID, BOUND_DEPLOYMENT_RUN_ID, @@ -13,7 +17,6 @@ import { BOUND_BINDING, boundCapabilityClaims, createBoundCapabilityClient, - createBoundDeploymentAuthoritySchema, createBoundTestBindings, deleteBoundDeployment, insertBoundDeployment, @@ -52,13 +55,12 @@ interface BoundSurface { async function createBoundSurface(claims = boundCapabilityClaims()): Promise { const database = await createPublicHttpContractDatabase(); - createBoundDeploymentAuthoritySchema(database); await insertBoundDeployment(database); const app = createPublicThreadApiTestApp(); const bucket = new PublicApiMemoryFileBucket(); const bindings = createBoundTestBindings(database, { - fileBucket: bucket as unknown as R2Bucket, + fileBucket: bucket, }); const client = await createBoundCapabilityClient({ app, bindings, claims }); @@ -190,33 +192,55 @@ async function simulateArtifactAndCompletion( httpMetadata: { contentType: "application/zip" }, }); - await insertSessionMessage(surface.database, { - content: FINAL_OUTPUT_TEXT, - createdByAccountId: PUBLIC_API_TEST_IDS.ownerAccount, - role: "assistant", - segments: [{ kind: "text", text: FINAL_OUTPUT_TEXT }], + const finalMessageId = createPlatformId(); + await appendSessionRuntimeEvents({ + bindings: surface.bindings, + deliver: false, + events: [ + createSessionRuntimeEvent({ + actor: "driver", + kind: "message.added", + occurredAtMs: 1_100, + origin: "driver", + payload: { + content: FINAL_OUTPUT_TEXT, + messageId: finalMessageId, + role: "agent", + }, + runId: input.runId, + sessionId: input.threadId, + sourceEventId: createPlatformId(), + visibility: "participant", + }), + createSessionRuntimeEvent({ + actor: "driver", + kind: "message.completed", + occurredAtMs: 1_125, + origin: "driver", + payload: { messageId: finalMessageId, role: "agent" }, + runId: input.runId, + sessionId: input.threadId, + sourceEventId: createPlatformId(), + visibility: "participant", + }), + ], sessionId: input.threadId, - sessionRunId: input.runId, }); - await surface.database - .app() - .update(sessionRunsTable) - .set({ - completedAt: 1_150, - errorCode: null, - errorDetailsJson: null, - errorMessage: null, - status: "completed", - updatedAt: 1_150, - }) - .where(eq(sessionRunsTable.id, input.runId)) - .run(); - await surface.database - .app() - .update(sessionsTable) - .set({ lastRunId: input.runId, status: "IDLE", updatedAt: 1_150 }) - .where(eq(sessionsTable.id, input.threadId)) - .run(); + await recordCanonicalSessionRunTerminal(surface.bindings, { + assistantMessage: prepareAssistantMessageProjection({ + createdByAccountId: PUBLIC_API_TEST_IDS.ownerAccount, + messageId: finalMessageId, + sessionId: input.threadId, + sessionRunId: input.runId, + }), + deliver: false, + error: null, + runId: input.runId, + sessionId: input.threadId, + source: "api", + status: "completed", + timestampMs: 1_150, + }); return artifactId; } @@ -376,18 +400,6 @@ describe("bound capability Public Thread API e2e", () => { test("keeps the capability inside its App, declared Agent, and Deployment", async () => { const surface = await createBoundSurface(); - await insertBoundDeployment(surface.database, { - deploymentId: BOUND_OTHER_DEPLOYMENT_ID, - deploymentRunId: BOUND_OTHER_DEPLOYMENT_RUN_ID, - }); - const otherDeployment = await createBoundCapabilityClient({ - app: createPublicThreadApiTestApp(), - bindings: surface.bindings, - claims: boundCapabilityClaims({ - deploymentId: BOUND_OTHER_DEPLOYMENT_ID, - deploymentRunId: BOUND_OTHER_DEPLOYMENT_RUN_ID, - }), - }); await withProviderProbeMock(async () => { const fileId = await uploadAttachment(surface); @@ -423,7 +435,22 @@ describe("bound capability Public Thread API e2e", () => { }); expect(ownerThreadContinue.status).toBe(404); - // Another Deployment of the same App and Agent cannot see this Thread. + // Rotate the App to a new Deployment. The production schema permits one + // active Deployment per App, and the successor cannot see its + // predecessor's Threads. + await deleteBoundDeployment(surface.database); + await insertBoundDeployment(surface.database, { + deploymentId: BOUND_OTHER_DEPLOYMENT_ID, + deploymentRunId: BOUND_OTHER_DEPLOYMENT_RUN_ID, + }); + const otherDeployment = await createBoundCapabilityClient({ + app: createPublicThreadApiTestApp(), + bindings: surface.bindings, + claims: boundCapabilityClaims({ + deploymentId: BOUND_OTHER_DEPLOYMENT_ID, + deploymentRunId: BOUND_OTHER_DEPLOYMENT_RUN_ID, + }), + }); const crossDeployment = await otherDeployment.request(`/threads/${threadId}`); expect(crossDeployment.status).toBe(404); const crossDeploymentList = await otherDeployment.request("/threads"); @@ -561,8 +588,8 @@ describe("bound capability Public Thread API e2e", () => { test("cleans up the Thread when deletion wins the guarded Run insert race", async () => { const surface = await createBoundSurface(); const revoking = revokeDeploymentWhenRunInsertStarts(surface.database); - const bindings = createBoundTestBindings(revoking as unknown as SqliteD1Database, { - fileBucket: surface.bucket as unknown as R2Bucket, + const bindings = createBoundTestBindings(revoking, { + fileBucket: surface.bucket, }); const client = await createBoundCapabilityClient({ app: createPublicThreadApiTestApp(), diff --git a/apps/api/tests/cattle-continuation-restore.test.ts b/apps/api/tests/cattle-continuation-restore.test.ts index 920cb710..7af652ab 100644 --- a/apps/api/tests/cattle-continuation-restore.test.ts +++ b/apps/api/tests/cattle-continuation-restore.test.ts @@ -105,9 +105,14 @@ async function createContinuationFixture(): Promise<{ .app() .insert(sandboxesTable) .values({ + agentId: PUBLIC_API_TEST_IDS.agent, + appId: PUBLIC_API_TEST_IDS.app, createdAt: now, id: PUBLIC_API_TEST_IDS.sandbox, + incarnation: 1, kind: "cattle", + networkConstraintsHash: "0".repeat(64), + ownerAccountId: PUBLIC_API_TEST_IDS.ownerAccount, status: "active", subjectId: PUBLIC_API_TEST_IDS.ownerSession, subjectKind: "session", @@ -122,6 +127,7 @@ async function createContinuationFixture(): Promise<{ cwd: SESSION_CWD, originJson: JSON.stringify(ORIGIN), sandboxId: PUBLIC_API_TEST_IDS.sandbox, + sandboxIncarnation: 1, sandboxSessionId: PRIOR_SANDBOX_SESSION_ID, sessionId: PUBLIC_API_TEST_IDS.ownerSession, status: "closed", @@ -136,10 +142,14 @@ async function createContinuationFixture(): Promise<{ dir: SESSION_CWD, id: STORED_BACKUP_ID, keep: false, + operationId: PUBLIC_API_TEST_IDS.operation, sandboxId: PUBLIC_API_TEST_IDS.sandbox, + sandboxIncarnation: 1, + stagingId: STORED_BACKUP_ID, status: "ready", ttlSeconds: 10 * 365 * 24 * 60 * 60, updatedAt: now, + workspaceSessionId: PUBLIC_API_TEST_IDS.ownerSession, }) .run(); @@ -154,6 +164,7 @@ function createInput(sandbox: SandboxHandle) { origin: ORIGIN, sandbox, sandboxId: PUBLIC_API_TEST_IDS.sandbox, + sandboxIncarnation: 1, sessionId: PUBLIC_API_TEST_IDS.ownerSession, }; } diff --git a/apps/api/tests/cattle-terminal-checkpoint.test.ts b/apps/api/tests/cattle-terminal-checkpoint.test.ts index bbcd895e..5518977a 100644 --- a/apps/api/tests/cattle-terminal-checkpoint.test.ts +++ b/apps/api/tests/cattle-terminal-checkpoint.test.ts @@ -1,8 +1,24 @@ import { describe, expect, test } from "bun:test"; -import { releaseTerminalDriverInstanceSessionRun } from "../src/modules/runtime/infrastructure/driver-instance/terminal-run-release"; +import { getSessionRuntimeStatePath } from "@mosoo/agent-driver/paths"; + +import { cleanupDriverInstances } from "../src/modules/runtime/infrastructure/driver-instance/maintenance"; +import { + releaseTerminalDriverInstanceSessionRun, + repairTerminalDriverRuntimeCommandsGlobally, +} from "../src/modules/runtime/infrastructure/driver-instance/terminal-run-release"; +import { repairClaimedDriverStopsGlobally } from "../src/modules/runtime/infrastructure/driver-session-stop.service"; import { encodeSandboxBackupIdForStorage } from "../src/modules/runtime/infrastructure/sandbox-backup-id"; -import type { SandboxHandle } from "../src/modules/runtime/infrastructure/sandbox-handles"; +import { + claimSandboxBackupStageActual, + finalizeSandboxBackupStage, + getSandboxBackupStage, + stageSandboxBackupWrites, +} from "../src/modules/runtime/infrastructure/sandbox-backup-store"; +import type { + RuntimeSubjectIncarnationHandle, + SandboxHandle, +} from "../src/modules/runtime/infrastructure/sandbox-handles"; import { isCattleTerminalCheckpointReadyForNextRun } from "../src/modules/runtime/infrastructure/session-runs/session-run-admission.repository"; import type { ApiBindings } from "../src/platform/cloudflare/worker-types"; import { @@ -19,13 +35,19 @@ const CREATED_BACKUP_ID = "550e8400-e29b-41d4-a716-446655440002"; interface CheckpointSandboxState { backupAvailable: boolean; - backupOptions: Array<{ dir: string; ttl: number | undefined }>; + backupOptions: Array<{ + dir: string; + excludes: string[] | undefined; + forbiddenPaths: string[] | undefined; + name: string; + ttl: number | undefined; + }>; createBackupCalls: number; } function createCheckpointSandbox(state: CheckpointSandboxState): { commands: string[]; - sandbox: SandboxHandle; + sandbox: RuntimeSubjectIncarnationHandle & SandboxHandle; } { const commands: string[] = []; const unavailable = async (): Promise => { @@ -35,10 +57,18 @@ function createCheckpointSandbox(state: CheckpointSandboxState): { return { commands, sandbox: { + activateRuntimeSubjectIncarnation: unavailable, configureNetworkConstraints: unavailable, - async createBackup(options) { + createBackup: unavailable, + async createRuntimeSubjectBackup(_incarnation, options) { state.createBackupCalls += 1; - state.backupOptions.push({ dir: options.dir, ttl: options.ttl }); + state.backupOptions.push({ + dir: options.dir, + excludes: options.excludes, + forbiddenPaths: options.forbiddenPaths, + name: options.name, + ttl: options.ttl, + }); if (!state.backupAvailable) { throw new Error("backup service unavailable"); @@ -49,11 +79,14 @@ function createCheckpointSandbox(state: CheckpointSandboxState): { createSession: unavailable, deleteSession: unavailable, destroy: unavailable, + destroyRuntimeSubjectIncarnation: unavailable, async exec(command) { commands.push(command); return { exitCode: 0, stderr: "", stdout: "", success: true }; }, getSession: unavailable, + inspectRuntimeSubjectIncarnation: unavailable, + markRuntimeSubjectIncarnationReady: unavailable, mkdir: async () => {}, mountBucket: unavailable, readFile: unavailable, @@ -84,33 +117,36 @@ async function createTerminalCheckpointFixture(): Promise<{ WHERE id = '${PUBLIC_API_TEST_IDS.ownerSession}'; INSERT INTO sandbox ( - id, kind, subject_kind, subject_id, status, bind_mount_ready, + agent_id, app_id, id, incarnation, kind, network_constraints_hash, + owner_account_id, subject_kind, subject_id, status, bind_mount_ready, global_mounts_json, created_at, updated_at ) VALUES ( - '${PUBLIC_API_TEST_IDS.sandbox}', 'cattle', 'session', '${PUBLIC_API_TEST_IDS.ownerSession}', + '${PUBLIC_API_TEST_IDS.agent}', '${PUBLIC_API_TEST_IDS.app}', + '${PUBLIC_API_TEST_IDS.sandbox}', 1, 'cattle', '${"0".repeat(64)}', + '${PUBLIC_API_TEST_IDS.ownerAccount}', 'session', '${PUBLIC_API_TEST_IDS.ownerSession}', 'active', 1, '[]', 1, 1 ); INSERT INTO sandbox_session ( cloudflare_session_id, created_at, cwd, origin_json, sandbox_id, - session_id, status, updated_at + sandbox_incarnation, session_id, status, updated_at ) VALUES ( '01J0000000000000000000000Z', 1, '${SESSION_CWD}', '{"callerUserId":"${PUBLIC_API_TEST_IDS.ownerAccount}","entrypoint":"api","executionOwnerUserId":"${PUBLIC_API_TEST_IDS.ownerAccount}","type":"agent"}', - '${PUBLIC_API_TEST_IDS.sandbox}', '${PUBLIC_API_TEST_IDS.ownerSession}', 'active', 1 + '${PUBLIC_API_TEST_IDS.sandbox}', 1, '${PUBLIC_API_TEST_IDS.ownerSession}', 'active', 1 ); INSERT INTO driver_instance ( id, boot_token_expires_at, boot_token_hash, connection_id, created_at, expires_at, heartbeat_count, protocol, protocol_version, runtime, - sandbox_id, sandbox_session_id, status, updated_at + sandbox_id, sandbox_incarnation, sandbox_session_id, status, updated_at ) VALUES ( '${PUBLIC_API_TEST_IDS.driverOwner}', 1, X'01', 'checkpoint-connection', 1, 1, 0, 'orpc-ws', 1, 'openai-runtime', '${PUBLIC_API_TEST_IDS.sandbox}', - '${PUBLIC_API_TEST_IDS.ownerSession}', 'ready', 1 + 1, '${PUBLIC_API_TEST_IDS.ownerSession}', 'ready', 1 ); INSERT INTO session_run ( @@ -125,19 +161,6 @@ async function createTerminalCheckpointFixture(): Promise<{ 'openai-runtime', 'trace-checkpoint', 1, 2, 1, 2 ); - CREATE TABLE native_resume_ref ( - committed_session_run_id text, - committed_value text, - created_at integer NOT NULL, - kind text NOT NULL, - observed_driver_instance_id text, - observed_session_run_id text, - runtime_id text NOT NULL, - session_id text PRIMARY KEY NOT NULL, - updated_at integer NOT NULL, - value text NOT NULL - ); - INSERT INTO native_resume_ref ( created_at, kind, observed_driver_instance_id, observed_session_run_id, runtime_id, session_id, updated_at, value @@ -149,11 +172,13 @@ async function createTerminalCheckpointFixture(): Promise<{ ); INSERT INTO sandbox_backup ( - created_at, dir, id, keep, sandbox_id, session_run_id, status, ttl_seconds, updated_at + created_at, dir, id, keep, sandbox_id, sandbox_incarnation, session_run_id, + staging_id, status, ttl_seconds, updated_at, workspace_session_id ) VALUES ( - 1, '${SESSION_CWD}', '${PRIOR_BACKUP_ID}', 0, '${PUBLIC_API_TEST_IDS.sandbox}', - '${PUBLIC_API_TEST_IDS.runAlt}', 'ready', 315360000, 1 + 1, '${SESSION_CWD}', '${PRIOR_BACKUP_ID}', 0, '${PUBLIC_API_TEST_IDS.sandbox}', 0, + '${PUBLIC_API_TEST_IDS.runAlt}', '${PRIOR_BACKUP_ID}', 'ready', 315360000, 1, + '${PUBLIC_API_TEST_IDS.ownerSession}' ); `); @@ -175,11 +200,293 @@ async function createTerminalCheckpointFixture(): Promise<{ } describe("cattle terminal checkpoint", () => { + test("an old terminal Run cannot commit its resume ref after a successor starts", async () => { + const { database } = await createTerminalCheckpointFixture(); + database.execute(` + UPDATE driver_instance + SET status_operation_id = '${PUBLIC_API_TEST_IDS.run}' + WHERE id = '${PUBLIC_API_TEST_IDS.driverOwner}' + `); + const [write] = await stageSandboxBackupWrites(database, { + admission: { + driverGeneration: 0, + driverInstanceId: PUBLIC_API_TEST_IDS.driverOwner, + incarnation: 1, + kind: "terminal", + sessionId: PUBLIC_API_TEST_IDS.ownerSession, + sessionRunId: PUBLIC_API_TEST_IDS.run, + }, + sandboxId: PUBLIC_API_TEST_IDS.sandbox, + targets: [ + { + dir: SESSION_CWD, + updateSandboxLastBackup: false, + workspaceSessionId: PUBLIC_API_TEST_IDS.ownerSession, + }, + ], + ttlSeconds: 100, + }); + if (write?.kind !== "staged") { + throw new Error("Terminal backup stage was not created."); + } + const actualBackupId = encodeSandboxBackupIdForStorage("550e8400-e29b-41d4-a716-446655440004"); + await claimSandboxBackupStageActual(database, { + actualBackupId, + dir: SESSION_CWD, + sandboxIncarnation: 1, + stagingId: write.stage.id, + }); + await database + .prepare( + `INSERT INTO sandbox_backup ( + created_at, dir, id, keep, sandbox_id, sandbox_incarnation, session_run_id, + staging_id, status, ttl_seconds, updated_at, workspace_session_id + ) SELECT created_at, dir, actual_backup_id, 0, sandbox_id, sandbox_incarnation, + session_run_id, id, 'ready', ttl_seconds, + CAST(unixepoch('subsec') * 1000 AS INTEGER), workspace_session_id + FROM sandbox_backup_staging WHERE id = ?`, + ) + .bind(write.stage.id) + .run(); + database.execute(` + INSERT INTO session_run ( + id, session_id, agent_id, created_by_account_id, deployment_version_id, + deployment_version_number, driver_instance_id, trigger, status, provider, + model, runtime_id, trace_id, started_at, created_at, updated_at + ) VALUES ( + '${PUBLIC_API_TEST_IDS.runAlt}', '${PUBLIC_API_TEST_IDS.ownerSession}', + '${PUBLIC_API_TEST_IDS.agent}', '${PUBLIC_API_TEST_IDS.ownerAccount}', + '${PUBLIC_API_TEST_IDS.deployment}', 1, '${PUBLIC_API_TEST_IDS.driverOwner}', + 'resume', 'running', 'openai', 'gpt-5.4', 'openai-runtime', + 'trace-successor', 3, 3, 3 + ) + `); + + await expect( + finalizeSandboxBackupStage(database, { + actualBackupId, + stagingId: write.stage.id, + }), + ).resolves.toMatchObject({ candidateAccepted: true, complete: false }); + await expect( + database + .prepare("SELECT committed_session_run_id, committed_value FROM native_resume_ref") + .first(), + ).resolves.toEqual({ committed_session_run_id: null, committed_value: null }); + await expect(getSandboxBackupStage(database, write.stage.id)).resolves.not.toBeNull(); + }); + + test("does not admit a Thread from another workspace's same-path checkpoint", async () => { + const foreignBackupId = encodeSandboxBackupIdForStorage("550e8400-e29b-41d4-a716-446655440003"); + const insertCheckpoint = async ( + database: D1Database, + workspaceSessionId: string, + ): Promise => { + await database + .prepare( + `INSERT INTO sandbox_backup ( + created_at, dir, id, keep, sandbox_id, sandbox_incarnation, session_run_id, + staging_id, status, ttl_seconds, updated_at, workspace_session_id + ) VALUES (2, ?, ?, 0, ?, 1, ?, ?, 'ready', 315360000, 2, ?)`, + ) + .bind( + SESSION_CWD, + foreignBackupId, + PUBLIC_API_TEST_IDS.sandbox, + PUBLIC_API_TEST_IDS.run, + foreignBackupId, + workspaceSessionId, + ) + .run(); + }; + + const { database } = await createTerminalCheckpointFixture(); + await insertCheckpoint(database, PUBLIC_API_TEST_IDS.nonOwnerSession); + + await expect( + isCattleTerminalCheckpointReadyForNextRun(database, PUBLIC_API_TEST_IDS.ownerSession), + ).resolves.toBe(false); + + const { database: ownerDatabase } = await createTerminalCheckpointFixture(); + await insertCheckpoint(ownerDatabase, PUBLIC_API_TEST_IDS.ownerSession); + await expect( + isCattleTerminalCheckpointReadyForNextRun(ownerDatabase, PUBLIC_API_TEST_IDS.ownerSession), + ).resolves.toBe(true); + }); + + test("maintenance hands a stopped foreign owner to the terminal Run without a command ledger", async () => { + const fixture = await createTerminalCheckpointFixture(); + fixture.sandboxState.backupAvailable = true; + fixture.database.execute(` + UPDATE driver_instance + SET expires_at = 9999999999999, status = 'stopped', + status_operation_id = '${PUBLIC_API_TEST_IDS.operation}' + WHERE id = '${PUBLIC_API_TEST_IDS.driverOwner}' + `); + const controlRequests: string[] = []; + const bindings = { + ...fixture.bindings, + DriverConnection: { + get: () => ({ + fetch: async (request: Request) => { + controlRequests.push(new URL(request.url).pathname); + return Response.json({ ok: true }); + }, + }), + idFromName: () => "driver-do-id", + }, + } as unknown as ApiBindings; + + await expect( + fixture.database.prepare("SELECT COUNT(*) AS count FROM driver_command").first(), + ).resolves.toEqual({ count: 0 }); + + await cleanupDriverInstances(bindings); + + expect(controlRequests).toEqual(["/control/destroy"]); + expect(fixture.sandboxState.createBackupCalls).toBe(1); + await expect( + fixture.database + .prepare("SELECT status_operation_id FROM driver_instance WHERE id = ?") + .bind(PUBLIC_API_TEST_IDS.driverOwner) + .first(), + ).resolves.toEqual({ status_operation_id: null }); + await expect( + fixture.database + .prepare("SELECT driver_instance_id FROM session_run WHERE id = ?") + .bind(PUBLIC_API_TEST_IDS.run) + .first(), + ).resolves.toEqual({ driver_instance_id: PUBLIC_API_TEST_IDS.driverOwner }); + }); + + test("does not run foreign-owner terminal side effects after the Driver generation rotates", async () => { + const fixture = await createTerminalCheckpointFixture(); + fixture.sandboxState.backupAvailable = true; + fixture.database.execute(` + UPDATE driver_instance + SET status = 'stopped', status_operation_id = '${PUBLIC_API_TEST_IDS.operation}' + WHERE id = '${PUBLIC_API_TEST_IDS.driverOwner}' + `); + let rotateAfterCandidateRead = true; + const interceptCandidateRead = (statement: D1PreparedStatement): D1PreparedStatement => + new Proxy(statement, { + get(target, property, receiver) { + if (property === "bind") { + return (...values: unknown[]) => + interceptCandidateRead(Reflect.apply(target.bind, target, values)); + } + if (property === "all" || property === "raw") { + return async (...args: unknown[]) => { + const read = property === "all" ? target.all : target.raw; + const result = await Reflect.apply(read, target, args); + rotateAfterCandidateRead = false; + fixture.database.execute(` + UPDATE driver_instance + SET generation = generation + 1 + WHERE id = '${PUBLIC_API_TEST_IDS.driverOwner}' + `); + return result; + }; + } + return Reflect.get(target, property, receiver); + }, + }); + const racingDatabase = new Proxy(fixture.database, { + get(target, property) { + if (property === "prepare") { + return (query: string) => { + const statement = target.prepare(query); + return rotateAfterCandidateRead && query.includes("status_operation_id") + ? interceptCandidateRead(statement) + : statement; + }; + } + const value = Reflect.get(target, property, target) as unknown; + return typeof value === "function" ? value.bind(target) : value; + }, + }); + let controlRequests = 0; + const bindings = { + ...fixture.bindings, + DB: racingDatabase, + DriverConnection: { + get: () => ({ + fetch: async () => { + controlRequests += 1; + return Response.json({ ok: true }); + }, + }), + idFromName: () => "driver-do-id", + }, + } as unknown as ApiBindings; + + await repairClaimedDriverStopsGlobally(bindings); + + expect(rotateAfterCandidateRead).toBe(false); + expect(controlRequests).toBe(0); + expect(fixture.sandboxState.createBackupCalls).toBe(0); + await expect( + fixture.database + .prepare("SELECT generation, status_operation_id FROM driver_instance WHERE id = ?") + .bind(PUBLIC_API_TEST_IDS.driverOwner) + .first(), + ).resolves.toEqual({ generation: 1, status_operation_id: PUBLIC_API_TEST_IDS.operation }); + await expect( + fixture.database + .prepare("SELECT driver_instance_id FROM session_run WHERE id = ?") + .bind(PUBLIC_API_TEST_IDS.run) + .first(), + ).resolves.toEqual({ driver_instance_id: PUBLIC_API_TEST_IDS.driverOwner }); + }); + + test("rejects an old Driver generation before checkpoint or conversation side effects", async () => { + const { bindings, commands, database, sandboxState } = await createTerminalCheckpointFixture(); + sandboxState.backupAvailable = true; + database.execute(` + UPDATE driver_instance + SET generation = 1 + WHERE id = '${PUBLIC_API_TEST_IDS.driverOwner}'; + + INSERT INTO session_run ( + id, session_id, agent_id, created_by_account_id, deployment_version_id, + deployment_version_number, driver_instance_id, trigger, status, provider, + model, runtime_id, trace_id, started_at, created_at, updated_at + ) + VALUES ( + '${PUBLIC_API_TEST_IDS.runAlt}', '${PUBLIC_API_TEST_IDS.ownerSession}', + '${PUBLIC_API_TEST_IDS.agent}', '${PUBLIC_API_TEST_IDS.ownerAccount}', + '${PUBLIC_API_TEST_IDS.deployment}', 1, '${PUBLIC_API_TEST_IDS.driverOwner}', + 'resume', 'running', 'openai', 'gpt-5.4', 'openai-runtime', 'trace-r2', 3, 3, 3 + ); + + UPDATE session + SET last_run_id = '${PUBLIC_API_TEST_IDS.runAlt}', status = 'RUNNING', updated_at = 3 + WHERE id = '${PUBLIC_API_TEST_IDS.ownerSession}'; + `); + + await expect( + releaseTerminalDriverInstanceSessionRun(bindings, { + driverGeneration: 0, + driverInstanceId: PUBLIC_API_TEST_IDS.driverOwner, + sessionRunId: PUBLIC_API_TEST_IDS.run, + }), + ).rejects.toThrow("exact Driver ownership"); + expect(sandboxState.createBackupCalls).toBe(0); + expect(commands).toEqual([]); + await expect( + database + .prepare("SELECT status FROM sandbox_session WHERE session_id = ?") + .bind(PUBLIC_API_TEST_IDS.ownerSession) + .first("status"), + ).resolves.toBe("active"); + }); + test("keeps the last good checkpoint and blocks continuation until a retry commits the Run", async () => { const { bindings, commands, database, sandboxState } = await createTerminalCheckpointFixture(); await expect( releaseTerminalDriverInstanceSessionRun(bindings, { + driverGeneration: 0, driverInstanceId: PUBLIC_API_TEST_IDS.driverOwner, sessionRunId: PUBLIC_API_TEST_IDS.run, }), @@ -201,10 +508,7 @@ describe("cattle terminal checkpoint", () => { ).resolves.toEqual({ committed_session_run_id: null, committed_value: null }); sandboxState.backupAvailable = true; - await releaseTerminalDriverInstanceSessionRun(bindings, { - driverInstanceId: PUBLIC_API_TEST_IDS.driverOwner, - sessionRunId: PUBLIC_API_TEST_IDS.run, - }); + await repairTerminalDriverRuntimeCommandsGlobally(bindings); await expect( isCattleTerminalCheckpointReadyForNextRun(database, PUBLIC_API_TEST_IDS.ownerSession), @@ -235,17 +539,37 @@ describe("cattle terminal checkpoint", () => { committed_value: "thread-checkpointed", }); expect(sandboxState.backupOptions).toEqual([ - { dir: SESSION_CWD, ttl: 10 * 365 * 24 * 60 * 60 }, - { dir: SESSION_CWD, ttl: 10 * 365 * 24 * 60 * 60 }, + expect.objectContaining({ + dir: SESSION_CWD, + excludes: undefined, + forbiddenPaths: [ + `${getSessionRuntimeStatePath(PUBLIC_API_TEST_IDS.ownerSession, "openai-runtime")}/auth.json`, + ], + ttl: 10 * 365 * 24 * 60 * 60, + }), + expect.objectContaining({ + dir: SESSION_CWD, + excludes: undefined, + forbiddenPaths: [ + `${getSessionRuntimeStatePath(PUBLIC_API_TEST_IDS.ownerSession, "openai-runtime")}/auth.json`, + ], + ttl: 10 * 365 * 24 * 60 * 60, + }), ]); - expect(commands.join("\n")).toContain("session-files"); - expect(commands.join("\n")).toContain("driver-boot-payload-*.json"); - expect(commands.join("\n")).toContain("openai-runtime/auth.json"); + expect( + sandboxState.backupOptions.every(({ name }) => name.startsWith("mosoo:runtime-backup:v1:")), + ).toBe(true); + expect(commands).toEqual([]); + await repairTerminalDriverRuntimeCommandsGlobally(bindings); + expect(sandboxState.createBackupCalls).toBe(2); + const commandCount = commands.length; await releaseTerminalDriverInstanceSessionRun(bindings, { + driverGeneration: 0, driverInstanceId: PUBLIC_API_TEST_IDS.driverOwner, sessionRunId: PUBLIC_API_TEST_IDS.run, }); expect(sandboxState.createBackupCalls).toBe(2); + expect(commands).toHaveLength(commandCount); }); }); diff --git a/apps/api/tests/cloudflare-sandbox-network-contract.test.ts b/apps/api/tests/cloudflare-sandbox-network-contract.test.ts index c3d1f301..a95f3978 100644 --- a/apps/api/tests/cloudflare-sandbox-network-contract.test.ts +++ b/apps/api/tests/cloudflare-sandbox-network-contract.test.ts @@ -8,12 +8,12 @@ interface PackageMetadata { } describe("pinned Cloudflare Sandbox network contract", () => { - test("keeps the 0.12.6 start-time and persisted interception semantics we enforce", async () => { + test("keeps the 0.12.9 start-time and persisted interception semantics we enforce", async () => { const workspaceRequire = createRequire(import.meta.url); const sandboxPackagePath = workspaceRequire.resolve("@cloudflare/sandbox/package.json"); const sandboxPackage = (await Bun.file(sandboxPackagePath).json()) as PackageMetadata; - expect(sandboxPackage.version).toBe("0.12.6"); + expect(sandboxPackage.version).toBe("0.12.9"); const sandboxRequire = createRequire(sandboxPackagePath); const containersPackagePath = sandboxRequire.resolve("@cloudflare/containers/package.json"); diff --git a/apps/api/tests/cost-ledger-reconciliation.test.ts b/apps/api/tests/cost-ledger-reconciliation.test.ts index 44b35eb0..cf43a7c5 100644 --- a/apps/api/tests/cost-ledger-reconciliation.test.ts +++ b/apps/api/tests/cost-ledger-reconciliation.test.ts @@ -147,6 +147,7 @@ async function createReconciliationDatabase(): Promise { native_call_id text, output_tokens integer, provider text NOT NULL, + source_event_seq integer DEFAULT 0 NOT NULL, session_id text NOT NULL, session_run_id text NOT NULL, started_at integer, @@ -157,6 +158,14 @@ async function createReconciliationDatabase(): Promise { UNIQUE (session_run_id, call_key) ); + CREATE TABLE session_event ( + content_text text NOT NULL, + event_type text NOT NULL, + run_id text, + seq integer NOT NULL, + session_id text NOT NULL + ); + CREATE TABLE usage_event ( actor_user_id text NOT NULL, agent_id text NOT NULL, @@ -181,6 +190,7 @@ async function createReconciliationDatabase(): Promise { session_run_id text, source text NOT NULL, source_event_id text NOT NULL, + source_event_seq integer DEFAULT 0 NOT NULL, total_cost_usd_micros integer NOT NULL, usage_contract text NOT NULL, UNIQUE (source, source_event_id) @@ -232,13 +242,17 @@ async function createReconciliationDatabase(): Promise { payload_json text NOT NULL, status text NOT NULL, attempt_count integer DEFAULT 0 NOT NULL, + delivery_generation integer DEFAULT 1 NOT NULL, claim_owner text, claim_expires_at integer, last_error_code text, last_error_message text, completed_at integer, created_at integer NOT NULL, - updated_at integer NOT NULL + updated_at integer NOT NULL, + CONSTRAINT api_command_delivery_generation_check + CHECK (typeof(delivery_generation) = 'integer' + AND delivery_generation BETWEEN 1 AND 9007199254740991) ); CREATE UNIQUE INDEX api_command_dedupe_idx ON api_command (dedupe_key); @@ -663,6 +677,13 @@ describe("cost ledger reconciliation", () => { id: "01J00000000000000000000203", sourceEventId, }); + await database + .prepare( + `INSERT INTO session_event (content_text, event_type, run_id, seq, session_id) + VALUES (?, 'usage.updated', ?, 0, ?)`, + ) + .bind(JSON.stringify({ callId: "rolled-history" }), RUN_ID, SESSION_ID) + .run(); const bindings = createPublicHttpTestBindings(database) as ApiBindings; await runUsageDailyRollup(bindings, new Date(NOW_MS)); diff --git a/apps/api/tests/cost-usage-event.test.ts b/apps/api/tests/cost-usage-event.test.ts index 4b8a4952..b9b2c5ab 100644 --- a/apps/api/tests/cost-usage-event.test.ts +++ b/apps/api/tests/cost-usage-event.test.ts @@ -80,6 +80,7 @@ function createUsageEventDatabase(): SqliteD1Database { session_run_id text, source text NOT NULL, source_event_id text NOT NULL, + source_event_seq integer DEFAULT 0 NOT NULL, total_cost_usd_micros integer NOT NULL, usage_contract text NOT NULL, UNIQUE (source, source_event_id) @@ -394,7 +395,7 @@ describe("cost usage event", () => { }); }); - test("uses reported USD cost for a known model when token counters are unavailable", async () => { + test("preserves reported USD cost across explicit zero token corrections", async () => { const database = createUsageEventDatabase(); const usage = { costAmount: 0.42, @@ -403,7 +404,7 @@ describe("cost usage event", () => { usageContract: "openai_total_with_cached_breakdown", } satisfies SessionUsageSummary; - await recordRuntimeUsageEvent(database, { + const input = { callKey: "known-cost-only-call", driverInstanceId: DRIVER_INSTANCE_ID, nativeCallId: "known-cost-only-native-call", @@ -412,24 +413,39 @@ describe("cost usage event", () => { model: "gpt-5.4", provider: "openai", }, + sourceEventSeq: 1, usage, + }; + + await recordRuntimeUsageEvent(database, input); + await recordRuntimeUsageEvent(database, { + ...input, + sourceEventSeq: 2, + usage: { + inputTokens: 0, + outputTokens: 0, + source: "session_update", + usageContract: "openai_total_with_cached_breakdown", + }, }); const row = await database .prepare( ` - SELECT price_snapshot_json, pricing_status, total_cost_usd_micros + SELECT price_snapshot_json, pricing_status, source_event_seq, total_cost_usd_micros FROM usage_event `, ) .first<{ price_snapshot_json: string | null; pricing_status: string; + source_event_seq: number; total_cost_usd_micros: number; }>(); expect(row).toMatchObject({ pricing_status: "priced", + source_event_seq: 2, total_cost_usd_micros: 420_000, }); expect(JSON.parse(row?.price_snapshot_json ?? "{}")).toEqual({ @@ -440,4 +456,152 @@ describe("cost usage event", () => { tokenCountersUnavailable: true, }); }); + + test("updates durable usage only from a higher source event seq", async () => { + const database = createUsageEventDatabase(); + const input = { + callKey: "sequenced-usage", + driverInstanceId: DRIVER_INSTANCE_ID, + nativeCallId: "sequenced-native-call", + run: RUN_CONTEXT, + sourceEventSeq: 5, + usage: { + inputTokens: 10, + outputTokens: 5, + source: "prompt_response" as const, + usageContract: "openai_total_with_cached_breakdown" as const, + }, + }; + + await recordRuntimeUsageEvent(database, input); + await recordRuntimeUsageEvent(database, { + ...input, + sourceEventSeq: 4, + usage: { ...input.usage, inputTokens: 1 }, + }); + await recordRuntimeUsageEvent(database, input); + await expect( + recordRuntimeUsageEvent(database, { + ...input, + usage: { ...input.usage, inputTokens: 20 }, + }), + ).rejects.toThrow("replayed with conflicting content"); + await recordRuntimeUsageEvent(database, { + ...input, + sourceEventSeq: 6, + usage: { ...input.usage, inputTokens: 30 }, + }); + + expect( + await database + .prepare("SELECT input_tokens, source_event_seq FROM usage_event") + .first<{ input_tokens: number; source_event_seq: number }>(), + ).toEqual({ input_tokens: 30, source_event_seq: 6 }); + }); + + test("merges partial Anthropic cache buckets without losing prior counters", async () => { + const database = createUsageEventDatabase(); + const input = { + callKey: "partial-anthropic", + driverInstanceId: DRIVER_INSTANCE_ID, + nativeCallId: "partial-anthropic", + run: { + ...RUN_CONTEXT, + createdAtMs: Date.UTC(2026, 7, 31), + model: "claude-sonnet-5", + provider: "anthropic", + }, + sourceEventSeq: 5, + usage: { + cachedReadTokens: 2, + inputTokens: 10, + outputTokens: 5, + source: "prompt_response" as const, + usageContract: "anthropic_bucketed" as const, + }, + }; + const partialInput = { + ...input, + run: { ...input.run, createdAtMs: Date.UTC(2026, 8, 1) }, + sourceEventSeq: 6, + usage: { + cachedReadTokens: 3, + source: "prompt_response" as const, + usageContract: "anthropic_bucketed" as const, + }, + }; + + await recordRuntimeUsageEvent(database, input); + await recordRuntimeUsageEvent(database, partialInput); + await recordRuntimeUsageEvent(database, partialInput); + + expect( + await database + .prepare( + `SELECT cache_read_tokens, input_tokens, output_tokens, price_snapshot_json, + source_event_seq, total_cost_usd_micros + FROM usage_event`, + ) + .first(), + ).toEqual({ + cache_read_tokens: 3, + input_tokens: 13, + output_tokens: 5, + price_snapshot_json: JSON.stringify({ + billableInputTokens: 10, + cacheReadUsdPerMillion: 0.2, + cacheWriteUsdPerMillion: 2.5, + inputUsdPerMillion: 2, + longContextApplied: false, + model: "claude-sonnet-5", + outputUsdPerMillion: 10, + provider: "anthropic", + source: "mosoo_seed_2026_07_10", + }), + source_event_seq: 6, + total_cost_usd_micros: 71, + }); + }); + + test("applies explicit zero corrections without creating empty ledger rows", async () => { + const database = createUsageEventDatabase(); + const input = { + callKey: "zero-correction", + driverInstanceId: DRIVER_INSTANCE_ID, + nativeCallId: "zero-correction", + run: { ...RUN_CONTEXT, model: "gpt-5.4", provider: "openai" }, + sourceEventSeq: 1, + usage: { + inputTokens: 0, + outputTokens: 0, + source: "prompt_response" as const, + usageContract: "openai_total_with_cached_breakdown" as const, + }, + }; + + await recordRuntimeUsageEvent(database, input); + expect(await database.prepare("SELECT COUNT(*) AS count FROM usage_event").first()).toEqual({ + count: 0, + }); + + await recordRuntimeUsageEvent(database, { + ...input, + usage: { ...input.usage, inputTokens: 10, outputTokens: 5 }, + }); + await recordRuntimeUsageEvent(database, { ...input, sourceEventSeq: 2 }); + + expect( + await database + .prepare( + `SELECT input_tokens, output_tokens, source_event_seq, total_cost_usd_micros + FROM usage_event`, + ) + .first(), + ).toEqual({ + input_tokens: 0, + output_tokens: 0, + source_event_seq: 2, + total_cost_usd_micros: 0, + }); + }); }); diff --git a/apps/api/tests/cost-usage-idempotency.test.ts b/apps/api/tests/cost-usage-idempotency.test.ts index f797e157..ea92c77f 100644 --- a/apps/api/tests/cost-usage-idempotency.test.ts +++ b/apps/api/tests/cost-usage-idempotency.test.ts @@ -47,12 +47,30 @@ function createUsageDatabase(): SqliteD1Database { session_run_id text, source text NOT NULL, source_event_id text NOT NULL, + source_event_seq integer DEFAULT 0 NOT NULL, total_cost_usd_micros integer NOT NULL, usage_contract text NOT NULL ); CREATE UNIQUE INDEX usage_event_source_event_idx ON usage_event (source, source_event_id); + CREATE TABLE session_model_call ( + call_key text NOT NULL, + driver_instance_id text, + native_call_id text, + session_id text NOT NULL, + session_run_id text NOT NULL, + source_event_seq integer DEFAULT 0 NOT NULL + ); + + CREATE TABLE session_event ( + content_text text NOT NULL, + event_type text NOT NULL, + run_id text, + seq integer NOT NULL, + session_id text NOT NULL + ); + CREATE TABLE usage_daily_rollup ( organization_id text NOT NULL, app_id text NOT NULL, @@ -131,6 +149,40 @@ function createUsageEventInput(): RecordRuntimeUsageEventInput { }; } +async function seedDurableUsageAuthority( + database: SqliteD1Database, + input: { eventSeq: number; nativeCallId?: string | null }, +): Promise { + const nativeCallId = input.nativeCallId === undefined ? "native-call-1" : input.nativeCallId; + const callKey = nativeCallId === null ? "run_usage" : `model_call:${nativeCallId}`; + + await database + .prepare( + `INSERT INTO session_model_call ( + call_key, + driver_instance_id, + native_call_id, + session_id, + session_run_id, + source_event_seq + ) VALUES (?, ?, ?, ?, ?, ?)`, + ) + .bind(callKey, DRIVER_INSTANCE_ID, nativeCallId, SESSION_ID, SESSION_RUN_ID, input.eventSeq) + .run(); + await database + .prepare( + `INSERT INTO session_event (content_text, event_type, run_id, seq, session_id) + VALUES (?, 'usage.updated', ?, ?, ?)`, + ) + .bind( + JSON.stringify(nativeCallId === null ? {} : { callId: nativeCallId }), + SESSION_RUN_ID, + input.eventSeq, + SESSION_ID, + ) + .run(); +} + async function readRollupTotals( database: SqliteD1Database, ): Promise<{ requestCount: number; totalCostUsdMicros: number }> { @@ -151,6 +203,7 @@ describe("runtime usage idempotency across rollup", () => { const database = createUsageDatabase(); const env = { DB: database } as unknown as ApiBindings; + await seedDurableUsageAuthority(database, { eventSeq: 0 }); await recordRuntimeUsageEvent(database, createUsageEventInput()); await runUsageDailyRollup(env, ROLLUP_TIME); @@ -182,6 +235,211 @@ describe("runtime usage idempotency across rollup", () => { expect(afterSecondRollup).toEqual({ requestCount: 1, totalCostUsdMicros: 5_000_000 }); }); + test("does not strand billable usage behind a newer non-recordable receipt", async () => { + const database = createUsageDatabase(); + const env = { DB: database } as unknown as ApiBindings; + const input = { ...createUsageEventInput(), sourceEventSeq: 1 }; + + await seedDurableUsageAuthority(database, { eventSeq: 1 }); + await recordRuntimeUsageEvent(database, input); + await database + .prepare( + `INSERT INTO session_event (content_text, event_type, run_id, seq, session_id) + VALUES (?, 'usage.updated', ?, 2, ?)`, + ) + .bind( + JSON.stringify({ callId: "native-call-1", source: "session_update", totalTokens: 150 }), + SESSION_RUN_ID, + SESSION_ID, + ) + .run(); + await database.prepare("UPDATE session_model_call SET source_event_seq = 2").run(); + await recordRuntimeUsageEvent(database, { + ...input, + sourceEventSeq: 2, + usage: { + callId: "native-call-1", + source: "session_update", + totalTokens: 150, + }, + }); + + await runUsageDailyRollup(env, ROLLUP_TIME); + + expect(await readRollupTotals(database)).toEqual({ + requestCount: 1, + totalCostUsdMicros: 5_000_000, + }); + expect( + await database + .prepare("SELECT COUNT(*) AS count FROM usage_event") + .first<{ count: number }>(), + ).toEqual({ count: 0 }); + expect( + await database + .prepare("SELECT COUNT(*) AS count FROM usage_event_rollup_receipt") + .first<{ count: number }>(), + ).toEqual({ count: 1 }); + + await runUsageDailyRollup(env, ROLLUP_TIME); + expect(await readRollupTotals(database)).toEqual({ + requestCount: 1, + totalCostUsdMicros: 5_000_000, + }); + }); + + test("rolls up only the latest recordable usage once", async () => { + const database = createUsageDatabase(); + const env = { DB: database } as unknown as ApiBindings; + const input = { ...createUsageEventInput(), sourceEventSeq: 1 }; + + await seedDurableUsageAuthority(database, { eventSeq: 1 }); + await recordRuntimeUsageEvent(database, input); + await database + .prepare( + `INSERT INTO session_event (content_text, event_type, run_id, seq, session_id) + VALUES (?, 'usage.updated', ?, 2, ?)`, + ) + .bind(JSON.stringify({ callId: "native-call-1" }), SESSION_RUN_ID, SESSION_ID) + .run(); + await database.prepare("UPDATE session_model_call SET source_event_seq = 2").run(); + + await recordRuntimeUsageEvent(database, { + ...input, + sourceEventSeq: 2, + usage: { ...input.usage, costAmount: 7 }, + }); + + expect( + await database + .prepare("SELECT COUNT(*) AS count, source_event_seq FROM usage_event") + .first<{ count: number; source_event_seq: number }>(), + ).toEqual({ count: 1, source_event_seq: 2 }); + + await runUsageDailyRollup(env, ROLLUP_TIME); + + expect(await readRollupTotals(database)).toEqual({ + requestCount: 1, + totalCostUsdMicros: 7_000_000, + }); + expect( + await database + .prepare("SELECT COUNT(*) AS count FROM usage_event") + .first<{ count: number }>(), + ).toEqual({ count: 0 }); + + await runUsageDailyRollup(env, ROLLUP_TIME); + expect(await readRollupTotals(database)).toEqual({ + requestCount: 1, + totalCostUsdMicros: 7_000_000, + }); + expect( + await database + .prepare("SELECT COUNT(*) AS count FROM usage_event_rollup_receipt") + .first<{ count: number }>(), + ).toEqual({ count: 1 }); + }); + + test("reprices the merged token snapshot before rolling up a partial update", async () => { + const database = createUsageDatabase(); + const env = { DB: database } as unknown as ApiBindings; + const input = { + ...createUsageEventInput(), + run: { + ...createUsageEventInput().run, + model: "gpt-5.4", + provider: "openai", + }, + sourceEventSeq: 1, + usage: { + callId: "native-call-1", + inputTokens: 300_000, + outputTokens: 5, + source: "prompt_response" as const, + usageContract: "openai_total_with_cached_breakdown" as const, + }, + }; + + await seedDurableUsageAuthority(database, { eventSeq: 1 }); + await recordRuntimeUsageEvent(database, input); + await database.prepare("UPDATE session_model_call SET source_event_seq = 2").run(); + const partialInput = { + ...input, + sourceEventSeq: 2, + usage: { + callId: "native-call-1", + outputTokens: 7, + source: "prompt_response", + usageContract: "openai_total_with_cached_breakdown", + }, + }; + + await recordRuntimeUsageEvent(database, partialInput); + await recordRuntimeUsageEvent(database, partialInput); + + expect( + await database + .prepare( + `SELECT input_tokens, output_tokens, price_snapshot_json, + source_event_seq, total_cost_usd_micros + FROM usage_event`, + ) + .first(), + ).toEqual({ + input_tokens: 300_000, + output_tokens: 7, + price_snapshot_json: JSON.stringify({ + billableInputTokens: 300_000, + cacheReadUsdPerMillion: 0.5, + cacheWriteUsdPerMillion: 6.25, + inputUsdPerMillion: 5, + longContextApplied: true, + model: "gpt-5.4", + outputUsdPerMillion: 22.5, + provider: "openai", + source: "mosoo_seed_2026_07_10", + }), + source_event_seq: 2, + total_cost_usd_micros: 1_500_158, + }); + + await runUsageDailyRollup(env, ROLLUP_TIME); + + expect( + await database + .prepare( + `SELECT input_tokens, output_tokens, request_count, total_cost_usd_micros + FROM usage_daily_rollup`, + ) + .first(), + ).toEqual({ + input_tokens: 300_000, + output_tokens: 7, + request_count: 1, + total_cost_usd_micros: 1_500_158, + }); + }); + + test("matches call-less usage receipts through the durable run_usage identity", async () => { + const database = createUsageDatabase(); + const env = { DB: database } as unknown as ApiBindings; + + await seedDurableUsageAuthority(database, { eventSeq: 3, nativeCallId: null }); + await recordRuntimeUsageEvent(database, { + ...createUsageEventInput(), + callKey: "run_usage", + nativeCallId: null, + sourceEventSeq: 3, + usage: { ...createUsageEventInput().usage, callId: null }, + }); + await runUsageDailyRollup(env, ROLLUP_TIME); + + expect(await readRollupTotals(database)).toEqual({ + requestCount: 1, + totalCostUsdMicros: 5_000_000, + }); + }); + test("prunes rollup receipts past the daily rollup retention window", async () => { const database = createUsageDatabase(); const env = { DB: database } as unknown as ApiBindings; diff --git a/apps/api/tests/driver-command-terminal-ack.test.ts b/apps/api/tests/driver-command-terminal-ack.test.ts index 6bcf29cb..8c91050d 100644 --- a/apps/api/tests/driver-command-terminal-ack.test.ts +++ b/apps/api/tests/driver-command-terminal-ack.test.ts @@ -83,6 +83,7 @@ function createDatabase(): GatedTerminalLookupDatabase { acked_at integer, completed_at integer, delivery_connection_id text, + driver_generation integer, driver_instance_id text NOT NULL, error_json text, expires_at integer, @@ -96,13 +97,75 @@ function createDatabase(): GatedTerminalLookupDatabase { ); CREATE TABLE session_run ( + created_at integer DEFAULT 0 NOT NULL, + created_by_account_id text, driver_instance_id text, + error_code text, + error_details_json text, + error_message text, + error_retryable integer, id text PRIMARY KEY NOT NULL, - status text NOT NULL + runtime_id text, + session_id text, + status text NOT NULL, + trace_id text, + updated_at integer DEFAULT 0 NOT NULL + ); + + CREATE TABLE driver_instance ( + connection_id text, + generation integer DEFAULT 0 NOT NULL, + id text PRIMARY KEY NOT NULL, + sandbox_id text NOT NULL, + sandbox_session_id text NOT NULL, + status text NOT NULL, + status_changed_at integer DEFAULT 0 NOT NULL, + status_event text DEFAULT 'driver.provision' NOT NULL, + status_operation_id text, + status_seq integer DEFAULT 0 NOT NULL, + status_source text DEFAULT 'api' NOT NULL, + updated_at integer DEFAULT 0 NOT NULL + ); + + CREATE TABLE sandbox_session ( + origin_json text, + sandbox_id text, + status text, + session_id text PRIMARY KEY NOT NULL + ); + + CREATE TABLE session ( + agent_id text, + app_id text, + creator_account_id text, + id text PRIMARY KEY NOT NULL, + runtime_id text, + type text + ); + + CREATE TABLE agent ( + id text PRIMARY KEY NOT NULL, + owner_account_id text + ); + + CREATE TABLE sandbox ( + id text PRIMARY KEY NOT NULL, + inactive_deadline_at integer, + kind text, + subject_kind text, + updated_at integer DEFAULT 0 NOT NULL + ); + + CREATE TABLE session_event ( + event_type text NOT NULL, + id text PRIMARY KEY NOT NULL, + run_id text, + source_event_id text ); INSERT INTO driver_command ( delivery_connection_id, + driver_generation, driver_instance_id, id, issued_at, @@ -112,6 +175,7 @@ function createDatabase(): GatedTerminalLookupDatabase { status ) VALUES ( 'connection-1', + 0, '${DRIVER_INSTANCE_ID}', '${COMMAND_ID}', 0, @@ -120,30 +184,46 @@ function createDatabase(): GatedTerminalLookupDatabase { 1, 'delivered' ); + + INSERT INTO driver_instance (id, sandbox_id, sandbox_session_id, status) + VALUES ( + '${DRIVER_INSTANCE_ID}', + '01J00000000000000000000004', + '01J00000000000000000000005', + 'ready' + ); + + INSERT INTO session_run (driver_instance_id, id, status) + VALUES ('${DRIVER_INSTANCE_ID}', '${SESSION_RUN_ID}', 'completed'); + + INSERT INTO session_event (event_type, id, run_id, source_event_id) + VALUES ( + 'run.completed', + '01J00000000000000000000006', + '${SESSION_RUN_ID}', + 'session-run-terminal:${SESSION_RUN_ID}:run.completed' + ); `); return database; } describe("terminal runtime command acknowledgement", () => { - test("returns before linked run cleanup finishes", async () => { + test("returns after linked run cleanup finishes", async () => { const database = createDatabase(); - const backgroundTasks: Promise[] = []; const controller = new DriverInstanceRpcCommandController({ env: { DB: database } as ApiBindings, state: { + requireDriverGeneration: () => 0, requireDriverInstanceId: () => DRIVER_INSTANCE_ID, }, - waitUntil: (task: Promise) => { - backgroundTasks.push(task); - }, - withRuntimeLogContext: (fn: () => unknown) => fn(), } as never); const update = controller.handleCommandUpdate( { commandId: COMMAND_ID, driverInstanceId: DRIVER_INSTANCE_ID, + result: { requestId: "request-1" }, status: "completed", }, { @@ -158,15 +238,13 @@ describe("terminal runtime command acknowledgement", () => { new Promise((resolve) => setTimeout(() => resolve(false), 20)), ]); - expect(backgroundTasks).toHaveLength(1); + expect(acknowledgedBeforeCleanup).toBeFalse(); database.cleanupGate.resolve(); const result = await update; - await Promise.all(backgroundTasks); - expect(acknowledgedBeforeCleanup).toBeTrue(); expect(result).toEqual({ ok: true }); await expect( - getRuntimeCommandRecord(database, DRIVER_INSTANCE_ID, COMMAND_ID), + getRuntimeCommandRecord(database, DRIVER_INSTANCE_ID, 0, COMMAND_ID), ).resolves.toMatchObject({ status: "completed" }); }); }); diff --git a/apps/api/tests/driver-event-batch-admission.test.ts b/apps/api/tests/driver-event-batch-admission.test.ts new file mode 100644 index 00000000..5077aa70 --- /dev/null +++ b/apps/api/tests/driver-event-batch-admission.test.ts @@ -0,0 +1,63 @@ +import { describe, expect, test } from "bun:test"; + +import type { DriverEventEnvelope } from "@mosoo/agent-driver/events"; +import { createPlatformId } from "@mosoo/id"; +import type { RuntimeEventId, SessionId, SessionRunId } from "@mosoo/id"; +import { createRuntimeEvent } from "@mosoo/runtime-events"; +import type { RuntimeEventKind } from "@mosoo/runtime-events"; + +import { assertDriverEventBatchTerminalOrder } from "../src/modules/runtime/infrastructure/driver-instance/rpc-event-ingestion-controller"; + +const SESSION_ID = "01J00000000000000000000001" as SessionId; +const RUN_ID = "01J00000000000000000000002" as SessionRunId; + +function driverEvent(kind: RuntimeEventKind, sourceEventId: string): DriverEventEnvelope { + const event = createRuntimeEvent({ + id: createPlatformId(), + kind, + occurredAt: "2026-08-29T00:00:00.000Z", + payload: + kind === "run.failed" + ? { + error: { code: "driver.failed", message: "Driver failed.", retryable: false }, + recoverable: false, + } + : kind === "message.completed" + ? { messageId: "message-1", role: "agent" } + : {}, + runId: RUN_ID, + sessionId: SESSION_ID, + sourceEventId, + }); + + return { event, eventId: sourceEventId, occurredAt: event.occurredAt }; +} + +describe("Driver event batch terminal admission", () => { + test("accepts one terminal event only when it is last", () => { + expect(() => + assertDriverEventBatchTerminalOrder([ + driverEvent("message.completed", "message-completed"), + driverEvent("run.completed", "run-completed"), + ]), + ).not.toThrow(); + }); + + test("rejects multiple terminal events before persistence", () => { + expect(() => + assertDriverEventBatchTerminalOrder([ + driverEvent("run.completed", "run-completed"), + driverEvent("run.failed", "run-failed"), + ]), + ).toThrow("multiple run terminal events"); + }); + + test("rejects any event after a run terminal", () => { + expect(() => + assertDriverEventBatchTerminalOrder([ + driverEvent("run.completed", "run-completed"), + driverEvent("message.completed", "late-message"), + ]), + ).toThrow("must be last"); + }); +}); diff --git a/apps/api/tests/driver-finalization-repair.test.ts b/apps/api/tests/driver-finalization-repair.test.ts index 85cf364e..24ab57a6 100644 --- a/apps/api/tests/driver-finalization-repair.test.ts +++ b/apps/api/tests/driver-finalization-repair.test.ts @@ -1,32 +1,58 @@ import { describe, expect, test } from "bun:test"; +import { createMcpExecuteFailedEventIdentity } from "@mosoo/agent-driver/events"; +import { + ExternalToolEffectSettlement, + MCP_EXTERNAL_TOOL_EFFECT_SETTLEMENT_MAX_UTF8_BYTES, + measureMcpExternalToolEffectSettlement, +} from "@mosoo/contracts/external-tool-effect"; +import { + RUNTIME_COMMAND_MAX_UTF8_BYTES, + RUNTIME_COMMAND_TERMINAL_PAYLOAD_MAX_UTF8_BYTES, + measureRuntimeCommandJson, +} from "@mosoo/contracts/runtime-command"; import type { RuntimeCommand } from "@mosoo/contracts/runtime-command"; -import type { DriverCommandId, DriverInstanceId, SessionRunId } from "@mosoo/id"; +import { DURABLE_RUN_ERROR_MAX_UTF8_BYTES } from "@mosoo/contracts/session-run"; +import type { RunError } from "@mosoo/contracts/session-run"; +import type { + DriverCommandId, + DriverInstanceId, + ExternalToolEffectId, + SessionRunId, +} from "@mosoo/id"; -import type { AgentDriverBackend } from "../../driver/src/core/agent-driver-backend"; -import { createAgentDriverContext } from "../../driver/src/core/agent-driver-backend"; -import { DriverCommandDispatcher } from "../../driver/src/core/driver-command-dispatcher"; -import { DriverPermissionBroker } from "../../driver/src/core/driver-permission-broker"; import type { DriverRuntimeIo } from "../../driver/src/core/driver-runtime-io"; import { DriverRuntimeStateMachine } from "../../driver/src/core/driver-runtime-state"; import type { AgentDriverMcpExecution } from "../../driver/src/host-ports"; -import { createBufferedSinkLogger } from "../../driver/src/observability"; -import { createDriverStartInputFromBootPayload } from "../../driver/src/protocol/start"; +import { parseRunId } from "../../driver/src/protocol/id"; import type { RuntimeCommand as DriverRuntimeCommand } from "../../driver/src/runtime-command"; -import { driverBootPayload } from "../../driver/tests/driver-boot-payload-fixture"; +import { promiseWithTimeout } from "../../driver/src/utils/async"; +import { + createBackend, + createDispatcher, + FakeDriverRuntimeIo, +} from "../../driver/tests/driver-runtime-boundary-fixtures"; import { recordCanonicalSessionRunFailure } from "../src/modules/runtime/application/session-runs/session-run-terminal-failure.service"; +import { + createFailedSessionRunRuntimeEvent, + createSessionRunUpdatedEvent, +} from "../src/modules/runtime/application/session-runs/session-run-view-events.service"; +import { createSessionRunTerminalSourceId } from "../src/modules/runtime/domain/session-run-terminal-event-id"; +import { commitTerminalRunProjection } from "../src/modules/runtime/infrastructure/driver-instance/completed-run-commit.repository"; import { cleanupDriverInstances } from "../src/modules/runtime/infrastructure/driver-instance/maintenance"; -import { repairFinalizedTerminalDriverRunState } from "../src/modules/runtime/infrastructure/driver-instance/terminal-run-release"; +import { repairFinalizedTerminalDriverRunState as repairFinalizedTerminalDriverRunStateForGeneration } from "../src/modules/runtime/infrastructure/driver-instance/terminal-run-release"; import { - claimExternalToolEffect, - completeExternalToolEffect, - getExternalToolEffectForCommand, - markExternalToolEffectUnknown, + claimExternalToolEffect as claimExternalToolEffectRecord, + getExternalToolEffectForCommand as getExternalToolEffectForCommandRecord, + observeExternalToolEffect as observeExternalToolEffectRecord, + settleExternalToolEffect as settleExternalToolEffectRecord, } from "../src/modules/runtime/infrastructure/session-runs/external-tool-effect-store.repository"; import { - createRuntimeCommandRecord, - getRuntimeCommandRecord, + createRuntimeCommandRecord as createRuntimeCommandRecordForGeneration, + getRuntimeCommandRecord as getRuntimeCommandRecordForGeneration, + updateRuntimeCommandRecord as updateRuntimeCommandRecordForGeneration, } from "../src/modules/runtime/infrastructure/session-runs/runtime-command-store.repository"; +import { getSessionRunSummary } from "../src/modules/runtime/infrastructure/session-runs/session-run-store.repository"; import type { ApiBindings } from "../src/platform/cloudflare/worker-types"; import { createPublicHttpContractDatabase, @@ -37,8 +63,12 @@ import { import type { SqliteD1Database } from "./helpers/public-api-http-test-fixture"; const FINALIZE_RUN_ID = "01J0000000000000000000000T" as SessionRunId; +const DRIVER_GENERATION = 0; const FINALIZE_COMMAND_ID = "01J0000000000000000000000V" as DriverCommandId; const MCP_COMMAND_ID = "01J0000000000000000000000X" as DriverCommandId; +const MCP_CLAIM_TOKEN = "00000000-0000-4000-8000-000000000001"; +const MCP_OTHER_CLAIM_TOKEN = "00000000-0000-4000-8000-000000000002"; +const DISPATCHER_TEST_PHASE_TIMEOUT_MS = 1_500; const FINALIZE_CLOUDFLARE_SESSION_ID = "01J0000000000000000000000W"; const TURN_INTERRUPTED_MESSAGE = "This turn was interrupted before it completed. Please resend your last request."; @@ -48,6 +78,168 @@ const PROVISION_ERROR = { message: "Driver command dispatch failed.", retryable: false, } as const; +const MCP_SUCCESS_RESULT = { + outputText: "created issue A-1", + requestId: "request-01J0000000000000000000000X", + serverId: "01J0000000000000000000000Y", + toolName: "createIssue", +} as const; + +const repairFinalizedTerminalDriverRunState = ( + bindings: ApiBindings, + input: Omit< + Parameters[1], + "driverGeneration" | "sessionRunId" + >, +) => + bindings.DB.prepare("UPDATE driver_instance SET status = ? WHERE id = ?") + .bind(input.status, PUBLIC_API_TEST_IDS.driverOwner) + .run() + .then(() => + repairFinalizedTerminalDriverRunStateForGeneration(bindings, { + ...input, + driverGeneration: DRIVER_GENERATION, + sessionRunId: FINALIZE_RUN_ID, + }), + ); + +const createRuntimeCommandRecord = ( + database: D1Database, + input: Omit[1], "driverGeneration">, +) => + createRuntimeCommandRecordForGeneration(database, { + ...input, + driverGeneration: DRIVER_GENERATION, + }); + +const getRuntimeCommandRecord = ( + database: D1Database, + driverInstanceId: DriverInstanceId, + commandId: DriverCommandId, +) => getRuntimeCommandRecordForGeneration(database, driverInstanceId, DRIVER_GENERATION, commandId); + +const updateRuntimeCommandRecord = ( + database: D1Database, + input: Omit[1], "driverGeneration">, +) => + updateRuntimeCommandRecordForGeneration(database, { + ...input, + driverGeneration: DRIVER_GENERATION, + }); + +type EffectLookupInput unknown> = Omit< + Parameters[1], + "driverGeneration" +>; + +const claimExternalToolEffect = ( + database: D1Database, + input: EffectLookupInput, +) => claimExternalToolEffectRecord(database, { ...input, driverGeneration: DRIVER_GENERATION }); + +const getExternalToolEffectForCommand = ( + database: D1Database, + input: EffectLookupInput, +) => + getExternalToolEffectForCommandRecord(database, { + ...input, + driverGeneration: DRIVER_GENERATION, + }); + +const observeExternalToolEffect = ( + database: D1Database, + input: EffectLookupInput, +) => observeExternalToolEffectRecord(database, { ...input, driverGeneration: DRIVER_GENERATION }); + +const settleExternalToolEffect = ( + database: D1Database, + input: EffectLookupInput, +) => settleExternalToolEffectRecord(database, { ...input, driverGeneration: DRIVER_GENERATION }); + +function succeededSettlementAtSize(byteLength: number) { + const empty = { + kind: "succeeded", + result: { ...MCP_SUCCESS_RESULT, outputText: "" }, + } as const; + const outputBytes = byteLength - measureMcpExternalToolEffectSettlement(empty); + + if (outputBytes < 0) { + throw new Error("Requested settlement size is smaller than its fixed fields."); + } + + return { + kind: "succeeded" as const, + result: { ...MCP_SUCCESS_RESULT, outputText: "x".repeat(outputBytes) }, + }; +} + +function textFieldAtJsonSize(byteLength: number, create: (text: string) => Value): Value { + const empty = create(""); + const value = create("x".repeat(byteLength - measureRuntimeCommandJson(empty))); + + expect(measureRuntimeCommandJson(value)).toBe(byteLength); + return value; +} + +function mcpCommandAtSize(byteLength: number) { + return textFieldAtJsonSize(byteLength, (argumentsJson) => ({ + ...mcpExecuteCommand(MCP_COMMAND_ID), + argumentsJson, + })); +} + +function inputStartCommandAtSize(byteLength: number) { + return textFieldAtJsonSize(byteLength, (text) => ({ + ...inputStartCommand(MCP_COMMAND_ID), + input: { text }, + })); +} + +async function commitInputCommandTerminalAuthority( + database: D1Database, + input: { error: RunError; status: "failed" } | { error: null; status: "completed" }, +): Promise { + const current = await getSessionRunSummary(database, FINALIZE_RUN_ID); + if (current === null) { + throw new Error("Missing Session Run fixture."); + } + + const timestampMs = Date.now(); + const timestamp = new Date(timestampMs).toISOString(); + const run = { + ...current, + completedAt: timestamp, + error: input.error, + startedAt: current.startedAt ?? timestamp, + status: input.status, + updatedAt: timestamp, + }; + const kind = input.status === "completed" ? "run.completed" : "run.failed"; + const sourceEventId = createSessionRunTerminalSourceId(FINALIZE_RUN_ID, kind); + const event = + input.status === "completed" + ? createSessionRunUpdatedEvent(run, PUBLIC_API_TEST_IDS.ownerSession, "IDLE", sourceEventId) + : createFailedSessionRunRuntimeEvent({ + run, + runError: input.error, + sessionId: PUBLIC_API_TEST_IDS.ownerSession, + sourceEventId, + }); + + const outcome = await commitTerminalRunProjection(database, { + assistantMessage: null, + error: input.error, + runId: FINALIZE_RUN_ID, + sessionId: PUBLIC_API_TEST_IDS.ownerSession, + source: "api", + targetStatus: input.status, + terminalEvent: { event, occurredAt: timestampMs, sourceEventId }, + timestampMs, + }); + if (outcome.kind === "stale") { + throw new Error("Session Run terminal authority lost its test setup race."); + } +} interface TerminalEventRow { content_text: string; @@ -72,15 +264,14 @@ interface DriverInterruptionBenchmarkMetrics { viewerTerminalRate: string; } -class PersistentEffectDriverIo implements DriverRuntimeIo { +class PersistentEffectDriverIo extends FakeDriverRuntimeIo { readonly completedReceipt = Promise.withResolvers(); readonly effectPersisted = Promise.withResolvers(); - readonly updates: Parameters[0][] = []; - readonly #commands: readonly DriverRuntimeCommand[]; + readonly lostCompletedReceipt = Promise.withResolvers(); readonly #database: SqliteD1Database; readonly #driverInstanceId: DriverInstanceId; readonly #loseCompletedReceipt: boolean; - #commandIndex = 0; + #terminalReceiptObserved = false; constructor(input: { commands: readonly DriverRuntimeCommand[]; @@ -88,146 +279,87 @@ class PersistentEffectDriverIo implements DriverRuntimeIo { driverInstanceId: DriverInstanceId; loseCompletedReceipt?: boolean; }) { - this.#commands = input.commands; + super(input.commands, parseRunId(FINALIZE_RUN_ID)); this.#database = input.database; this.#driverInstanceId = input.driverInstanceId; this.#loseCompletedReceipt = input.loseCompletedReceipt ?? false; } - beginRun(): void {} - - async claimExternalToolEffect( + override async claimExternalToolEffect( input: Parameters[0], _signal: AbortSignal, ): ReturnType { return claimExternalToolEffect(this.#database, { + claimToken: input.claimToken, commandId: input.commandId as DriverCommandId, driverInstanceId: this.#driverInstanceId, }); } - async commandUpdate( + override async commandUpdate( input: Parameters[0], _signal: AbortSignal, ): Promise { this.updates.push(input); - if (this.#loseCompletedReceipt && input.status === "completed") { - throw new Error("injected terminal receipt loss after durable effect completion"); + if (input.status !== "completed") { + return; } - if (input.status === "completed") { - this.completedReceipt.resolve(); + this.#terminalReceiptObserved = true; + if (this.#loseCompletedReceipt) { + this.lostCompletedReceipt.resolve(); + throw new Error("injected terminal receipt loss after durable effect completion"); } + + this.completedReceipt.resolve(); } - async completeExternalToolEffect( - input: Parameters[0], + terminalReceiptObserved(): boolean { + return this.#terminalReceiptObserved; + } + + override async observeExternalToolEffect( + input: Parameters[0], _signal: AbortSignal, - ): Promise { - await completeExternalToolEffect(this.#database, { + ): ReturnType { + return observeExternalToolEffect(this.#database, { commandId: input.commandId as DriverCommandId, driverInstanceId: this.#driverInstanceId, - ...(input.providerReceiptJson === undefined - ? {} - : { providerReceiptJson: input.providerReceiptJson }), - result: input.result, }); - this.effectPersisted.resolve(); } - async completeRun(): Promise {} - - endRun(): void {} - - async failRun(): Promise {} - - async heartbeat(): ReturnType { - return { heartbeatCount: 1, ok: true }; - } - - isDrained(): boolean { - return this.#commandIndex >= this.#commands.length; - } - - async markExternalToolEffectUnknown( - input: Parameters[0], + override async settleExternalToolEffect( + input: Parameters[0], _signal: AbortSignal, - ): Promise { - await markExternalToolEffectUnknown(this.#database, { + ): ReturnType { + const state = await settleExternalToolEffect(this.#database, { + claimToken: input.claimToken, commandId: input.commandId as DriverCommandId, driverInstanceId: this.#driverInstanceId, + effectId: input.effectId as ExternalToolEffectId, + settlement: input.settlement, }); - } - - async nextCommand(_signal: AbortSignal): Promise { - const command = this.#commands[this.#commandIndex] ?? null; - - if (command !== null) { - this.#commandIndex += 1; + if (state.kind === "succeeded") { + this.effectPersisted.resolve(); } - - return command; - } - - async pushEvents( - input: Parameters[0], - ): ReturnType { - return { - accepted: input.events.map((event, index) => ({ seq: index + 1, type: event.kind })), - }; + return state; } } function createPersistentEffectDispatcher(input: { io: PersistentEffectDriverIo; mcpExecute: AgentDriverMcpExecution["execute"]; -}): { dispatcher: DriverCommandDispatcher; logger: ReturnType } { - const logger = createBufferedSinkLogger({ - level: "debug", - service: "persistent-effect-dispatcher-test", - sink: async () => {}, - }); - const backend: AgentDriverBackend = { - cancelActiveTurn: async () => {}, - handleInput: async () => {}, - runtime: "openai-runtime", - start: async () => {}, - stop: async () => {}, - }; - const payload = createDriverStartInputFromBootPayload(driverBootPayload); - const dispatcher = new DriverCommandDispatcher({ - backend, - driverInstanceId: PUBLIC_API_TEST_IDS.driverOwner, - isShuttingDown: () => input.io.isDrained(), - permissionRequests: new DriverPermissionBroker(() => logger), - rememberRunFailure: () => {}, - runtimeContextFactory: (socket, runtimeLogger) => - createAgentDriverContext({ - eventSink: socket, - logger: runtimeLogger, - payload, - permission: { request: async () => "reject_once" }, - ports: { - commandSource: { nextCommand: (signal) => socket.nextCommand(signal) }, - mcp: { - prepare: async () => ({ - [Symbol.asyncDispose]: async () => {}, - execute: input.mcpExecute, - }), - }, - }, - }), +}) { + return createDispatcher({ + backend: createBackend(), + isShuttingDown: () => input.io.terminalReceiptObserved(), + mcpExecute: (_command, effect) => input.mcpExecute(effect), runtimeState: new DriverRuntimeStateMachine("ready"), - sandboxId: payload.sandboxId, - shutdown: async () => {}, - shutdownSignal: new AbortController().signal, }); - - return { dispatcher, logger }; } -function inputStartCommand(id: DriverCommandId): RuntimeCommand { +function inputStartCommand(id: DriverCommandId): Extract { return { commandId: id, input: { @@ -239,12 +371,13 @@ function inputStartCommand(id: DriverCommandId): RuntimeCommand { }; } -function mcpExecuteCommand(id: DriverCommandId): RuntimeCommand { +function mcpExecuteCommand(id: DriverCommandId): Extract { return { argumentsJson: '{"title":"do not duplicate"}', commandId: id, kind: "mcp.execute", requestId: `request-${id}`, + runId: FINALIZE_RUN_ID, serverId: "01J0000000000000000000000Y", toolCallId: `tool-${id}`, toolName: "createIssue", @@ -253,56 +386,17 @@ function mcpExecuteCommand(id: DriverCommandId): RuntimeCommand { async function insertFinalizedDriverLeaseFixture(database: SqliteD1Database): Promise { await insertOwnerSession(database); - database.execute(` - CREATE TABLE IF NOT EXISTS driver_command ( - acked_at integer, - completed_at integer, - delivery_connection_id text, - driver_instance_id text NOT NULL, - error_json text, - expires_at integer, - id text PRIMARY KEY NOT NULL, - issued_at integer NOT NULL, - kind text NOT NULL, - payload_json text NOT NULL, - result_json text, - seq integer NOT NULL, - status text NOT NULL - ); - - CREATE TABLE IF NOT EXISTS external_tool_effect ( - attempt_count integer NOT NULL, - command_id text NOT NULL UNIQUE, - created_at integer NOT NULL, - driver_instance_id text NOT NULL, - id text PRIMARY KEY NOT NULL, - idempotency_key text NOT NULL UNIQUE, - provider_receipt_json text, - result_json text, - server_id text NOT NULL, - session_run_id text NOT NULL, - status text NOT NULL, - tool_name text NOT NULL, - updated_at integer NOT NULL - ); - - CREATE TABLE IF NOT EXISTS external_tool_effect_attempt ( - attempt integer NOT NULL, - completed_at integer, - created_at integer NOT NULL, - effect_id text NOT NULL, - provider_receipt_json text, - result_json text, - status text NOT NULL, - PRIMARY KEY (effect_id, attempt) - ); - `); await database .prepare( ` INSERT INTO sandbox ( id, + agent_id, + app_id, + owner_account_id, + incarnation, kind, + network_constraints_hash, subject_kind, subject_id, status, @@ -311,12 +405,17 @@ async function insertFinalizedDriverLeaseFixture(database: SqliteD1Database): Pr created_at, updated_at ) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) `, ) .bind( PUBLIC_API_TEST_IDS.sandbox, + PUBLIC_API_TEST_IDS.agent, + PUBLIC_API_TEST_IDS.app, + PUBLIC_API_TEST_IDS.ownerAccount, + 1, "pet", + "0".repeat(64), "agent", PUBLIC_API_TEST_IDS.agent, "active", @@ -335,11 +434,12 @@ async function insertFinalizedDriverLeaseFixture(database: SqliteD1Database): Pr cwd, origin_json, sandbox_id, + sandbox_incarnation, session_id, status, updated_at ) - VALUES (?, ?, ?, ?, ?, ?, ?, ?) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) `, ) .bind( @@ -353,6 +453,7 @@ async function insertFinalizedDriverLeaseFixture(database: SqliteD1Database): Pr type: "agent", }), PUBLIC_API_TEST_IDS.sandbox, + 1, PUBLIC_API_TEST_IDS.ownerSession, "active", 1, @@ -373,11 +474,12 @@ async function insertFinalizedDriverLeaseFixture(database: SqliteD1Database): Pr protocol_version, runtime, sandbox_id, + sandbox_incarnation, sandbox_session_id, status, updated_at ) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) `, ) .bind( @@ -392,8 +494,9 @@ async function insertFinalizedDriverLeaseFixture(database: SqliteD1Database): Pr 1, "openai-runtime", PUBLIC_API_TEST_IDS.sandbox, + 1, PUBLIC_API_TEST_IDS.ownerSession, - "stopped", + "ready", 1, ) .run(); @@ -446,6 +549,58 @@ async function insertFinalizedDriverLeaseFixture(database: SqliteD1Database): Pr .run(); } +async function setDriverStatus( + database: SqliteD1Database, + status: "ready" | "failed" | "stopped", +): Promise { + await database + .prepare("UPDATE driver_instance SET status = ?, updated_at = ? WHERE id = ?") + .bind(status, Date.now(), PUBLIC_API_TEST_IDS.driverOwner) + .run(); +} + +async function createTerminalDriverFixture() { + const database = await createPublicHttpContractDatabase(); + await insertFinalizedDriverLeaseFixture(database); + + return { + bindings: createPublicHttpTestBindings(database) as ApiBindings, + database, + driverInstanceId: PUBLIC_API_TEST_IDS.driverOwner as DriverInstanceId, + }; +} + +async function createReadyMcpEffectFixture( + command: RuntimeCommand = mcpExecuteCommand(MCP_COMMAND_ID), +) { + const fixture = await createTerminalDriverFixture(); + await setDriverStatus(fixture.database, "ready"); + await createRuntimeCommandRecord(fixture.database, { + command, + driverInstanceId: fixture.driverInstanceId, + status: "accepted", + }); + + return fixture; +} + +async function createClaimedMcpEffectFixture( + command: Extract = mcpExecuteCommand(MCP_COMMAND_ID), +) { + const fixture = await createReadyMcpEffectFixture(command); + const claim = await claimExternalToolEffect(fixture.database, { + claimToken: MCP_CLAIM_TOKEN, + commandId: command.commandId as DriverCommandId, + driverInstanceId: fixture.driverInstanceId, + }); + + if (claim.kind !== "claimed") { + throw new Error("Expected the test effect to be claimed."); + } + + return { ...fixture, claim }; +} + async function readTerminalEvents(database: SqliteD1Database): Promise { return database .prepare( @@ -521,13 +676,11 @@ function createBeforeBenchmarkSample() { } async function createAfterBenchmarkSample() { - const database = await createPublicHttpContractDatabase(); - await insertFinalizedDriverLeaseFixture(database); - const bindings = createPublicHttpTestBindings(database) as ApiBindings; + const { bindings, database, driverInstanceId } = await createTerminalDriverFixture(); const startedAt = performance.now(); await repairFinalizedTerminalDriverRunState(bindings, { - driverInstanceId: PUBLIC_API_TEST_IDS.driverOwner as DriverInstanceId, + driverInstanceId, status: "stopped", }); const visibleInterruptedMs = performance.now() - startedAt; @@ -549,22 +702,20 @@ async function createAfterBenchmarkSample() { describe("driver finalization repair", () => { test("fails active run lease, accepted commands, and publishes a replayable terminal event", async () => { - const database = await createPublicHttpContractDatabase(); - await insertFinalizedDriverLeaseFixture(database); - const bindings = createPublicHttpTestBindings(database) as ApiBindings; + const { bindings, database, driverInstanceId } = await createTerminalDriverFixture(); await createRuntimeCommandRecord(database, { command: inputStartCommand(FINALIZE_COMMAND_ID), - driverInstanceId: PUBLIC_API_TEST_IDS.driverOwner as DriverInstanceId, + driverInstanceId, status: "accepted", }); await repairFinalizedTerminalDriverRunState(bindings, { - driverInstanceId: PUBLIC_API_TEST_IDS.driverOwner as DriverInstanceId, + driverInstanceId, status: "stopped", }); await repairFinalizedTerminalDriverRunState(bindings, { - driverInstanceId: PUBLIC_API_TEST_IDS.driverOwner as DriverInstanceId, + driverInstanceId, status: "stopped", }); @@ -578,11 +729,7 @@ describe("driver finalization repair", () => { ) .bind(PUBLIC_API_TEST_IDS.driverOwner) .first<{ id: string }>(); - const command = await getRuntimeCommandRecord( - database, - PUBLIC_API_TEST_IDS.driverOwner as DriverInstanceId, - FINALIZE_COMMAND_ID, - ); + const command = await getRuntimeCommandRecord(database, driverInstanceId, FINALIZE_COMMAND_ID); const terminalEvents = await readTerminalEvents(database); expect(run).toEqual({ @@ -591,7 +738,7 @@ describe("driver finalization repair", () => { }); expect(activeLease).toBeNull(); expect(command?.status).toBe("failed"); - expect(command?.error?.code).toBe("driver.command_driver_terminal"); + expect(command?.error?.code).toBe("runtime.turn_interrupted"); expect(terminalEvents).toEqual([ { content_text: TURN_INTERRUPTED_MESSAGE, @@ -601,7 +748,7 @@ describe("driver finalization repair", () => { process_type: "run.failed", run_id: FINALIZE_RUN_ID, seq: 1, - source: "api", + source: "driver", source_event_id: `session-run-terminal:${FINALIZE_RUN_ID}:run.failed`, trace_id: "trace-finalize", visibility: "all_consumers", @@ -610,12 +757,10 @@ describe("driver finalization repair", () => { }); test("deduplicates dispatch repair after driver finalization", async () => { - const database = await createPublicHttpContractDatabase(); - await insertFinalizedDriverLeaseFixture(database); - const bindings = createPublicHttpTestBindings(database) as ApiBindings; + const { bindings, database, driverInstanceId } = await createTerminalDriverFixture(); await repairFinalizedTerminalDriverRunState(bindings, { - driverInstanceId: PUBLIC_API_TEST_IDS.driverOwner as DriverInstanceId, + driverInstanceId, status: "failed", }); await recordCanonicalSessionRunFailure(bindings, { @@ -635,8 +780,7 @@ describe("driver finalization repair", () => { }); test("does not queue an MCP command when its durable effect intent cannot be prepared", async () => { - const database = await createPublicHttpContractDatabase(); - await insertFinalizedDriverLeaseFixture(database); + const { database, driverInstanceId } = await createTerminalDriverFixture(); await database .prepare("UPDATE session_run SET status = 'failed' WHERE id = ?") .bind(FINALIZE_RUN_ID) @@ -645,34 +789,62 @@ describe("driver finalization repair", () => { await expect( createRuntimeCommandRecord(database, { command: mcpExecuteCommand(MCP_COMMAND_ID), - driverInstanceId: PUBLIC_API_TEST_IDS.driverOwner as DriverInstanceId, + driverInstanceId, status: "accepted", }), - ).rejects.toThrow("MCP external tool effects require an active Session Run."); + ).rejects.toThrow("MCP external tool effects require the command's active Session Run."); - expect( - await getRuntimeCommandRecord( - database, - PUBLIC_API_TEST_IDS.driverOwner as DriverInstanceId, - MCP_COMMAND_ID, - ), - ).toBeNull(); + expect(await getRuntimeCommandRecord(database, driverInstanceId, MCP_COMMAND_ID)).toBeNull(); }); - test("persists the MCP effect intent and fences it as unknown after Driver loss", async () => { - const database = await createPublicHttpContractDatabase(); - await insertFinalizedDriverLeaseFixture(database); - const bindings = createPublicHttpTestBindings(database) as ApiBindings; - - await createRuntimeCommandRecord(database, { - command: mcpExecuteCommand(MCP_COMMAND_ID), - driverInstanceId: PUBLIC_API_TEST_IDS.driverOwner as DriverInstanceId, - status: "accepted", + for (const repair of ["finalizer", "maintenance"] as const) { + test(`${repair} reports an unclaimed MCP intent as safely retryable`, async () => { + const { bindings, database, driverInstanceId } = await createTerminalDriverFixture(); + await createRuntimeCommandRecord(database, { + command: mcpExecuteCommand(MCP_COMMAND_ID), + driverInstanceId, + status: "accepted", + }); + const effect = await getExternalToolEffectForCommand(database, { + commandId: MCP_COMMAND_ID, + driverInstanceId, + }); + + if (repair === "finalizer") { + await repairFinalizedTerminalDriverRunState(bindings, { + driverInstanceId, + status: "stopped", + }); + } else { + await database + .prepare("UPDATE driver_instance SET expires_at = ? WHERE id = ?") + .bind(Date.now() + 60_000, PUBLIC_API_TEST_IDS.driverOwner) + .run(); + await cleanupDriverInstances(bindings); + } + + expect( + await getRuntimeCommandRecord(database, driverInstanceId, MCP_COMMAND_ID), + ).toMatchObject({ + error: { + code: "driver.external_tool_effect_not_executed", + details: { + commandId: MCP_COMMAND_ID, + effectId: effect?.id, + }, + retryable: true, + }, + status: "failed", + }); }); + } + + test("persists the MCP effect intent and fences it as unknown after Driver loss", async () => { + const { bindings, database, driverInstanceId } = await createReadyMcpEffectFixture(); const intent = await getExternalToolEffectForCommand(database, { commandId: MCP_COMMAND_ID, - driverInstanceId: PUBLIC_API_TEST_IDS.driverOwner as DriverInstanceId, + driverInstanceId, }); expect(intent).toMatchObject({ attemptCount: 0, @@ -682,26 +854,30 @@ describe("driver finalization repair", () => { expect(intent?.idempotencyKey).toHaveLength(26); const claim = await claimExternalToolEffect(database, { + claimToken: MCP_CLAIM_TOKEN, commandId: MCP_COMMAND_ID, - driverInstanceId: PUBLIC_API_TEST_IDS.driverOwner as DriverInstanceId, + driverInstanceId, }); - expect(claim).toMatchObject({ attempt: 1, kind: "execute" }); + expect(claim).toMatchObject({ attempt: 1, kind: "claimed" }); + + await setDriverStatus(database, "failed"); await repairFinalizedTerminalDriverRunState(bindings, { - driverInstanceId: PUBLIC_API_TEST_IDS.driverOwner as DriverInstanceId, + driverInstanceId, status: "failed", }); await expect( claimExternalToolEffect(database, { + claimToken: MCP_OTHER_CLAIM_TOKEN, commandId: MCP_COMMAND_ID, - driverInstanceId: PUBLIC_API_TEST_IDS.driverOwner as DriverInstanceId, + driverInstanceId, }), ).resolves.toMatchObject({ kind: "unknown" }); expect( await getExternalToolEffectForCommand(database, { commandId: MCP_COMMAND_ID, - driverInstanceId: PUBLIC_API_TEST_IDS.driverOwner as DriverInstanceId, + driverInstanceId, }), ).toMatchObject({ attemptCount: 1, status: "unknown" }); expect( @@ -709,27 +885,496 @@ describe("driver finalization repair", () => { .prepare("SELECT completed_at, status FROM external_tool_effect_attempt") .first<{ completed_at: number; status: string }>(), ).toMatchObject({ status: "unknown" }); + const record = await getRuntimeCommandRecord(database, driverInstanceId, MCP_COMMAND_ID); + expect(record).toMatchObject({ + error: { + code: "driver.external_tool_effect_unknown", + details: { effectId: claim.effectId }, + }, + status: "failed", + }); + if (record?.status !== "failed") { + throw new Error("Expected repaired MCP failure."); + } + + const command = mcpExecuteCommand(MCP_COMMAND_ID); + const failedEvent = createMcpExecuteFailedEventIdentity({ + commandId: MCP_COMMAND_ID, + rawInput: command.argumentsJson, + rawOutput: record.error.message, + title: command.toolName, + toolCallId: command.toolCallId, + }); + expect( + await database + .prepare( + "SELECT source_event_id, tool_input_json, tool_output_text, tool_status FROM session_event WHERE mcp_command_id = ?", + ) + .bind(MCP_COMMAND_ID) + .first(), + ).toEqual({ + source_event_id: failedEvent.sourceEventId, + tool_input_json: failedEvent.payload.rawInput, + tool_output_text: failedEvent.payload.rawOutput, + tool_status: "failed", + }); }); - test("retains an unknown MCP effect after terminal Driver maintenance", async () => { - const database = await createPublicHttpContractDatabase(); - await insertFinalizedDriverLeaseFixture(database); - const bindings = createPublicHttpTestBindings(database) as ApiBindings; + test("recovers a lost claim response only for the same claim token", async () => { + const { database, driverInstanceId } = await createReadyMcpEffectFixture(); - await createRuntimeCommandRecord(database, { - command: mcpExecuteCommand(MCP_COMMAND_ID), - driverInstanceId: PUBLIC_API_TEST_IDS.driverOwner as DriverInstanceId, - status: "accepted", + await expect( + observeExternalToolEffect(database, { + commandId: MCP_COMMAND_ID, + driverInstanceId, + }), + ).resolves.toMatchObject({ kind: "intent" }); + + const first = await claimExternalToolEffect(database, { + claimToken: MCP_CLAIM_TOKEN, + commandId: MCP_COMMAND_ID, + driverInstanceId, }); - await claimExternalToolEffect(database, { + await expect( + claimExternalToolEffect(database, { + claimToken: MCP_CLAIM_TOKEN, + commandId: MCP_COMMAND_ID, + driverInstanceId, + }), + ).resolves.toEqual(first); + await expect( + observeExternalToolEffect(database, { + commandId: MCP_COMMAND_ID, + driverInstanceId, + }), + ).resolves.toEqual(first); + expect( + await database + .prepare("SELECT COUNT(*) AS count FROM external_tool_effect_attempt") + .first<{ count: number }>(), + ).toEqual({ count: 1 }); + + await expect( + claimExternalToolEffect(database, { + claimToken: MCP_OTHER_CLAIM_TOKEN, + commandId: MCP_COMMAND_ID, + driverInstanceId, + }), + ).resolves.toMatchObject({ effectId: first.effectId, kind: "unknown" }); + await expect( + updateRuntimeCommandRecord(database, { + commandId: MCP_COMMAND_ID, + driverInstanceId, + result: MCP_SUCCESS_RESULT, + status: "completed", + }), + ).resolves.toMatchObject({ kind: "rejected", reason: "illegal_transition" }); + expect( + await database + .prepare("SELECT claim_token, status FROM external_tool_effect_attempt") + .first<{ claim_token: string; status: string }>(), + ).toEqual({ claim_token: MCP_CLAIM_TOKEN, status: "unknown" }); + }); + + test("atomically fences both command-terminal and claim-first interleavings", async () => { + for (const first of ["command", "claim"] as const) { + const { bindings, database, driverInstanceId } = await createReadyMcpEffectFixture(); + + if (first === "claim") { + await expect( + claimExternalToolEffect(database, { + claimToken: MCP_CLAIM_TOKEN, + commandId: MCP_COMMAND_ID, + driverInstanceId, + }), + ).resolves.toMatchObject({ kind: "claimed" }); + } + + await setDriverStatus(database, "failed"); + await repairFinalizedTerminalDriverRunState(bindings, { + driverInstanceId, + status: "failed", + }); + + await expect( + observeExternalToolEffect(database, { + commandId: MCP_COMMAND_ID, + driverInstanceId, + }), + ).resolves.toMatchObject({ kind: first === "claim" ? "unknown" : "intent" }); + const replayClaim = claimExternalToolEffect(database, { + claimToken: MCP_OTHER_CLAIM_TOKEN, + commandId: MCP_COMMAND_ID, + driverInstanceId, + }); + if (first === "claim") { + await expect(replayClaim).resolves.toMatchObject({ kind: "unknown" }); + } else { + await expect(replayClaim).rejects.toThrow("did not reach a stable state"); + } + expect( + await getRuntimeCommandRecord(database, driverInstanceId, MCP_COMMAND_ID), + ).toMatchObject({ + error: { + code: + first === "claim" + ? "driver.external_tool_effect_unknown" + : "driver.external_tool_effect_not_executed", + }, + status: "failed", + }); + + const attemptCount = await database + .prepare("SELECT COUNT(*) AS count FROM external_tool_effect_attempt") + .first<{ count: number }>(); + expect(attemptCount).toEqual({ count: first === "claim" ? 1 : 0 }); + } + }); + + test("returns stored success when settlement acknowledgement is lost", async () => { + const { claim, database, driverInstanceId } = await createClaimedMcpEffectFixture(); + const settlement = { + claimToken: MCP_CLAIM_TOKEN, commandId: MCP_COMMAND_ID, - driverInstanceId: PUBLIC_API_TEST_IDS.driverOwner as DriverInstanceId, + driverInstanceId, + effectId: claim.effectId as ExternalToolEffectId, + settlement: { + kind: "succeeded" as const, + providerReceiptJson: '{"orderId":"A-1"}', + result: MCP_SUCCESS_RESULT, + }, + }; + + await expect( + settleExternalToolEffect(database, { + ...settlement, + claimToken: MCP_OTHER_CLAIM_TOKEN, + settlement: { kind: "unknown" }, + }), + ).resolves.toEqual(claim); + await settleExternalToolEffect(database, settlement); + await expect( + settleExternalToolEffect(database, { + ...settlement, + settlement: { + kind: "succeeded", + result: { ...MCP_SUCCESS_RESULT, outputText: "conflicting retry" }, + }, + }), + ).resolves.toEqual({ + effectId: claim.effectId, + kind: "succeeded", + result: MCP_SUCCESS_RESULT, }); + }); + + test("rejects an oversized succeeded settlement before its ledger batch", async () => { + const command = mcpCommandAtSize(RUNTIME_COMMAND_MAX_UTF8_BYTES); + const exact = succeededSettlementAtSize(MCP_EXTERNAL_TOOL_EFFECT_SETTLEMENT_MAX_UTF8_BYTES); + const oversized = succeededSettlementAtSize( + MCP_EXTERNAL_TOOL_EFFECT_SETTLEMENT_MAX_UTF8_BYTES + 1, + ); + expect(measureMcpExternalToolEffectSettlement(exact)).toBe( + MCP_EXTERNAL_TOOL_EFFECT_SETTLEMENT_MAX_UTF8_BYTES, + ); + expect(ExternalToolEffectSettlement.allows(exact)).toBeTrue(); + expect(ExternalToolEffectSettlement.allows(oversized)).toBeFalse(); + + expect(measureRuntimeCommandJson(command) + measureMcpExternalToolEffectSettlement(exact)).toBe( + 2_000_000 - 128 * 1_024, + ); + + const { bindings, claim, database, driverInstanceId } = + await createClaimedMcpEffectFixture(command); + const originalBatch = database.batch.bind(database); + let batchCalls = 0; + database.batch = ((statements: D1PreparedStatement[]) => { + batchCalls += 1; + return originalBatch(statements); + }) as typeof database.batch; + + await expect( + settleExternalToolEffect(database, { + claimToken: MCP_CLAIM_TOKEN, + commandId: MCP_COMMAND_ID, + driverInstanceId, + effectId: claim.effectId as ExternalToolEffectId, + settlement: oversized, + }), + ).rejects.toThrow(`${MCP_EXTERNAL_TOOL_EFFECT_SETTLEMENT_MAX_UTF8_BYTES} UTF-8 bytes`); + expect(batchCalls).toBe(0); + await expect( + observeExternalToolEffect(database, { commandId: MCP_COMMAND_ID, driverInstanceId }), + ).resolves.toEqual(claim); + expect( + await database + .prepare( + "SELECT completed_at, result_json, status FROM external_tool_effect_attempt WHERE effect_id = ?", + ) + .bind(claim.effectId) + .first(), + ).toEqual({ completed_at: null, result_json: null, status: "claimed" }); + + await expect( + settleExternalToolEffect(database, { + claimToken: MCP_CLAIM_TOKEN, + commandId: MCP_COMMAND_ID, + driverInstanceId, + effectId: claim.effectId as ExternalToolEffectId, + settlement: exact, + }), + ).resolves.toMatchObject({ kind: "succeeded", result: exact.result }); + expect(batchCalls).toBe(1); + await setDriverStatus(database, "failed"); await repairFinalizedTerminalDriverRunState(bindings, { - driverInstanceId: PUBLIC_API_TEST_IDS.driverOwner as DriverInstanceId, - status: "stopped", + driverInstanceId, + status: "failed", }); + await expect( + updateRuntimeCommandRecord(database, { + commandId: MCP_COMMAND_ID, + driverInstanceId, + result: exact.result, + status: "completed", + }), + ).resolves.toMatchObject({ kind: "duplicate" }); + + const row = await database + .prepare("SELECT payload_json, result_json, status FROM driver_command WHERE id = ?") + .bind(MCP_COMMAND_ID) + .first<{ payload_json: string; result_json: string; status: string }>(); + expect(row?.status).toBe("completed"); + expect(measureRuntimeCommandJson(JSON.parse(row!.payload_json))).toBe( + RUNTIME_COMMAND_MAX_UTF8_BYTES, + ); + expect(measureRuntimeCommandJson(JSON.parse(row!.result_json))).toBe( + measureRuntimeCommandJson(exact.result), + ); + }); + + test("rejects an oversized command before allocating sequence or effect state", async () => { + const { database, driverInstanceId } = await createTerminalDriverFixture(); + await setDriverStatus(database, "ready"); + const command = mcpCommandAtSize(RUNTIME_COMMAND_MAX_UTF8_BYTES + 1); + const originalPrepare = database.prepare; + let prepareCalls = 0; + database.prepare = ((query: string) => { + prepareCalls += 1; + return originalPrepare.call(database, query); + }) as typeof database.prepare; + + try { + await expect( + createRuntimeCommandRecord(database, { + command, + driverInstanceId, + status: "accepted", + }), + ).rejects.toThrow(`Runtime command exceeds ${RUNTIME_COMMAND_MAX_UTF8_BYTES} UTF-8 bytes.`); + } finally { + database.prepare = originalPrepare; + } + + expect(prepareCalls).toBe(0); + expect( + await database + .prepare("SELECT command_seq_cursor FROM driver_instance WHERE id = ?") + .bind(driverInstanceId) + .first(), + ).toEqual({ command_seq_cursor: 0 }); + expect(await database.prepare("SELECT COUNT(*) AS count FROM driver_command").first()).toEqual({ + count: 0, + }); + expect( + await database.prepare("SELECT COUNT(*) AS count FROM external_tool_effect").first(), + ).toEqual({ count: 0 }); + }); + + test("stores the maximum command beside the maximum error payload", async () => { + expect(DURABLE_RUN_ERROR_MAX_UTF8_BYTES).toBe(RUNTIME_COMMAND_TERMINAL_PAYLOAD_MAX_UTF8_BYTES); + const createError = (message: string): RunError => ({ + code: "driver.failed", + details: {}, + message, + retryable: false, + }); + const exact = textFieldAtJsonSize(RUNTIME_COMMAND_TERMINAL_PAYLOAD_MAX_UTF8_BYTES, createError); + const oversized = textFieldAtJsonSize( + RUNTIME_COMMAND_TERMINAL_PAYLOAD_MAX_UTF8_BYTES + 1, + createError, + ); + const command = inputStartCommandAtSize(RUNTIME_COMMAND_MAX_UTF8_BYTES); + const { database, driverInstanceId } = await createReadyMcpEffectFixture(command); + const update = (error: RunError) => + updateRuntimeCommandRecord(database, { + commandId: MCP_COMMAND_ID, + driverInstanceId, + error, + status: "failed", + }); + const originalPrepare = database.prepare; + let prepareCalls = 0; + database.prepare = ((query: string) => { + prepareCalls += 1; + return originalPrepare.call(database, query); + }) as typeof database.prepare; + + try { + await expect(update(oversized)).rejects.toThrow( + `Runtime command terminal payload exceeds ${RUNTIME_COMMAND_TERMINAL_PAYLOAD_MAX_UTF8_BYTES} UTF-8 bytes.`, + ); + } finally { + database.prepare = originalPrepare; + } + + expect(prepareCalls).toBe(0); + await commitInputCommandTerminalAuthority(database, { error: exact, status: "failed" }); + await expect(update(exact)).resolves.toMatchObject({ kind: "applied" }); + + const row = await database + .prepare( + "SELECT error_json, payload_json, result_json, status FROM driver_command WHERE id = ?", + ) + .bind(MCP_COMMAND_ID) + .first<{ + error_json: string | null; + payload_json: string; + result_json: string | null; + status: string; + }>(); + expect(row?.status).toBe("failed"); + expect(measureRuntimeCommandJson(JSON.parse(row!.payload_json))).toBe( + RUNTIME_COMMAND_MAX_UTF8_BYTES, + ); + expect(measureRuntimeCommandJson(JSON.parse(row!.error_json))).toBe( + RUNTIME_COMMAND_TERMINAL_PAYLOAD_MAX_UTF8_BYTES, + ); + expect(row!.result_json).toBeNull(); + }); + test.each(["requestId", "serverId", "toolName"] as const)( + "rejects a succeeded settlement whose %s does not match the immutable intent", + async (field) => { + const { bindings, claim, database, driverInstanceId } = await createClaimedMcpEffectFixture(); + + await expect( + settleExternalToolEffect(database, { + claimToken: MCP_CLAIM_TOKEN, + commandId: MCP_COMMAND_ID, + driverInstanceId, + effectId: claim.effectId as ExternalToolEffectId, + settlement: { + kind: "succeeded", + result: { ...MCP_SUCCESS_RESULT, [field]: `wrong-${field}` }, + }, + }), + ).rejects.toThrow("External tool effect result does not match its immutable command intent."); + await expect( + observeExternalToolEffect(database, { commandId: MCP_COMMAND_ID, driverInstanceId }), + ).resolves.toEqual(claim); + + await setDriverStatus(database, "failed"); + await repairFinalizedTerminalDriverRunState(bindings, { + driverInstanceId, + status: "failed", + }); + + await expect( + observeExternalToolEffect(database, { commandId: MCP_COMMAND_ID, driverInstanceId }), + ).resolves.toMatchObject({ effectId: claim.effectId, kind: "unknown" }); + expect( + await getRuntimeCommandRecord(database, driverInstanceId, MCP_COMMAND_ID), + ).toMatchObject({ + error: { code: "driver.external_tool_effect_unknown" }, + status: "failed", + }); + }, + ); + + test("does not collapse an MCP error result into a successful command result", async () => { + const { bindings, claim, database, driverInstanceId } = await createClaimedMcpEffectFixture(); + const errorResult = { ...MCP_SUCCESS_RESULT, isError: true } as const; + await settleExternalToolEffect(database, { + claimToken: MCP_CLAIM_TOKEN, + commandId: MCP_COMMAND_ID, + driverInstanceId, + effectId: claim.effectId as ExternalToolEffectId, + settlement: { kind: "succeeded", result: errorResult }, + }); + await setDriverStatus(database, "failed"); + await repairFinalizedTerminalDriverRunState(bindings, { + driverInstanceId, + status: "failed", + }); + + await expect( + updateRuntimeCommandRecord(database, { + commandId: MCP_COMMAND_ID, + driverInstanceId, + result: MCP_SUCCESS_RESULT, + status: "completed", + }), + ).rejects.toThrow("duplicate conflicts with its durable terminal payload"); + await expect( + updateRuntimeCommandRecord(database, { + commandId: MCP_COMMAND_ID, + driverInstanceId, + result: errorResult, + status: "completed", + }), + ).resolves.toMatchObject({ kind: "duplicate" }); + expect(await getRuntimeCommandRecord(database, driverInstanceId, MCP_COMMAND_ID)).toMatchObject( + { + result: errorResult, + status: "completed", + }, + ); + }); + + test("projects the canonical winner of settlement and terminal repair", async () => { + const { bindings, claim, database, driverInstanceId } = await createClaimedMcpEffectFixture(); + await setDriverStatus(database, "failed"); + + await Promise.all([ + settleExternalToolEffect(database, { + claimToken: MCP_CLAIM_TOKEN, + commandId: MCP_COMMAND_ID, + driverInstanceId, + effectId: claim.effectId as ExternalToolEffectId, + settlement: { kind: "succeeded", result: MCP_SUCCESS_RESULT }, + }), + repairFinalizedTerminalDriverRunState(bindings, { driverInstanceId, status: "failed" }), + ]); + + const state = await observeExternalToolEffect(database, { + commandId: MCP_COMMAND_ID, + driverInstanceId, + }); + await repairFinalizedTerminalDriverRunState(bindings, { driverInstanceId, status: "failed" }); + const command = await getRuntimeCommandRecord(database, driverInstanceId, MCP_COMMAND_ID); + + if (state.kind === "succeeded") { + expect(command).toMatchObject({ result: MCP_SUCCESS_RESULT, status: "completed" }); + } else { + expect(state.kind).toBe("unknown"); + expect(command).toMatchObject({ + error: { + code: "driver.external_tool_effect_unknown", + details: { effectId: claim.effectId }, + }, + status: "failed", + }); + } + expect( + await database + .prepare("SELECT completed_at, status FROM external_tool_effect_attempt") + .first<{ completed_at: number | null; status: string }>(), + ).toMatchObject({ completed_at: expect.any(Number), status: state.kind }); + }); + + test("retains an unknown MCP effect after terminal Driver maintenance", async () => { + const { bindings, database, driverInstanceId } = await createClaimedMcpEffectFixture(); + await setDriverStatus(database, "stopped"); await cleanupDriverInstances(bindings); expect( @@ -741,43 +1386,39 @@ describe("driver finalization repair", () => { expect( await getExternalToolEffectForCommand(database, { commandId: MCP_COMMAND_ID, - driverInstanceId: PUBLIC_API_TEST_IDS.driverOwner as DriverInstanceId, + driverInstanceId, }), ).toMatchObject({ status: "unknown" }); + expect(await getRuntimeCommandRecord(database, driverInstanceId, MCP_COMMAND_ID)).toMatchObject( + { + error: { code: "driver.external_tool_effect_unknown" }, + status: "failed", + }, + ); }); test("redelivers a persisted successful MCP result without a second provider invocation", async () => { - const database = await createPublicHttpContractDatabase(); - await insertFinalizedDriverLeaseFixture(database); - - await createRuntimeCommandRecord(database, { - command: mcpExecuteCommand(MCP_COMMAND_ID), - driverInstanceId: PUBLIC_API_TEST_IDS.driverOwner as DriverInstanceId, - status: "accepted", - }); - await claimExternalToolEffect(database, { + const { bindings, claim, database, driverInstanceId } = await createClaimedMcpEffectFixture(); + await settleExternalToolEffect(database, { + claimToken: MCP_CLAIM_TOKEN, commandId: MCP_COMMAND_ID, - driverInstanceId: PUBLIC_API_TEST_IDS.driverOwner as DriverInstanceId, - }); - await completeExternalToolEffect(database, { - commandId: MCP_COMMAND_ID, - driverInstanceId: PUBLIC_API_TEST_IDS.driverOwner as DriverInstanceId, - providerReceiptJson: '{"orderId":"A-1"}', - result: { - outputText: "created issue A-1", - requestId: "request-01J0000000000000000000000X", - serverId: "01J0000000000000000000000Y", - toolName: "createIssue", + driverInstanceId, + effectId: claim.effectId as ExternalToolEffectId, + settlement: { + kind: "succeeded", + providerReceiptJson: '{"orderId":"A-1"}', + result: MCP_SUCCESS_RESULT, }, }); await expect( claimExternalToolEffect(database, { + claimToken: MCP_OTHER_CLAIM_TOKEN, commandId: MCP_COMMAND_ID, - driverInstanceId: PUBLIC_API_TEST_IDS.driverOwner as DriverInstanceId, + driverInstanceId, }), ).resolves.toMatchObject({ - kind: "completed", + kind: "succeeded", result: { outputText: "created issue A-1", requestId: "request-01J0000000000000000000000X", @@ -785,10 +1426,26 @@ describe("driver finalization repair", () => { toolName: "createIssue", }, }); + await expect( + updateRuntimeCommandRecord(database, { + commandId: MCP_COMMAND_ID, + driverInstanceId, + result: { ...MCP_SUCCESS_RESULT, outputText: "not the stored result" }, + status: "completed", + }), + ).resolves.toMatchObject({ kind: "rejected", reason: "illegal_transition" }); + await setDriverStatus(database, "failed"); + await repairFinalizedTerminalDriverRunState(bindings, { + driverInstanceId, + status: "failed", + }); + expect(await getRuntimeCommandRecord(database, driverInstanceId, MCP_COMMAND_ID)).toMatchObject( + { result: MCP_SUCCESS_RESULT, status: "completed" }, + ); expect( await getExternalToolEffectForCommand(database, { commandId: MCP_COMMAND_ID, - driverInstanceId: PUBLIC_API_TEST_IDS.driverOwner as DriverInstanceId, + driverInstanceId, }), ).toMatchObject({ providerReceiptJson: '{"orderId":"A-1"}', status: "succeeded" }); expect( @@ -799,15 +1456,8 @@ describe("driver finalization repair", () => { }); test("restarts a real Driver dispatcher without replaying a persisted MCP effect", async () => { - const database = await createPublicHttpContractDatabase(); - await insertFinalizedDriverLeaseFixture(database); const command = mcpExecuteCommand(MCP_COMMAND_ID) as DriverRuntimeCommand; - const driverInstanceId = PUBLIC_API_TEST_IDS.driverOwner as DriverInstanceId; - await createRuntimeCommandRecord(database, { - command, - driverInstanceId, - status: "accepted", - }); + const { database, driverInstanceId } = await createReadyMcpEffectFixture(command); let providerCalls = 0; const firstIo = new PersistentEffectDriverIo({ @@ -830,8 +1480,18 @@ describe("driver finalization repair", () => { }, }); - await first.dispatcher.run(firstIo, first.logger); - await firstIo.effectPersisted.promise; + const firstRun = first.dispatcher.run(firstIo, first.logger); + await Promise.all([ + expect(firstRun).rejects.toThrow("terminal status could not be delivered"), + promiseWithTimeout(firstIo.effectPersisted.promise, { + label: "durable MCP effect persistence", + timeoutMs: DISPATCHER_TEST_PHASE_TIMEOUT_MS, + }), + promiseWithTimeout(firstIo.lostCompletedReceipt.promise, { + label: "lost MCP completion receipt", + timeoutMs: DISPATCHER_TEST_PHASE_TIMEOUT_MS, + }), + ]); await first.logger.destroy(); const secondIo = new PersistentEffectDriverIo({ @@ -848,7 +1508,10 @@ describe("driver finalization repair", () => { }); await second.dispatcher.run(secondIo, second.logger); - await secondIo.completedReceipt.promise; + await promiseWithTimeout(secondIo.completedReceipt.promise, { + label: "replayed MCP completion receipt", + timeoutMs: DISPATCHER_TEST_PHASE_TIMEOUT_MS, + }); await second.logger.destroy(); expect(providerCalls).toBe(1); diff --git a/apps/api/tests/driver-instance-http.test.ts b/apps/api/tests/driver-instance-http.test.ts index fae1840c..63603f75 100644 --- a/apps/api/tests/driver-instance-http.test.ts +++ b/apps/api/tests/driver-instance-http.test.ts @@ -6,28 +6,35 @@ import type { DriverInstanceHttpHandler } from "../src/modules/runtime/infrastru import { handleDriverInstanceRequest } from "../src/modules/runtime/infrastructure/driver-instance/http"; interface CapturingDriverInstanceHttpHandler extends DriverInstanceHttpHandler { + readonly destroyGenerations: number[]; readonly destroyReasons: string[]; readonly driverSocketRequests: Request[]; + readonly readyGenerations: number[]; } function createDriverInstanceHttpHandler(): CapturingDriverInstanceHttpHandler { + const destroyGenerations: number[] = []; const destroyReasons: string[] = []; const driverSocketRequests: Request[] = []; + const readyGenerations: number[] = []; return { + destroyGenerations, destroyReasons, driverSocketRequests, + readyGenerations, async acceptDriverSocket(request: Request): Promise { driverSocketRequests.push(request); return Response.json({ ok: true }); }, - async destroy(reason: string): Promise { + async destroy(generation: number, reason: string): Promise { + destroyGenerations.push(generation); destroyReasons.push(reason); }, - async fail(_message: string): Promise { + async fail(_generation: number, _message: string): Promise { throw new Error("Unexpected fail call."); }, - async sendControlCommand(_command: RuntimeCommand): Promise { + async sendControlCommand(_generation: number, _command: RuntimeCommand): Promise { throw new Error("Unexpected command call."); }, snapshot() { @@ -36,14 +43,17 @@ function createDriverInstanceHttpHandler(): CapturingDriverInstanceHttpHandler { async waitForClose() { throw new Error("Unexpected close wait call."); }, - async waitForHeartbeat() { - throw new Error("Unexpected heartbeat wait call."); - }, - async waitForHello() { - throw new Error("Unexpected hello wait call."); - }, - async waitForReady() { - throw new Error("Unexpected ready wait call."); + async waitForReady(generation: number) { + readyGenerations.push(generation); + return { + heartbeatCount: 0, + lastHeartbeatAt: null, + ready: { + at: "2026-01-01T00:00:00.000Z", + driverInstanceId: "driver-instance", + pid: 1, + }, + }; }, }; } @@ -94,14 +104,51 @@ describe("driver instance HTTP boundary", () => { expect(handler.driverSocketRequests).toHaveLength(0); }); - test("uses the default destroy reason for an empty destroy body", async () => { - const { handler, payload, response } = await postDestroyRequest(); + test("requires and preserves the exact ready generation", async () => { + const handler = createDriverInstanceHttpHandler(); + const missing = await handleDriverInstanceRequest( + handler, + new Request("https://driver.local/wait/ready?timeoutMs=1000"), + ).catch((error: unknown) => error); + + expect(missing).toBeInstanceOf(TypeError); + const response = await handleDriverInstanceRequest( + handler, + new Request("https://driver.local/wait/ready?generation=7&timeoutMs=1000"), + ); + expect(response.status).toBe(200); + expect(handler.readyGenerations).toEqual([7]); + }); + + test("removes unused hello and heartbeat wait routes", async () => { + const handler = createDriverInstanceHttpHandler(); + + for (const path of ["/wait/hello", "/wait/heartbeat"]) { + const response = await handleDriverInstanceRequest( + handler, + new Request(`https://driver.local${path}?timeoutMs=1000`), + ); + expect(response.status).toBe(404); + } + }); + + test("uses the default destroy reason while preserving the exact generation", async () => { + const { handler, payload, response } = await postDestroyRequest('{"generation":3}'); expect(response.status).toBe(200); expect(payload).toEqual({ ok: true }); + expect(handler.destroyGenerations).toEqual([3]); expect(handler.destroyReasons).toEqual(["runtime.driver_instance.destroyed"]); }); + test("rejects a destroy without a generation", async () => { + const { handler, response } = await postDestroyRequest(); + + expect(response.status).toBe(400); + expect(handler.destroyGenerations).toEqual([]); + expect(handler.destroyReasons).toEqual([]); + }); + test("rejects malformed destroy JSON instead of silently using the default reason", async () => { const { handler, payload, response } = await postDestroyRequest("{"); @@ -111,4 +158,18 @@ describe("driver instance HTTP boundary", () => { }); expect(handler.destroyReasons).toEqual([]); }); + + test("rejects an invalid runtime command at the HTTP boundary", async () => { + const handler = createDriverInstanceHttpHandler(); + const response = await handleDriverInstanceRequest( + handler, + new Request("https://driver.local/control/send", { + body: JSON.stringify({ commandId: "command-1", kind: "input.start" }), + method: "POST", + }), + ); + + expect(response.status).toBe(400); + await expect(response.json()).resolves.toEqual({ error: expect.any(String) }); + }); }); diff --git a/apps/api/tests/driver-instance-record.test.ts b/apps/api/tests/driver-instance-record.test.ts index c6528322..55413068 100644 --- a/apps/api/tests/driver-instance-record.test.ts +++ b/apps/api/tests/driver-instance-record.test.ts @@ -1,7 +1,8 @@ import { describe, expect, test } from "bun:test"; +import { DRIVER_PROTOCOL_VERSION } from "@mosoo/agent-driver/boot"; import { parsePlatformId } from "@mosoo/id"; -import type { DriverInstanceId, SandboxId, SessionId, SessionRunId } from "@mosoo/id"; +import type { DriverInstanceId, McpServerId, SandboxId, SessionId, SessionRunId } from "@mosoo/id"; import { PLATFORM_ID_FIXTURES } from "@mosoo/id/testing"; import { @@ -15,7 +16,10 @@ import { markDriverInstanceFailedIfBootTokenMatches, recordRuntimeProcessStarted, } from "../src/modules/runtime/infrastructure/driver-instance/driver-instance-record.repository"; -import { claimDriverInstanceByBootTokenHash } from "../src/modules/runtime/infrastructure/driver-instance/driver-instance-token.repository"; +import { + claimDriverInstanceByBootTokenHash, + validateDriverInstanceBootTokenHash, +} from "../src/modules/runtime/infrastructure/driver-instance/driver-instance-token.repository"; import { finalizeDriverInstance, markDriverInstanceConnected, @@ -32,37 +36,55 @@ const REPLACEMENT_DRIVER_INSTANCE_ID = parsePlatformId( "replacement driver instance id", ); const SANDBOX_ID = PLATFORM_ID_FIXTURES.sandbox; +const SANDBOX_INCARNATION = 1; const SESSION_ID = PLATFORM_ID_FIXTURES.session; const SESSION_RUN_ID = PLATFORM_ID_FIXTURES.sessionRun; const NEXT_SESSION_RUN_ID = parsePlatformId( "01J0000000000000000000000Q", "next session run id", ); +const EFFECT_COMMAND_ID = "01J0000000000000000000000T"; +const EFFECT_ID = "01J0000000000000000000000V"; +const MCP_SERVER_ID = parsePlatformId("01J0000000000000000000000W", "MCP server id"); +const REPLACEMENT_MCP_SERVER_ID = parsePlatformId( + "01J0000000000000000000000X", + "replacement MCP server id", +); function createDriverInstanceRecordDatabase(): SqliteD1Database { - const database = new SqliteD1Database({ foreignKeys: false }); + const database = new SqliteD1Database(); database.execute(` CREATE TABLE driver_command ( - driver_instance_id text NOT NULL + driver_generation integer DEFAULT 0 NOT NULL, + driver_instance_id text NOT NULL, + id text PRIMARY KEY NOT NULL, + status text NOT NULL, + FOREIGN KEY (driver_instance_id) REFERENCES driver_instance(id) ON DELETE CASCADE ); CREATE TABLE driver_instance_mcp_grant ( + app_id text NOT NULL, auth_type text NOT NULL, - authorization_state text, + authorization_state text NOT NULL, can_invalidate integer NOT NULL, can_refresh integer NOT NULL, created_at integer NOT NULL, credential_id text, driver_instance_id text NOT NULL, server_id text NOT NULL, - updated_at integer NOT NULL + updated_at integer NOT NULL, + FOREIGN KEY (driver_instance_id) REFERENCES driver_instance(id) ON DELETE CASCADE, + UNIQUE (driver_instance_id, server_id) ); CREATE TABLE external_tool_effect ( + command_id text NOT NULL, driver_instance_id text NOT NULL, id text PRIMARY KEY NOT NULL, - status text NOT NULL + status text NOT NULL, + FOREIGN KEY (command_id) REFERENCES driver_command(id) ON DELETE CASCADE, + FOREIGN KEY (driver_instance_id) REFERENCES driver_instance(id) ON DELETE CASCADE ); CREATE TABLE driver_instance ( @@ -89,6 +111,7 @@ function createDriverInstanceRecordDatabase(): SqliteD1Database { restart_count integer NOT NULL, runtime text NOT NULL, sandbox_id text NOT NULL, + sandbox_incarnation integer DEFAULT 0 NOT NULL, sandbox_session_id text NOT NULL, status text NOT NULL, status_changed_at integer DEFAULT 0 NOT NULL, @@ -102,8 +125,15 @@ function createDriverInstanceRecordDatabase(): SqliteD1Database { CREATE TABLE session_run ( driver_instance_id text, id text PRIMARY KEY NOT NULL, + status text DEFAULT 'running' NOT NULL, updated_at integer NOT NULL ); + + CREATE TABLE session ( + id text PRIMARY KEY NOT NULL, + runtime_provisioning_operation_id text, + runtime_provisioning_sandbox_id text + ); `); return database; @@ -192,6 +222,7 @@ function insertDriverRecord( restart_count, runtime, sandbox_id, + sandbox_incarnation, sandbox_session_id, status, status_changed_at, @@ -224,6 +255,7 @@ function insertDriverRecord( 0, 'openai-runtime', '${SANDBOX_ID}', + ${SANDBOX_INCARNATION}, '${SESSION_ID}', '${input.status ?? "provisioning"}', 1, @@ -239,6 +271,28 @@ function insertDriverRecord( .run(); } +function insertExternalEffectFixture( + database: SqliteD1Database, + input: { + commandStatus: "accepted" | "completed" | "failed" | "queued"; + effectStatus: "claimed" | "intent" | "succeeded" | "unknown"; + }, +): void { + database.execute(` + INSERT INTO driver_command (driver_instance_id, id, status) + VALUES ('${DRIVER_INSTANCE_ID}', '${EFFECT_COMMAND_ID}', '${input.commandStatus}'); + INSERT INTO external_tool_effect (command_id, driver_instance_id, id, status) + VALUES ('${EFFECT_COMMAND_ID}', '${DRIVER_INSTANCE_ID}', '${EFFECT_ID}', '${input.effectStatus}'); + INSERT INTO driver_instance_mcp_grant ( + app_id, auth_type, authorization_state, can_invalidate, can_refresh, + created_at, credential_id, driver_instance_id, server_id, updated_at + ) VALUES ( + '${PLATFORM_ID_FIXTURES.app}', 'oauth', 'expired', 0, 0, + 1, NULL, '${DRIVER_INSTANCE_ID}', '${MCP_SERVER_ID}', 1 + ); + `); +} + describe("driver instance records", () => { test("insert-only creation does not overwrite an existing record", async () => { const database = createDriverInstanceRecordDatabase(); @@ -250,6 +304,7 @@ describe("driver instance records", () => { driverInstanceId: DRIVER_INSTANCE_ID, runtime: "openai-runtime", sandboxId: SANDBOX_ID, + sandboxIncarnation: SANDBOX_INCARNATION, sandboxSessionId: SESSION_ID, }); @@ -269,6 +324,10 @@ describe("driver instance records", () => { test("replace creation rotates generation on an existing record", async () => { const database = createDriverInstanceRecordDatabase(); insertDriverRecord(database, {}); + insertExternalEffectFixture(database, { + commandStatus: "queued", + effectStatus: "intent", + }); database.execute(` INSERT INTO session_run (driver_instance_id, id, updated_at) VALUES (NULL, '${NEXT_SESSION_RUN_ID}', 1) @@ -279,7 +338,19 @@ describe("driver instance records", () => { driverInstanceId: DRIVER_INSTANCE_ID, runtime: "openai-runtime", sandboxId: SANDBOX_ID, + sandboxIncarnation: SANDBOX_INCARNATION, sandboxSessionId: SESSION_ID, + mcpGrants: [ + { + appId: PLATFORM_ID_FIXTURES.app, + authType: "bearer", + authorizationState: "active", + canInvalidate: false, + canRefresh: true, + credentialId: null, + serverId: MCP_SERVER_ID, + }, + ], }); await expect(readDriverRecord(database)).resolves.toMatchObject({ @@ -289,6 +360,132 @@ describe("driver instance records", () => { }); expect(result.status).toBe("created"); expect(result.generation).toBe(1); + await expect( + database + .prepare("SELECT id FROM driver_command WHERE id = ?") + .bind(EFFECT_COMMAND_ID) + .first(), + ).resolves.toBeNull(); + await expect( + database.prepare("SELECT id FROM external_tool_effect WHERE id = ?").bind(EFFECT_ID).first(), + ).resolves.toBeNull(); + await expect( + database + .prepare( + "SELECT auth_type, authorization_state, can_refresh FROM driver_instance_mcp_grant WHERE driver_instance_id = ?", + ) + .bind(DRIVER_INSTANCE_ID) + .first(), + ).resolves.toEqual({ + auth_type: "bearer", + authorization_state: "active", + can_refresh: 1, + }); + }); + + test("does not rotate a Driver generation owned by terminal cleanup", async () => { + const database = createDriverInstanceRecordDatabase(); + insertDriverRecord(database, { status: "failed" }); + database.execute(` + UPDATE driver_instance + SET status_operation_id = '${SESSION_RUN_ID}' + WHERE id = '${DRIVER_INSTANCE_ID}' + `); + + await expect( + createDriverInstanceRecord(createBindings(database), { + bootTokenHash: token(2), + driverInstanceId: DRIVER_INSTANCE_ID, + runtime: "openai-runtime", + sandboxId: SANDBOX_ID, + sandboxIncarnation: SANDBOX_INCARNATION, + sandboxSessionId: SESSION_ID, + }), + ).rejects.toThrow("replacement is blocked"); + await expect(readDriverRecord(database)).resolves.toMatchObject({ + bootTokenHex: "01", + generation: 0, + status: "failed", + }); + }); + + for (const effectStatus of ["intent", "claimed", "unknown", "succeeded"] as const) { + test(`replace preserves a protected ${effectStatus} external effect`, async () => { + const database = createDriverInstanceRecordDatabase(); + insertDriverRecord(database, { status: "failed" }); + insertExternalEffectFixture(database, { commandStatus: "accepted", effectStatus }); + + await expect( + createDriverInstanceRecord(createBindings(database), { + bootTokenHash: token(2), + driverInstanceId: DRIVER_INSTANCE_ID, + runtime: "openai-runtime", + sandboxId: SANDBOX_ID, + sandboxIncarnation: SANDBOX_INCARNATION, + sandboxSessionId: SESSION_ID, + mcpGrants: [ + { + appId: PLATFORM_ID_FIXTURES.app, + authType: "bearer", + authorizationState: "active", + canInvalidate: false, + canRefresh: false, + credentialId: null, + serverId: REPLACEMENT_MCP_SERVER_ID, + }, + ], + }), + ).rejects.toThrow("blocked by a protected external effect"); + + await expect(readDriverRecord(database)).resolves.toMatchObject({ + bootTokenHex: "01", + generation: 0, + status: "failed", + }); + await expect( + database + .prepare("SELECT status FROM driver_command WHERE id = ?") + .bind(EFFECT_COMMAND_ID) + .first(), + ).resolves.toEqual({ status: "accepted" }); + await expect( + database + .prepare("SELECT status FROM external_tool_effect WHERE id = ?") + .bind(EFFECT_ID) + .first(), + ).resolves.toEqual({ status: effectStatus }); + await expect( + database + .prepare( + "SELECT COUNT(*) AS count, MIN(auth_type) AS auth_type FROM driver_instance_mcp_grant WHERE driver_instance_id = ?", + ) + .bind(DRIVER_INSTANCE_ID) + .first(), + ).resolves.toEqual({ auth_type: "oauth", count: 1 }); + }); + } + + test("replace discards a succeeded effect only after its command is terminal", async () => { + const database = createDriverInstanceRecordDatabase(); + insertDriverRecord(database, { status: "failed" }); + insertExternalEffectFixture(database, { + commandStatus: "completed", + effectStatus: "succeeded", + }); + + await expect( + createDriverInstanceRecord(createBindings(database), { + bootTokenHash: token(2), + driverInstanceId: DRIVER_INSTANCE_ID, + runtime: "openai-runtime", + sandboxId: SANDBOX_ID, + sandboxIncarnation: SANDBOX_INCARNATION, + sandboxSessionId: SESSION_ID, + }), + ).resolves.toMatchObject({ generation: 1, status: "created" }); + await expect( + database.prepare("SELECT id FROM external_tool_effect WHERE id = ?").bind(EFFECT_ID).first(), + ).resolves.toBeNull(); }); test("does not reuse stopping driver records", async () => { @@ -307,6 +504,7 @@ describe("driver instance records", () => { await expect( getReusableDriverInstanceRecord(database, { sandboxId: SANDBOX_ID, + sandboxIncarnation: SANDBOX_INCARNATION, sandboxSessionId: SESSION_ID, }), ).resolves.toMatchObject({ @@ -334,8 +532,10 @@ describe("driver instance records", () => { const database = createDriverInstanceRecordDatabase(); insertDriverRecord(database, { status: "failed" }); database.execute(` - INSERT INTO external_tool_effect (driver_instance_id, id, status) - VALUES ('${DRIVER_INSTANCE_ID}', '01J0000000000000000000000Z', 'unknown') + INSERT INTO driver_command (driver_instance_id, id, status) + VALUES ('${DRIVER_INSTANCE_ID}', '${EFFECT_COMMAND_ID}', 'failed'); + INSERT INTO external_tool_effect (command_id, driver_instance_id, id, status) + VALUES ('${EFFECT_COMMAND_ID}', '${DRIVER_INSTANCE_ID}', '${EFFECT_ID}', 'unknown') `); await cleanupDriverInstances(createBindings(database)); @@ -343,6 +543,35 @@ describe("driver instance records", () => { await expect(readDriverRecord(database)).resolves.toMatchObject({ status: "failed" }); }); + test("maintenance retains a provisioning cleanup target until its lease is released", async () => { + const database = createDriverInstanceRecordDatabase(); + insertDriverRecord(database, { status: "failed" }); + database.execute(` + INSERT INTO session ( + id, runtime_provisioning_operation_id, runtime_provisioning_sandbox_id + ) VALUES ( + '${SESSION_ID}', '01J0000000000000000000000R', '${SANDBOX_ID}' + ) + `); + + await cleanupDriverInstances(createBindings(database)); + await expect(readDriverRecord(database)).resolves.toMatchObject({ status: "failed" }); + + database.execute(` + UPDATE session + SET runtime_provisioning_operation_id = NULL, + runtime_provisioning_sandbox_id = NULL + WHERE id = '${SESSION_ID}' + `); + await cleanupDriverInstances(createBindings(database)); + await expect( + database + .prepare("SELECT id FROM driver_instance WHERE id = ?") + .bind(DRIVER_INSTANCE_ID) + .first(), + ).resolves.toBeNull(); + }); + test("maintenance gives connecting drivers the cold ready budget", async () => { const database = createDriverInstanceRecordDatabase(); insertDriverRecord(database, { @@ -493,26 +722,39 @@ describe("driver instance records", () => { generation: 0, }), ).resolves.toBe(true); - await recordDriverInstanceHello(bindings, { - connectionId: "connection-1", - driverInstanceId: DRIVER_INSTANCE_ID, - generation: 0, - hello: { - capabilities: [], - driverVersion: "driver-test", - pid: 11, - protocolVersion: 2, - runtime: "openai-runtime", - startedAt: "2026-05-08T00:00:00.000Z", - }, - }); - await markDriverInstanceReady(bindings, { + const hello = { + capabilities: [], + driverVersion: "driver-test", + pid: 11, + protocolVersion: DRIVER_PROTOCOL_VERSION, + runtime: "openai-runtime" as const, + startedAt: "2026-05-08T00:00:00.000Z", + }; + await expect( + recordDriverInstanceHello(bindings, { + connectionId: "connection-1", + driverInstanceId: DRIVER_INSTANCE_ID, + generation: 0, + hello, + }), + ).resolves.toBe("applied"); + await expect( + recordDriverInstanceHello(bindings, { + connectionId: "connection-1", + driverInstanceId: DRIVER_INSTANCE_ID, + generation: 0, + hello, + }), + ).resolves.toBe("replay"); + const ready = { at: "2026-05-08T00:00:01.000Z", connectionId: "connection-1", driverInstanceId: DRIVER_INSTANCE_ID, generation: 0, pid: 22, - }); + }; + await expect(markDriverInstanceReady(bindings, ready)).resolves.toBe("applied"); + await expect(markDriverInstanceReady(bindings, ready)).resolves.toBe("replay"); await expect( recordDriverInstanceHello(bindings, { connectionId: "connection-1", @@ -522,12 +764,12 @@ describe("driver instance records", () => { capabilities: [], driverVersion: "late-driver-test", pid: 99, - protocolVersion: 2, + protocolVersion: DRIVER_PROTOCOL_VERSION, runtime: "openai-runtime", startedAt: "2026-05-08T00:00:02.000Z", }, }), - ).resolves.toBe(false); + ).resolves.toBe("conflict"); const row = await database .prepare( @@ -550,6 +792,47 @@ describe("driver instance records", () => { }); }); + test("replays the same boot claim after D1 connected but before DO state persisted", async () => { + const database = createDriverInstanceRecordDatabase(); + insertDriverRecord(database, {}); + const bindings = createBindings(database); + + await claimDriverInstanceByBootTokenHash(bindings, token(1)); + await expect( + markDriverInstanceConnected(bindings, { + bootTokenHash: token(1), + connectedAt: 1, + connectionId: "lost-connection", + driverInstanceId: DRIVER_INSTANCE_ID, + generation: 0, + }), + ).resolves.toBe(true); + + await expect(validateDriverInstanceBootTokenHash(bindings, token(1))).resolves.toEqual({ + driverInstanceId: DRIVER_INSTANCE_ID, + error: null, + generation: 0, + }); + await expect(claimDriverInstanceByBootTokenHash(bindings, token(1))).resolves.toEqual({ + driverInstanceId: DRIVER_INSTANCE_ID, + error: null, + generation: 0, + }); + await expect( + markDriverInstanceConnected(bindings, { + bootTokenHash: token(1), + connectedAt: 2, + connectionId: "recovered-connection", + driverInstanceId: DRIVER_INSTANCE_ID, + generation: 0, + }), + ).resolves.toBe(true); + await expect(readDriverRecord(database)).resolves.toMatchObject({ + connectionId: "recovered-connection", + status: "connecting", + }); + }); + test("does not let terminal driver finalization overwrite a failed driver", async () => { const database = createDriverInstanceRecordDatabase(); insertDriverRecord(database, {}); @@ -571,7 +854,7 @@ describe("driver instance records", () => { heartbeatCount: 3, status: "stopped", }), - ).resolves.toBe(false); + ).resolves.toBeNull(); await expect(readDriverRecord(database)).resolves.toMatchObject({ errorMessage: "failed first", @@ -595,6 +878,7 @@ describe("driver instance records", () => { driverInstanceId: DRIVER_INSTANCE_ID, runtime: "openai-runtime", sandboxId: SANDBOX_ID, + sandboxIncarnation: SANDBOX_INCARNATION, sandboxSessionId: SESSION_ID, }); @@ -628,7 +912,7 @@ describe("driver instance records", () => { heartbeatCount: 1, status: "failed", }), - ).resolves.toBe(false); + ).resolves.toBeNull(); await expect(readDriverRecord(database)).resolves.toMatchObject({ bootTokenHex: "02", diff --git a/apps/api/tests/driver-instance-runtime-state.test.ts b/apps/api/tests/driver-instance-runtime-state.test.ts new file mode 100644 index 00000000..3c50daa2 --- /dev/null +++ b/apps/api/tests/driver-instance-runtime-state.test.ts @@ -0,0 +1,199 @@ +import { describe, expect, test } from "bun:test"; + +import { DRIVER_PROTOCOL_VERSION } from "@mosoo/agent-driver/boot"; +import type { + DriverHelloInput, + DriverHelloOutput, + DriverReadyInput, +} from "@mosoo/agent-driver/orpc"; +import { PLATFORM_ID_FIXTURES } from "@mosoo/id/testing"; + +import { DriverInstanceRuntimeState } from "../src/modules/runtime/infrastructure/driver-instance/runtime-state"; + +class MemoryStorage { + failHelloCommitOnce = false; + failReadyCommitOnce = false; + readonly values = new Map(); + + async deleteAll(): Promise { + this.values.clear(); + } + + async get(key: string): Promise { + return structuredClone(this.values.get(key)) as T | undefined; + } + + async put(key: string, value: unknown): Promise { + if ( + this.failHelloCommitOnce && + typeof value === "object" && + value !== null && + "hello" in value && + value.hello !== null && + "pendingHello" in value && + value.pendingHello === null + ) { + this.failHelloCommitOnce = false; + throw new Error("DO hello commit failed"); + } + if ( + this.failReadyCommitOnce && + typeof value === "object" && + value !== null && + "ready" in value && + value.ready !== null && + "pendingReady" in value && + value.pendingReady === null + ) { + this.failReadyCommitOnce = false; + throw new Error("DO ready commit failed"); + } + + this.values.set(key, structuredClone(value)); + } +} + +const HELLO: DriverHelloInput = { + capabilities: [], + driverVersion: "test-driver", + pid: 42, + protocolVersion: DRIVER_PROTOCOL_VERSION, + runtime: "openai-runtime", + startedAt: "2026-08-30T00:00:00.000Z", +}; + +const HELLO_OUTPUT: DriverHelloOutput = { + acceptedCapabilities: [], + connectionId: "connection-0", + driverInstanceId: PLATFORM_ID_FIXTURES.driverInstance, + heartbeatIntervalMs: 1_000, + runConfig: { + commandLeaseMs: 1_000, + envPolicy: "strict", + eventBatchMaxSize: 64, + organizationPath: "/workspace", + }, + runId: PLATFORM_ID_FIXTURES.sessionRun, +}; + +const READY: DriverReadyInput = { + at: "2026-08-30T00:00:01.000Z", + driverInstanceId: PLATFORM_ID_FIXTURES.driverInstance, + pid: 42, +}; + +async function createState(storage = new MemoryStorage(), generation = 0) { + const state = new DriverInstanceRuntimeState({ storage }); + await state.load(); + await state.initializeDriverInstance(PLATFORM_ID_FIXTURES.driverInstance, generation); + await state.recordAcceptedConnection({ + connectedAt: 1, + connectionId: `connection-${generation}`, + driverGeneration: generation, + traceId: null, + }); + return { state, storage }; +} + +describe("driver instance durable handshake receipts", () => { + test("resumes an exact hello after D1 committed but the DO commit failed", async () => { + const { state, storage } = await createState(); + const epoch = state.requireConnectionEpoch(); + + await expect(state.stageHello(epoch, HELLO, HELLO_OUTPUT)).resolves.toBe("applied"); + storage.failHelloCommitOnce = true; + await expect(state.commitHello(epoch)).rejects.toThrow("DO hello commit failed"); + + const restarted = new DriverInstanceRuntimeState({ storage }); + await restarted.load(); + await expect(restarted.stageHello(epoch, HELLO, HELLO_OUTPUT)).resolves.toBe("resume"); + await expect(restarted.commitHello(epoch)).resolves.toEqual(HELLO_OUTPUT); + await expect(restarted.stageHello(epoch, HELLO, HELLO_OUTPUT)).resolves.toBe("replay"); + await expect(restarted.stageHello(epoch, { ...HELLO, pid: 43 }, HELLO_OUTPUT)).rejects.toThrow( + "conflicts with the canonical receipt", + ); + }); + + test("a same-generation successor requires a fresh hello receipt", async () => { + const { state } = await createState(); + const firstEpoch = state.requireConnectionEpoch(); + await state.stageHello(firstEpoch, HELLO, HELLO_OUTPUT); + await state.commitHello(firstEpoch); + + await state.recordAcceptedConnection({ + connectedAt: 2, + connectionId: "connection-successor", + driverGeneration: 0, + traceId: null, + }); + + expect(state.hello).toBeNull(); + expect(state.pendingHello).toBeNull(); + expect(state.ready).toBeNull(); + await expect(state.stageHello(firstEpoch, HELLO, HELLO_OUTPUT)).rejects.toThrow( + "no longer current", + ); + }); + + test("resumes exact ready projection after its DO commit fails", async () => { + const { state, storage } = await createState(); + const epoch = state.requireConnectionEpoch(); + await state.stageHello(epoch, HELLO, HELLO_OUTPUT); + await state.commitHello(epoch); + await expect(state.stageReady(epoch, READY)).resolves.toBe("applied"); + storage.failReadyCommitOnce = true; + await expect(state.commitReady(epoch)).rejects.toThrow("DO ready commit failed"); + + const restarted = new DriverInstanceRuntimeState({ storage }); + await restarted.load(); + await expect(restarted.stageReady(epoch, READY)).resolves.toBe("resume"); + await expect(restarted.commitReady(epoch)).resolves.toMatchObject({ ready: READY }); + await expect(restarted.stageReady(epoch, READY)).resolves.toBe("replay"); + await expect(restarted.stageReady(epoch, { ...READY, pid: 99 })).rejects.toThrow( + "conflicts with the canonical receipt", + ); + }); +}); + +describe("driver instance generation waiters", () => { + test("a generation-zero waiter cannot be resolved by generation one", async () => { + const { state } = await createState(); + const oldWait = state.waitForReady(0, 10_000); + const oldRejection = oldWait.catch((error: unknown) => error); + expect(state.readyWaiters).toHaveLength(1); + + await state.resetForReuse({ beforeReset: async () => {}, driverGeneration: 1 }); + expect(state.readyWaiters).toHaveLength(0); + expect(await oldRejection).toEqual( + expect.objectContaining({ + message: expect.stringContaining("generation is no longer current"), + }), + ); + await state.recordAcceptedConnection({ + connectedAt: 2, + connectionId: "connection-1", + driverGeneration: 1, + traceId: null, + }); + const epoch = state.requireConnectionEpoch(); + const output = { ...HELLO_OUTPUT, connectionId: epoch.connectionId }; + await state.stageHello(epoch, HELLO, output); + await state.commitHello(epoch); + + const currentWait = state.waitForReady(1, 10_000); + await state.stageReady(epoch, READY); + const result = await state.commitReady(epoch); + state.resolveReadyWaiters(result, 1); + await expect(currentWait).resolves.toEqual(result); + }); + + test("removes a timed-out waiter", async () => { + const { state } = await createState(); + + await expect(state.waitForReady(0, 1)).rejects.toThrow("timed out"); + expect(state.readyWaiters).toHaveLength(0); + expect(state.closeWaiters).toHaveLength(0); + expect(await state.waitForClose(0, 1).catch(() => null)).toBeNull(); + expect(state.closeWaiters).toHaveLength(0); + }); +}); diff --git a/apps/api/tests/driver-instance-sockets.test.ts b/apps/api/tests/driver-instance-sockets.test.ts index f198ede3..8b880bad 100644 --- a/apps/api/tests/driver-instance-sockets.test.ts +++ b/apps/api/tests/driver-instance-sockets.test.ts @@ -4,22 +4,33 @@ import { DriverInstanceSocketRegistry } from "../src/modules/runtime/infrastruct interface FakeSocket { acceptedTags: string[][]; + attachment: unknown; closes: { code: number; reason: string }[]; readyState: number; close(code?: number, reason?: string): void; + deserializeAttachment(): unknown; + serializeAttachment(value: unknown): void; } const SOCKET_OPEN = 1; +const SOCKET_CLOSING = 2; const SOCKET_CLOSED = 3; +const EPOCH_A = { connectionId: "connection-a", generation: 1 } as const; +const EPOCH_B = { connectionId: "connection-b", generation: 1 } as const; function createFakeSocket(): FakeSocket { const socket: FakeSocket = { acceptedTags: [], + attachment: null, closes: [], readyState: SOCKET_OPEN, close(code = 1000, reason = "") { socket.closes.push({ code, reason }); - socket.readyState = SOCKET_CLOSED; + socket.readyState = SOCKET_CLOSING; + }, + deserializeAttachment: () => socket.attachment, + serializeAttachment(value: unknown) { + socket.attachment = structuredClone(value); }, }; @@ -58,52 +69,98 @@ describe("driver instance socket registry", () => { const registry = new DriverInstanceSocketRegistry(ctx); const socket = createFakeSocket(); - registry.acceptDriverSocket(socket as unknown as WebSocket); + registry.acceptDriverSocket(socket as unknown as WebSocket, EPOCH_A); expect(accepted).toHaveLength(1); expect(accepted[0]?.tags).toEqual(["driver"]); - expect(registry.getDriverSocket()).toBe(socket as unknown as WebSocket); + expect(socket.attachment).toEqual(EPOCH_A); + expect(registry.getDriverSocket(EPOCH_A)).toBe(socket); }); test("finds the driver socket via tags after a hibernation wake", () => { const { ctx } = createFakeContext(); const bootRegistry = new DriverInstanceSocketRegistry(ctx); const socket = createFakeSocket(); - bootRegistry.acceptDriverSocket(socket as unknown as WebSocket); + bootRegistry.acceptDriverSocket(socket as unknown as WebSocket, EPOCH_A); // A wake after eviction constructs a fresh registry with no in-memory // active socket; the tagged socket must still be discoverable. const wokenRegistry = new DriverInstanceSocketRegistry(ctx); - expect(wokenRegistry.getDriverSocket()).toBe(socket as unknown as WebSocket); - expect(wokenRegistry.isActiveDriverSocket(socket as unknown as WebSocket)).toBe(true); - expect(wokenRegistry.isSupersededDriverSocket(socket as unknown as WebSocket)).toBe(false); + expect(wokenRegistry.getDriverSocket(EPOCH_A)).toBe(socket); + expect(wokenRegistry.socketMatchesEpoch(socket as unknown as WebSocket, EPOCH_A)).toBe(true); }); test("marks replaced sockets as superseded without affecting the successor", () => { const { ctx } = createFakeContext(); const registry = new DriverInstanceSocketRegistry(ctx); const first = createFakeSocket(); - registry.acceptDriverSocket(first as unknown as WebSocket); + registry.acceptDriverSocket(first as unknown as WebSocket, EPOCH_A); registry.replaceDriverSockets(); const second = createFakeSocket(); - registry.acceptDriverSocket(second as unknown as WebSocket); + registry.acceptDriverSocket(second as unknown as WebSocket, EPOCH_B); expect(first.closes).toEqual([{ code: 1012, reason: "runtime.socket.replaced" }]); - expect(registry.isSupersededDriverSocket(first as unknown as WebSocket)).toBe(true); - expect(registry.isSupersededDriverSocket(second as unknown as WebSocket)).toBe(false); - expect(registry.getDriverSocket()).toBe(second as unknown as WebSocket); + expect(first.readyState).toBe(SOCKET_CLOSING); + expect(registry.getDriverSocket(EPOCH_A)).toBeNull(); + expect(registry.getDriverSocket(EPOCH_B)).toBe(second); }); - test("does not treat the last closing socket as superseded", () => { + test("hibernation ignores a closing predecessor even when it is listed before the successor", () => { const { ctx } = createFakeContext(); const registry = new DriverInstanceSocketRegistry(ctx); + const first = createFakeSocket(); + registry.acceptDriverSocket(first as unknown as WebSocket, EPOCH_A); + registry.replaceDriverSockets(); + const second = createFakeSocket(); + registry.acceptDriverSocket(second as unknown as WebSocket, EPOCH_B); + + const wokenRegistry = new DriverInstanceSocketRegistry(ctx); + + expect(wokenRegistry.getDriverSocket(EPOCH_B)).toBe(second); + expect(wokenRegistry.getDriverSocket(EPOCH_A)).toBeNull(); + }); + + test("ignores missing and malformed persistent attachments", () => { + const { ctx } = createFakeContext(); const socket = createFakeSocket(); - registry.acceptDriverSocket(socket as unknown as WebSocket); + const registry = new DriverInstanceSocketRegistry(ctx); + ctx.acceptWebSocket(socket, ["driver"]); - socket.readyState = SOCKET_CLOSED; + expect(registry.getDriverSocket(EPOCH_A)).toBeNull(); + socket.attachment = { connectionId: "connection-a", generation: -1 }; + expect(registry.getDriverSocket(EPOCH_A)).toBeNull(); + }); + + test("an old callback resumed after a successor can only close its own socket", async () => { + const { ctx } = createFakeContext(); + const registry = new DriverInstanceSocketRegistry(ctx); + const first = createFakeSocket(); + registry.acceptDriverSocket(first as unknown as WebSocket, EPOCH_A); + const capturedEpoch = registry.getSocketEpoch(first as unknown as WebSocket); + let resume: () => void = () => {}; + const gate = new Promise((resolve) => { + resume = resolve; + }); + const staleCallback = (async () => { + await gate; + + if ( + capturedEpoch !== null && + !registry.isCurrentDriverSocket(first as unknown as WebSocket, capturedEpoch, EPOCH_B) + ) { + first.close(1000, "runtime.socket.superseded"); + } + })(); + + const second = createFakeSocket(); + registry.acceptDriverSocket(second as unknown as WebSocket, EPOCH_B); + resume(); + await staleCallback; - expect(registry.isSupersededDriverSocket(socket as unknown as WebSocket)).toBe(false); + expect(first.closes).toEqual([{ code: 1000, reason: "runtime.socket.superseded" }]); + expect(second.closes).toEqual([]); + expect(registry.getDriverSocket(EPOCH_B)).toBe(second); }); }); diff --git a/apps/api/tests/driver-llm-proxy-route.test.ts b/apps/api/tests/driver-llm-proxy-route.test.ts index edefdb30..913f9401 100644 --- a/apps/api/tests/driver-llm-proxy-route.test.ts +++ b/apps/api/tests/driver-llm-proxy-route.test.ts @@ -1,5 +1,6 @@ import { afterEach, describe, expect, test } from "bun:test"; +import { DRIVER_PROTOCOL_VERSION } from "@mosoo/agent-driver/boot"; import { driverInstancesTable, vendorCredentialsTable } from "@mosoo/db"; import { parsePlatformId } from "@mosoo/id"; import type { DriverInstanceId, AppId, VendorCredentialId } from "@mosoo/id"; @@ -61,7 +62,7 @@ function createDriverRouteTestApp(): Hono { function captureUpstreamFetch(response?: () => Response): CapturedUpstreamRequest[] { const captured: CapturedUpstreamRequest[] = []; - globalThis.fetch = (async (input: RequestInfo | URL, init?: RequestInit) => { + globalThis.fetch = async (input: RequestInfo | URL, init?: RequestInit) => { const request = new Request(input, init); captured.push({ body: request.method === "GET" || request.method === "HEAD" ? null : await request.text(), @@ -83,7 +84,7 @@ function captureUpstreamFetch(response?: () => Response): CapturedUpstreamReques status: 200, }) ); - }) as typeof fetch; + }; return captured; } @@ -93,9 +94,11 @@ async function insertDriverInstance( status: "provisioning" | "connecting" | "ready" | "failed", input: { bootTokenExpiresAt?: number; + bootTokenHash?: Uint8Array; driverInstanceId?: DriverInstanceId; generation?: number; lastHeartbeatAt?: number | null; + sandboxIncarnation?: number; updatedAt?: number; } = {}, ) { @@ -105,7 +108,7 @@ async function insertDriverInstance( .insert(driverInstancesTable) .values({ bootTokenExpiresAt: input.bootTokenExpiresAt ?? nowMs + 60_000, - bootTokenHash: new Uint8Array([1, 2, 3]), + bootTokenHash: input.bootTokenHash ?? new Uint8Array([1, 2, 3]), bootTokenUsedAt: null, closeCode: null, closeReason: null, @@ -122,9 +125,10 @@ async function insertDriverInstance( lastHeartbeatAt: input.lastHeartbeatAt ?? null, processId: null, protocol: "orpc-ws", - protocolVersion: 2, + protocolVersion: DRIVER_PROTOCOL_VERSION, runtime: "claude-agent-sdk", sandboxId: PUBLIC_API_TEST_IDS.sandbox, + sandboxIncarnation: input.sandboxIncarnation ?? 1, sandboxSessionId: PUBLIC_API_TEST_IDS.ownerSession, status, statusChangedAt: nowMs, @@ -580,7 +584,9 @@ describe("driver LLM proxy route", () => { const { bindings, database } = await setupFixture(); await insertDriverInstance(database, "provisioning", { bootTokenExpiresAt: Date.now() - 1, + bootTokenHash: new Uint8Array([4, 5, 6]), driverInstanceId: OTHER_DRIVER_INSTANCE_ID, + sandboxIncarnation: 2, }); const response = await dispatch(bindings, llmProxyRequest("/v1/messages", { method: "POST" })); @@ -856,9 +862,9 @@ describe("driver LLM proxy route", () => { test("maps upstream failures to 502", async () => { const { bindings } = await setupFixture(); - globalThis.fetch = (async () => { + globalThis.fetch = async () => { throw new Error("boom"); - }) as typeof fetch; + }; const grant = await createLlmProxyGrant(bindings); const response = await dispatch( diff --git a/apps/api/tests/driver-log-pre-hello-buffering.test.ts b/apps/api/tests/driver-log-pre-hello-buffering.test.ts index 48e76257..9e597475 100644 --- a/apps/api/tests/driver-log-pre-hello-buffering.test.ts +++ b/apps/api/tests/driver-log-pre-hello-buffering.test.ts @@ -41,7 +41,7 @@ function createBatch(message: string): DriverLogBatchInput { timestamp: new Date(0).toISOString(), }, ], - } as DriverLogBatchInput; + }; } const activeContext = { diff --git a/apps/api/tests/driver-session-link.test.ts b/apps/api/tests/driver-session-link.test.ts index f179ae5a..c34ac01d 100644 --- a/apps/api/tests/driver-session-link.test.ts +++ b/apps/api/tests/driver-session-link.test.ts @@ -27,12 +27,15 @@ function createDriverSessionLinkDatabase(): SqliteD1Database { ); CREATE TABLE session_run ( + created_at integer DEFAULT 0 NOT NULL, created_by_account_id text, driver_instance_id text, id text PRIMARY KEY NOT NULL, + runtime_id text, session_id text NOT NULL, status text NOT NULL, - trace_id text + trace_id text, + updated_at integer DEFAULT 0 NOT NULL ); CREATE TABLE session ( @@ -40,6 +43,7 @@ function createDriverSessionLinkDatabase(): SqliteD1Database { app_id text, creator_account_id text NOT NULL, id text PRIMARY KEY NOT NULL, + runtime_id text, type text DEFAULT 'preview' NOT NULL ); @@ -101,12 +105,14 @@ describe("driver runtime session link", () => { expect( runtimeSessionLinkNeedsRefresh({ agentId: AGENT_ID, + appId: null, callerId: CALLER_FROM_ORIGIN_ID, creatorId: CREATOR_ID, executionOwnerId: EXECUTION_OWNER_FROM_ORIGIN_ID, sandboxId: SANDBOX_ID, sandboxKind: "cattle", sandboxSubjectKind: "session", + runtimeId: null, sessionId: SESSION_ID, sessionRunId: null, sessionRunStatus: null, diff --git a/apps/api/tests/driver-session-ready.test.ts b/apps/api/tests/driver-session-ready.test.ts index 37bb8ef6..22fb02c9 100644 --- a/apps/api/tests/driver-session-ready.test.ts +++ b/apps/api/tests/driver-session-ready.test.ts @@ -25,6 +25,7 @@ type ProvisionSessionDriverInput = { }; type ProvisionSessionDriverResult = { + driverGeneration: number; driverInstanceId: string; process: RuntimeProcessHandle; sandboxId: string; @@ -60,6 +61,17 @@ const SANDBOX_ID = PLATFORM_ID_FIXTURES.sandbox; const SESSION_ID = PLATFORM_ID_FIXTURES.session; const SESSION_RUN_ID = PLATFORM_ID_FIXTURES.sessionRun; const SANDBOX_SESSION_ID = SESSION_ID as unknown as SandboxSessionId; +const SANDBOX_INCARNATION = 1; + +const RUNTIME_PROVISIONING_LEASE = { + heartbeatAt: 1, + operationId: PLATFORM_ID_FIXTURES.runtimeOperation, + runId: SESSION_RUN_ID, + sandboxId: SANDBOX_ID, + sandboxIncarnation: SANDBOX_INCARNATION, + sandboxSessionId: SANDBOX_SESSION_ID, + sessionId: SESSION_ID, +} as const; const PROFILE: DriverProfileConfig = { agentId: AGENT_ID, @@ -112,10 +124,13 @@ function createDriverSessionDatabase(): SqliteD1Database { database.execute(` CREATE TABLE driver_instance ( command_seq_cursor integer DEFAULT 0 NOT NULL, + generation integer DEFAULT 0 NOT NULL, id text PRIMARY KEY NOT NULL, sandbox_id text NOT NULL, + sandbox_incarnation integer NOT NULL, sandbox_session_id text NOT NULL, status text NOT NULL, + status_operation_id text, updated_at integer NOT NULL ); @@ -123,6 +138,7 @@ function createDriverSessionDatabase(): SqliteD1Database { acked_at integer, completed_at integer, delivery_connection_id text, + driver_generation integer, driver_instance_id text NOT NULL, error_json text, expires_at integer, @@ -136,14 +152,20 @@ function createDriverSessionDatabase(): SqliteD1Database { ); CREATE TABLE sandbox ( + claim_owner text, id text PRIMARY KEY NOT NULL, inactive_deadline_at integer, + incarnation integer NOT NULL, kind text NOT NULL, + operation_kind text, + status text NOT NULL, + status_operation_id text, updated_at integer NOT NULL ); CREATE TABLE sandbox_session ( sandbox_id text NOT NULL, + sandbox_incarnation integer NOT NULL, session_id text PRIMARY KEY NOT NULL, status text NOT NULL ); @@ -157,20 +179,28 @@ function createDriverSessionDatabase(): SqliteD1Database { updated_at integer NOT NULL ); - INSERT INTO sandbox (id, inactive_deadline_at, kind, updated_at) - VALUES ('${SANDBOX_ID}', 1, 'pet', 1); + INSERT INTO sandbox (id, inactive_deadline_at, incarnation, kind, status, updated_at) + VALUES ('${SANDBOX_ID}', 1, ${SANDBOX_INCARNATION}, 'pet', 'active', 1); - INSERT INTO sandbox_session (sandbox_id, session_id, status) - VALUES ('${SANDBOX_ID}', '${SESSION_ID}', 'active'); + INSERT INTO sandbox_session (sandbox_id, sandbox_incarnation, session_id, status) + VALUES ('${SANDBOX_ID}', ${SANDBOX_INCARNATION}, '${SESSION_ID}', 'active'); INSERT INTO driver_instance ( id, sandbox_id, + sandbox_incarnation, sandbox_session_id, status, updated_at ) - VALUES ('${DRIVER_INSTANCE_ID}', '${SANDBOX_ID}', '${SESSION_ID}', 'provisioning', 1); + VALUES ( + '${DRIVER_INSTANCE_ID}', + '${SANDBOX_ID}', + ${SANDBOX_INCARNATION}, + '${SESSION_ID}', + 'provisioning', + 1 + ); INSERT INTO session_run (id, session_id, status, status_seq, updated_at) VALUES ('${SESSION_RUN_ID}', '${SESSION_ID}', 'running', 0, 1); @@ -228,12 +258,15 @@ describe("driver session readiness", () => { const bindings = createBindings(database, requests); const driver = await ensureDriverSessionReady(bindings, "https://api.test/runtime", { + builtInTools: [], cloudflareSession: {} as ExecutionSessionHandle, profile: PROFILE, resolvedMcpServers: [], resolvedSkillCatalog: [], resolvedSkills: [], + runtimeProvisioningLease: RUNTIME_PROVISIONING_LEASE, sandbox: {} as SandboxHandle, + sandboxIncarnation: SANDBOX_INCARNATION, sandboxSessionId: SESSION_ID, sessionId: SESSION_ID, sessionRunId: SESSION_RUN_ID, @@ -271,17 +304,24 @@ describe("driver session readiness", () => { INSERT INTO driver_instance ( id, sandbox_id, + sandbox_incarnation, sandbox_session_id, status, updated_at ) - VALUES (?, ?, ?, 'connecting', 2) + VALUES (?, ?, ?, ?, 'connecting', 2) `, ) - .bind(input.driverInstanceId, input.profile.sandbox.id, input.sandboxSessionId) + .bind( + input.driverInstanceId, + input.profile.sandbox.id, + SANDBOX_INCARNATION, + input.sandboxSessionId, + ) .run(); return { + driverGeneration: 0, driverInstanceId: input.driverInstanceId, process: createHangingProcess(), sandboxId: input.profile.sandbox.id, @@ -299,12 +339,15 @@ describe("driver session readiness", () => { }); const driver = await ensureDriverSessionReady(bindings, "https://api.test/runtime", { + builtInTools: [], cloudflareSession: {} as ExecutionSessionHandle, profile: PROFILE, resolvedMcpServers: [], resolvedSkillCatalog: [], resolvedSkills: [], + runtimeProvisioningLease: RUNTIME_PROVISIONING_LEASE, sandbox: {} as SandboxHandle, + sandboxIncarnation: SANDBOX_INCARNATION, sandboxSessionId: SESSION_ID, sessionId: SESSION_ID, sessionRunId: SESSION_RUN_ID, @@ -335,12 +378,15 @@ describe("driver session readiness", () => { const requests = { count: 0 }; const bindings = createBindings(database, requests); const driver = await ensureDriverSessionReady(bindings, "https://api.test/runtime", { + builtInTools: [], cloudflareSession: {} as ExecutionSessionHandle, profile: PROFILE, resolvedMcpServers: [], resolvedSkillCatalog: [], resolvedSkills: [], + runtimeProvisioningLease: RUNTIME_PROVISIONING_LEASE, sandbox: {} as SandboxHandle, + sandboxIncarnation: SANDBOX_INCARNATION, sandboxSessionId: SESSION_ID, sessionId: SESSION_ID, sessionRunId: SESSION_RUN_ID, @@ -349,6 +395,7 @@ describe("driver session readiness", () => { await dispatchDriverTurn(bindings, { attachmentIds: [], + driverGeneration: driver.driverGeneration, driverInstanceId: driver.driverInstanceId, prompt: "hello", sessionRunId: SESSION_RUN_ID, diff --git a/apps/api/tests/driver-session-stop.test.ts b/apps/api/tests/driver-session-stop.test.ts index 3071c2aa..c0964d30 100644 --- a/apps/api/tests/driver-session-stop.test.ts +++ b/apps/api/tests/driver-session-stop.test.ts @@ -1,6 +1,11 @@ import { describe, expect, test } from "bun:test"; -import { stopDriverSession } from "../src/modules/runtime/infrastructure/driver-session-stop.service"; +import type { DriverInstanceId, SessionRunId } from "@mosoo/id"; + +import { + repairClaimedDriverStopsGlobally, + stopDriverSession, +} from "../src/modules/runtime/infrastructure/driver-session-stop.service"; import type { ApiBindings } from "../src/platform/cloudflare/worker-types"; import { SqliteD1Database } from "./helpers/sqlite-d1"; @@ -10,9 +15,15 @@ function createDriverStopDatabase(): SqliteD1Database { database.execute(` CREATE TABLE driver_instance ( id text PRIMARY KEY NOT NULL, + generation integer NOT NULL, sandbox_id text NOT NULL, sandbox_session_id text NOT NULL, status text NOT NULL, + status_changed_at integer DEFAULT 0 NOT NULL, + status_event text DEFAULT 'driver.provision' NOT NULL, + status_operation_id text, + status_seq integer DEFAULT 0 NOT NULL, + status_source text DEFAULT 'system' NOT NULL, updated_at integer NOT NULL ); @@ -120,33 +131,25 @@ function createDriverStopDatabase(): SqliteD1Database { INSERT INTO driver_instance ( id, + generation, sandbox_id, sandbox_session_id, status, updated_at ) - VALUES ('driver-1', '01J0000000000000000000000D', 'sandbox-session-1', 'failed', 1); + VALUES ('driver-1', 0, '01J0000000000000000000000D', 'sandbox-session-1', 'failed', 1); `); return database; } describe("driver session stop", () => { - test("releases linked runs and records the terminal status", async () => { + test("releases the linked lease without competing with Driver terminal events", async () => { const database = createDriverStopDatabase(); await stopDriverSession({ DB: database } as ApiBindings, { driverInstanceId: "driver-1", reason: "test.stop", - terminalRun: { - error: { - code: "agent.runtime_state_operation", - details: {}, - message: "Stopped by runtime operation.", - retryable: false, - }, - status: "cancelled", - }, }); const run = await database @@ -154,20 +157,205 @@ describe("driver session stop", () => { .bind("run-1") .first<{ error_code: string | null; status: string }>(); expect(run).toEqual({ - error_code: "agent.runtime_state_operation", - status: "cancelled", + error_code: null, + status: "running", }); const session = await database .prepare("SELECT status FROM session WHERE id = ?") .bind("session-1") .first<{ status: string }>(); - expect(session).toEqual({ status: "IDLE" }); + expect(session).toEqual({ status: "RUNNING" }); const runLink = await database .prepare("SELECT driver_instance_id FROM session_run WHERE id = ?") .bind("run-1") .first<{ driver_instance_id: string | null }>(); - expect(runLink).toEqual({ driver_instance_id: "driver-1" }); + expect(runLink).toEqual({ driver_instance_id: null }); + }); + + for (const preclaimed of [false, true]) { + test(`claims the exact Driver and Run before sending the unscoped stop command${ + preclaimed ? " from its terminal owner" : "" + }`, async () => { + const database = createDriverStopDatabase(); + database.execute(` + UPDATE driver_instance + SET status = 'ready', status_operation_id = ${preclaimed ? "'run-1'" : "NULL"} + WHERE id = 'driver-1' + `); + const observedClaims: unknown[] = []; + const bindings = { + DB: database, + DriverConnection: { + get: () => ({ + fetch: async (request: Request) => { + const path = new URL(request.url).pathname; + if (path === "/control/send") { + observedClaims.push( + await database + .prepare( + "SELECT status, status_operation_id FROM driver_instance WHERE id = 'driver-1'", + ) + .first(), + ); + return Response.json({ ok: true }); + } + if (path === "/wait/close") { + database.execute( + "UPDATE driver_instance SET status = 'stopped' WHERE id = 'driver-1'", + ); + return Response.json({ close: null, terminalized: true }); + } + throw new Error(`Unexpected Driver control request: ${path}`); + }, + }), + idFromName: () => "driver-do-id", + }, + } as unknown as ApiBindings; + + await stopDriverSession(bindings, { + driverInstanceId: "driver-1" as DriverInstanceId, + expectedDriverGeneration: 0, + expectedSessionRunId: "run-1" as SessionRunId, + reason: "test.claimed-stop", + }); + + expect(observedClaims).toEqual([ + { + status: "stopping", + status_operation_id: expect.any(String), + }, + ]); + await expect( + database + .prepare("SELECT status, status_operation_id FROM driver_instance WHERE id = 'driver-1'") + .first(), + ).resolves.toEqual({ status: "stopped", status_operation_id: null }); + await expect( + database.prepare("SELECT driver_instance_id FROM session_run WHERE id = 'run-1'").first(), + ).resolves.toEqual({ driver_instance_id: null }); + }); + } + + test("does not send a stale stop after a successor Run wins on the same Driver generation", async () => { + const database = createDriverStopDatabase(); + database.execute("UPDATE driver_instance SET status = 'ready' WHERE id = 'driver-1'"); + const originalPrepare = database.prepare.bind(database); + let injectSuccessor = true; + database.prepare = ((query: string) => { + const wrap = (statement: D1PreparedStatement): D1PreparedStatement => + new Proxy(statement, { + get(target, property, receiver) { + if (property === "bind") { + return (...values: unknown[]) => wrap(target.bind(...values)); + } + if ( + property === "all" || + property === "first" || + property === "raw" || + property === "run" + ) { + return async (...args: unknown[]) => { + if (injectSuccessor) { + injectSuccessor = false; + database.execute(` + UPDATE session_run SET status = 'completed' WHERE id = 'run-1'; + INSERT INTO session_run ( + id, driver_instance_id, session_id, agent_id, created_at, + created_by_account_id, status, trace_id, trigger, updated_at + ) VALUES ( + 'run-2', 'driver-1', 'session-1', '01J00000000000000000000009', 2, + 'account-1', 'running', 'trace-2', 'resume', 2 + ); + UPDATE session SET last_run_id = 'run-2', updated_at = 2 WHERE id = 'session-1'; + `); + } + return Reflect.apply(target[property], target, args); + }; + } + return Reflect.get(target, property, receiver); + }, + }); + + return /update\s+(?:"|`)?driver_instance(?:"|`)?/i.test(query) + ? wrap(originalPrepare(query)) + : originalPrepare(query); + }) as typeof database.prepare; + let controlRequests = 0; + const bindings = { + DB: database, + DriverConnection: { + get: () => ({ + fetch: async () => { + controlRequests += 1; + return Response.json({ ok: true }); + }, + }), + idFromName: () => "driver-do-id", + }, + } as unknown as ApiBindings; + + await expect( + stopDriverSession(bindings, { + driverInstanceId: "driver-1" as DriverInstanceId, + expectedDriverGeneration: 0, + expectedSessionRunId: "run-1" as SessionRunId, + reason: "test.stale-stop", + }), + ).rejects.toThrow("exact Driver and Session Run ownership"); + + expect(controlRequests).toBe(0); + await expect( + database + .prepare("SELECT status, status_operation_id FROM driver_instance WHERE id = 'driver-1'") + .first(), + ).resolves.toEqual({ status: "ready", status_operation_id: null }); + await expect( + database + .prepare("SELECT driver_instance_id, status FROM session_run WHERE id = 'run-2'") + .first(), + ).resolves.toEqual({ driver_instance_id: "driver-1", status: "running" }); + }); + + test("maintenance resumes a stop that crashed after its durable claim", async () => { + const database = createDriverStopDatabase(); + const operationId = "01J0000000000000000000000X"; + database.execute(` + UPDATE driver_instance + SET status = 'stopping', status_operation_id = '${operationId}' + WHERE id = 'driver-1' + `); + const requests: string[] = []; + const bindings = { + DB: database, + DriverConnection: { + get: () => ({ + fetch: async (request: Request) => { + const path = new URL(request.url).pathname; + requests.push(path); + if (path === "/control/fail") { + database.execute( + "UPDATE driver_instance SET status = 'failed' WHERE id = 'driver-1'", + ); + } + return Response.json({ ok: true }); + }, + }), + idFromName: () => "driver-do-id", + }, + } as unknown as ApiBindings; + + await repairClaimedDriverStopsGlobally(bindings); + + expect(requests).toEqual(["/control/fail", "/wait/close"]); + await expect( + database + .prepare("SELECT status_operation_id FROM driver_instance WHERE id = 'driver-1'") + .first(), + ).resolves.toEqual({ status_operation_id: null }); + await expect( + database.prepare("SELECT driver_instance_id FROM session_run WHERE id = 'run-1'").first(), + ).resolves.toEqual({ driver_instance_id: null }); }); }); diff --git a/apps/api/tests/driver-skill-package-route.test.ts b/apps/api/tests/driver-skill-package-route.test.ts index 0925503f..e7b6f9ff 100644 --- a/apps/api/tests/driver-skill-package-route.test.ts +++ b/apps/api/tests/driver-skill-package-route.test.ts @@ -1,5 +1,6 @@ import { describe, expect, test } from "bun:test"; +import { DRIVER_PROTOCOL_VERSION } from "@mosoo/agent-driver/boot"; import { driverInstancesTable, skillSnapshotsTable } from "@mosoo/db"; import { Hono } from "hono"; @@ -111,9 +112,10 @@ async function insertDriverInstance( lastHeartbeatAt: null, processId: null, protocol: "orpc-ws", - protocolVersion: 2, + protocolVersion: DRIVER_PROTOCOL_VERSION, runtime: "openai-runtime", sandboxId: PUBLIC_API_TEST_IDS.sandbox, + sandboxIncarnation: 1, sandboxSessionId: PUBLIC_API_TEST_IDS.ownerSession, status, statusChangedAt: nowMs, @@ -144,7 +146,7 @@ describe("driver skill package route", () => { const database = await createPublicHttpContractDatabase(); const bucket = new PublicApiMemoryFileBucket(); const bindings = createPublicHttpTestBindings(database, { - fileBucket: bucket as unknown as R2Bucket, + fileBucket: bucket, }) as ApiBindings; ensureSkillRouteTables(database); @@ -168,7 +170,7 @@ describe("driver skill package route", () => { const database = await createPublicHttpContractDatabase(); const bucket = new PublicApiMemoryFileBucket(); const bindings = createPublicHttpTestBindings(database, { - fileBucket: bucket as unknown as R2Bucket, + fileBucket: bucket, }) as ApiBindings; ensureSkillRouteTables(database); diff --git a/apps/api/tests/driver-terminal-state-coordinator.test.ts b/apps/api/tests/driver-terminal-state-coordinator.test.ts new file mode 100644 index 00000000..4c728190 --- /dev/null +++ b/apps/api/tests/driver-terminal-state-coordinator.test.ts @@ -0,0 +1,479 @@ +import { describe, expect, test } from "bun:test"; + +import { PLATFORM_ID_FIXTURES } from "@mosoo/id/testing"; + +import { finalizeDriverInstance } from "../src/modules/runtime/infrastructure/driver-instance/lifecycle"; +import type { RuntimeSessionViewCache } from "../src/modules/runtime/infrastructure/driver-instance/runtime-session-view-cache"; +import { DriverInstanceRuntimeState } from "../src/modules/runtime/infrastructure/driver-instance/runtime-state"; +import { DRIVER_INSTANCE_STATE_STORAGE_KEY } from "../src/modules/runtime/infrastructure/driver-instance/runtime-state-store"; +import type { DriverInstanceStoredState } from "../src/modules/runtime/infrastructure/driver-instance/runtime-state-store"; +import type { SessionViewerEventDeliveryBuffer } from "../src/modules/runtime/infrastructure/driver-instance/session-viewer-event-delivery-buffer"; +import type { repairFinalizedTerminalDriverRunState } from "../src/modules/runtime/infrastructure/driver-instance/terminal-run-release"; +import { DriverInstanceTerminalStateCoordinator } from "../src/modules/runtime/infrastructure/driver-instance/terminal-state-coordinator"; +import type { ApiBindings } from "../src/platform/cloudflare/worker-types"; +import { SqliteD1Database } from "./helpers/sqlite-d1"; + +const DRIVER_INSTANCE_ID = PLATFORM_ID_FIXTURES.driverInstance; +const TERMINAL_REPAIR_RESULT = { link: null, released: false } as const; + +class MemoryDriverInstanceStorage { + cleanupWriteFailures = 0; + readonly values = new Map(); + + async deleteAll(): Promise { + this.values.clear(); + } + + async get(key: string): Promise { + return structuredClone(this.values.get(key)) as T | undefined; + } + + async put(key: string, value: unknown): Promise { + if ( + this.cleanupWriteFailures > 0 && + typeof value === "object" && + value !== null && + "terminalCleanupComplete" in value && + value.terminalCleanupComplete === true + ) { + this.cleanupWriteFailures -= 1; + throw new Error("terminal snapshot write failed"); + } + + this.values.set(key, structuredClone(value)); + } + + storedState(): DriverInstanceStoredState { + const value = this.values.get(DRIVER_INSTANCE_STATE_STORAGE_KEY); + + if (value === undefined) { + throw new Error("Driver instance state was not persisted."); + } + + return value as DriverInstanceStoredState; + } +} + +type FinalizeDriver = typeof finalizeDriverInstance; +type RepairFinalizedRunState = typeof repairFinalizedTerminalDriverRunState; + +async function createConnectedState( + storage: MemoryDriverInstanceStorage, +): Promise { + const state = new DriverInstanceRuntimeState({ storage }); + await state.load(); + + if (state.driverInstanceId === null) { + await state.initializeDriverInstance(DRIVER_INSTANCE_ID, 1); + await state.recordAcceptedConnection({ + connectedAt: 1, + connectionId: "connection-1", + driverGeneration: 1, + traceId: null, + }); + } + + state.setRuntimeSessionLink({ + agentId: null, + appId: null, + callerId: null, + creatorId: null, + executionOwnerId: null, + sandboxId: null, + sandboxKind: null, + sandboxSubjectKind: null, + runtimeId: null, + sessionId: PLATFORM_ID_FIXTURES.session, + sessionRunId: PLATFORM_ID_FIXTURES.sessionRun, + sessionRunStatus: "running", + sessionType: null, + traceId: null, + }); + + return state; +} + +function createCoordinator( + storage: MemoryDriverInstanceStorage, + state: DriverInstanceRuntimeState, + input: { + finalizeDriver: FinalizeDriver; + flush?: () => Promise; + repairFinalizedRunState: RepairFinalizedRunState; + requestStateSync?: (sessionId: string | null) => void; + }, +): DriverInstanceTerminalStateCoordinator { + return new DriverInstanceTerminalStateCoordinator({ + clearStorage: async () => storage.deleteAll(), + env: {} as ApiBindings, + finalizeDriver: input.finalizeDriver, + repairFinalizedRunState: input.repairFinalizedRunState, + state, + viewCache: { reset: () => {} } as unknown as RuntimeSessionViewCache, + viewerEventDelivery: { + flush: input.flush ?? (async () => {}), + requestStateSync: input.requestStateSync ?? (() => {}), + resetAfterFlush: () => {}, + } as unknown as SessionViewerEventDeliveryBuffer, + withRuntimeLogContext: (fn) => fn(), + }); +} + +function createDriverInstanceDatabase(status: "failed" | "ready"): SqliteD1Database { + const database = new SqliteD1Database(); + database.execute(` + CREATE TABLE driver_instance ( + close_code integer, + close_reason text, + connection_id text, + driver_pid integer, + driver_started_at integer, + error_message text, + expires_at integer NOT NULL, + generation integer NOT NULL, + heartbeat_count integer NOT NULL, + id text PRIMARY KEY NOT NULL, + last_heartbeat_at integer, + status text NOT NULL, + status_changed_at integer NOT NULL, + status_event text NOT NULL, + status_seq integer NOT NULL, + status_source text NOT NULL, + updated_at integer NOT NULL + ); + INSERT INTO driver_instance ( + connection_id, expires_at, generation, heartbeat_count, id, status, + status_changed_at, status_event, status_seq, status_source, updated_at + ) VALUES ( + 'connection-1', 1, 1, 0, '${DRIVER_INSTANCE_ID}', '${status}', + 1, 'driver.${status}', 1, 'driver', 1 + ); + `); + return database; +} + +describe("driver terminal state coordinator", () => { + test("joins concurrent finalization callers onto one in-flight transition", async () => { + const storage = new MemoryDriverInstanceStorage(); + const state = await createConnectedState(storage); + let finalizeCalls = 0; + let markStarted: () => void = () => {}; + let releaseFinalization: () => void = () => {}; + const started = new Promise((resolve) => { + markStarted = resolve; + }); + const finalizationGate = new Promise((resolve) => { + releaseFinalization = resolve; + }); + const coordinator = createCoordinator(storage, state, { + finalizeDriver: async () => { + finalizeCalls += 1; + markStarted(); + await finalizationGate; + return "stopped"; + }, + repairFinalizedRunState: async () => TERMINAL_REPAIR_RESULT, + }); + + const epoch = state.requireConnectionEpoch(); + const first = coordinator.finalize(epoch); + const second = coordinator.finalize(epoch); + await started; + + expect(finalizeCalls).toBe(1); + releaseFinalization(); + await expect(Promise.all([first, second])).resolves.toEqual([undefined, undefined]); + expect(state.terminalCleanupComplete).toBe(true); + }); + + test("an old finalizer cannot settle a successor connection after its await", async () => { + const storage = new MemoryDriverInstanceStorage(); + const state = await createConnectedState(storage); + const oldEpoch = state.requireConnectionEpoch(); + let markStarted: () => void = () => {}; + let resume: () => void = () => {}; + const started = new Promise((resolve) => { + markStarted = resolve; + }); + const gate = new Promise((resolve) => { + resume = resolve; + }); + let repairCalls = 0; + const coordinator = createCoordinator(storage, state, { + finalizeDriver: async () => { + markStarted(); + await gate; + return "stopped"; + }, + repairFinalizedRunState: async () => { + repairCalls += 1; + return TERMINAL_REPAIR_RESULT; + }, + }); + + const oldFinalizer = coordinator.finalize(oldEpoch); + await started; + state.connectionId = "connection-2"; + state.close = null; + state.terminalized = false; + const successorWait = state.waitForReady(1, 10_000).catch((error: unknown) => error); + resume(); + await oldFinalizer; + + expect(repairCalls).toBe(0); + expect(state.connectionId).toBe("connection-2"); + expect(state.close).toBeNull(); + expect(state.terminalCleanupComplete).toBe(false); + expect(state.readyWaiters).toHaveLength(1); + state.resetAfterDestroy("test cleanup"); + await successorWait; + }); + + test("retries driver finalization in the same object after close intent is durable", async () => { + const storage = new MemoryDriverInstanceStorage(); + const state = await createConnectedState(storage); + let finalizeCalls = 0; + let repairCalls = 0; + const coordinator = createCoordinator(storage, state, { + finalizeDriver: async () => { + finalizeCalls += 1; + + if (finalizeCalls === 1) { + throw new Error("driver finalization failed"); + } + + return "stopped"; + }, + repairFinalizedRunState: async () => { + repairCalls += 1; + return TERMINAL_REPAIR_RESULT; + }, + }); + + const epoch = state.requireConnectionEpoch(); + await expect(coordinator.finalize(epoch)).rejects.toThrow("driver finalization failed"); + expect(state.close).not.toBeNull(); + expect(state.terminalized).toBe(true); + expect(state.terminalCleanupComplete).toBe(false); + + await expect(coordinator.finalize(epoch)).resolves.toBeUndefined(); + expect(finalizeCalls).toBe(2); + expect(repairCalls).toBe(1); + expect(state.terminalCleanupComplete).toBe(true); + }); + + test("finalizes without waiting for derived viewer event delivery", async () => { + const storage = new MemoryDriverInstanceStorage(); + const state = await createConnectedState(storage); + let flushCalls = 0; + let finalizeCalls = 0; + const syncSessionIds: Array = []; + const coordinator = createCoordinator(storage, state, { + finalizeDriver: async () => { + finalizeCalls += 1; + return "stopped"; + }, + flush: async () => { + flushCalls += 1; + }, + repairFinalizedRunState: async () => TERMINAL_REPAIR_RESULT, + requestStateSync: (sessionId) => syncSessionIds.push(sessionId), + }); + + await expect(coordinator.finalize(state.requireConnectionEpoch())).resolves.toBeUndefined(); + expect(flushCalls).toBe(0); + expect(finalizeCalls).toBe(1); + expect(syncSessionIds).toEqual([PLATFORM_ID_FIXTURES.session]); + expect(state.terminalCleanupComplete).toBe(true); + }); + + test("restarts repair after the terminal driver CAS was already committed", async () => { + const storage = new MemoryDriverInstanceStorage(); + const firstState = await createConnectedState(storage); + let casCommitted = false; + let casWrites = 0; + let finalizeCalls = 0; + let repairCalls = 0; + const finalizeDriver: FinalizeDriver = async () => { + finalizeCalls += 1; + + if (!casCommitted) { + casCommitted = true; + casWrites += 1; + } + + return "stopped"; + }; + const repairFinalizedRunState: RepairFinalizedRunState = async () => { + repairCalls += 1; + + if (repairCalls === 1) { + throw new Error("run repair failed"); + } + + return TERMINAL_REPAIR_RESULT; + }; + const firstCoordinator = createCoordinator(storage, firstState, { + finalizeDriver, + repairFinalizedRunState, + }); + + await expect(firstCoordinator.finalize(firstState.requireConnectionEpoch())).rejects.toThrow( + "run repair failed", + ); + expect(firstState.terminalCleanupComplete).toBe(false); + expect(casWrites).toBe(1); + + const restartedState = await createConnectedState(storage); + expect(restartedState.terminalized).toBe(true); + expect(restartedState.terminalCleanupComplete).toBe(false); + await expect( + createCoordinator(storage, restartedState, { + finalizeDriver, + repairFinalizedRunState, + }).finalize(restartedState.requireConnectionEpoch()), + ).resolves.toBeUndefined(); + + expect(casWrites).toBe(1); + expect(finalizeCalls).toBe(2); + expect(repairCalls).toBe(2); + expect(storage.storedState().terminalCleanupComplete).toBe(true); + + const completedState = await createConnectedState(storage); + await expect( + createCoordinator(storage, completedState, { + finalizeDriver: async () => { + throw new Error("completed finalization must not run again"); + }, + repairFinalizedRunState, + }).finalize(completedState.requireConnectionEpoch()), + ).resolves.toBeUndefined(); + }); + + test("restarts after terminal snapshot persistence fails without duplicating side effects", async () => { + const storage = new MemoryDriverInstanceStorage(); + const firstState = await createConnectedState(storage); + storage.cleanupWriteFailures = 1; + let finalizeCalls = 0; + let finalizeWrites = 0; + let finalized = false; + let repairCalls = 0; + let repairWrites = 0; + let repaired = false; + const finalizeDriver: FinalizeDriver = async () => { + finalizeCalls += 1; + + if (!finalized) { + finalized = true; + finalizeWrites += 1; + } + + return "stopped"; + }; + const repairFinalizedRunState: RepairFinalizedRunState = async () => { + repairCalls += 1; + + if (!repaired) { + repaired = true; + repairWrites += 1; + } + + return TERMINAL_REPAIR_RESULT; + }; + const firstCoordinator = createCoordinator(storage, firstState, { + finalizeDriver, + repairFinalizedRunState, + }); + + await expect(firstCoordinator.finalize(firstState.requireConnectionEpoch())).rejects.toThrow( + "terminal snapshot write failed", + ); + expect(firstState.terminalCleanupComplete).toBe(false); + expect(storage.storedState().terminalCleanupComplete).toBe(false); + + const restartedState = await createConnectedState(storage); + await expect( + createCoordinator(storage, restartedState, { + finalizeDriver, + repairFinalizedRunState, + }).finalize(restartedState.requireConnectionEpoch()), + ).resolves.toBeUndefined(); + + expect(finalizeCalls).toBe(2); + expect(finalizeWrites).toBe(1); + expect(repairCalls).toBe(2); + expect(repairWrites).toBe(1); + expect(restartedState.terminalCleanupComplete).toBe(true); + }); + + test("repairs the canonical failed state when maintenance wins a clean-close race", async () => { + const storage = new MemoryDriverInstanceStorage(); + const state = await createConnectedState(storage); + let repairedStatus: "failed" | "stopped" | null = null; + const coordinator = createCoordinator(storage, state, { + finalizeDriver: async () => "failed", + repairFinalizedRunState: async (_bindings, input) => { + repairedStatus = input.status; + return TERMINAL_REPAIR_RESULT; + }, + }); + + await coordinator.finalize(state.requireConnectionEpoch()); + + expect(state.close?.code).toBe(1000); + expect(repairedStatus).toBe("failed"); + expect(state.terminalCleanupComplete).toBe(true); + }); +}); + +describe("driver terminal finalization CAS", () => { + test("recognizes an exact replay without advancing the terminal transition twice", async () => { + const database = createDriverInstanceDatabase("ready"); + const bindings = { DB: database } as ApiBindings; + const input = { + connectionId: "connection-1", + generation: 1, + heartbeatCount: 2, + status: "stopped" as const, + }; + + await expect(finalizeDriverInstance(bindings, DRIVER_INSTANCE_ID, input)).resolves.toBe( + "stopped", + ); + await expect(finalizeDriverInstance(bindings, DRIVER_INSTANCE_ID, input)).resolves.toBe( + "stopped", + ); + await expect( + finalizeDriverInstance(bindings, DRIVER_INSTANCE_ID, { + ...input, + connectionId: "stale-connection", + }), + ).resolves.toBeNull(); + await expect( + database + .prepare("SELECT status, status_seq AS statusSeq FROM driver_instance WHERE id = ?") + .bind(DRIVER_INSTANCE_ID) + .first(), + ).resolves.toEqual({ status: "stopped", statusSeq: 2 }); + }); + + test("returns a prior canonical failed status for the same connection generation", async () => { + const database = createDriverInstanceDatabase("failed"); + const bindings = { DB: database } as ApiBindings; + + await expect( + finalizeDriverInstance(bindings, DRIVER_INSTANCE_ID, { + connectionId: "connection-1", + generation: 1, + heartbeatCount: 2, + status: "stopped", + }), + ).resolves.toBe("failed"); + await expect( + database + .prepare("SELECT status, status_seq AS statusSeq FROM driver_instance WHERE id = ?") + .bind(DRIVER_INSTANCE_ID) + .first(), + ).resolves.toEqual({ status: "failed", statusSeq: 1 }); + }); +}); diff --git a/apps/api/tests/environment-package-artifact.test.ts b/apps/api/tests/environment-package-artifact.test.ts index 7461081a..42d36530 100644 --- a/apps/api/tests/environment-package-artifact.test.ts +++ b/apps/api/tests/environment-package-artifact.test.ts @@ -17,9 +17,15 @@ import { resolveReadyEnvironmentPackageArtifact, } from "../src/modules/environments/application/environment-package-artifact.service"; import { resolveEnvironmentSetupScriptForExecution } from "../src/modules/environments/application/environment-runtime-snapshot"; -import { createEnvironmentPackageArtifactKey } from "../src/modules/environments/domain/environment-package-artifact"; -import { environmentPackageArtifactSandboxId } from "../src/modules/environments/domain/environment-package-artifact"; +import { + createEnvironmentPackageArtifactKey, + environmentPackageArtifactBuildSandboxId, +} from "../src/modules/environments/domain/environment-package-artifact"; import { exposeEnvironmentNodeModules } from "../src/modules/runtime/infrastructure/runtime-sandbox-provisioning/runtime-environment-artifact"; +import { + createEphemeralSandboxOptions, + getEphemeralUnversionedSandboxHandle, +} from "../src/modules/runtime/infrastructure/runtime-subject-lifecycle/runtime-subject-platform"; import type { ApiBindings } from "../src/platform/cloudflare/worker-types"; import { createApiCommandQueueStub, @@ -57,7 +63,9 @@ describe("Environment package artifacts", () => { { manager: "pip", packages: ["jsonschema==4.25.1", "requests==2.32.4"] }, ]); expect(key.inputDigest).toMatch(/^[0-9a-f]{64}$/u); - expect(environmentPackageArtifactSandboxId(key).length).toBeLessThanOrEqual(63); + expect(environmentPackageArtifactBuildSandboxId("01J0000000000000000000000B", 2, 3)).toBe( + "envpkg-01j0000000000000000000000b-2-3", + ); expect( parseApiCommandPayload( "environment_package_artifact_build", @@ -104,10 +112,26 @@ describe("Environment package artifacts", () => { if (!queued) { throw new Error("Expected queued artifact command."); } - await processApiCommandDeadLetterMessage( - bindings, - createRecordedQueueMessage({ body: queued.body }).message, - ); + const deadLetter = createRecordedQueueMessage({ body: queued.body }); + await processApiCommandDeadLetterMessage(bindings, deadLetter.message); + + expect(deadLetter.recorded).toEqual([{ type: "ack" }]); + await expect( + database + .app() + .select({ + lastErrorCode: apiCommandsTable.lastErrorCode, + lastErrorMessage: apiCommandsTable.lastErrorMessage, + status: apiCommandsTable.status, + }) + .from(apiCommandsTable) + .where(eq(apiCommandsTable.id, command.id)) + .get(), + ).resolves.toEqual({ + lastErrorCode: "package_install_failed", + lastErrorMessage: "Package installation failed.", + status: "dead_lettered", + }); await expect( resolveReadyEnvironmentPackageArtifact(bindings, APP_ID, JSON.stringify(packages)), @@ -179,4 +203,43 @@ describe("Environment package artifacts", () => { rmSync(root, { force: true, recursive: true }); } }); + + test("isolates attempt cleanup and lets abandoned build sandboxes expire", async () => { + const created: string[] = []; + const destroyed: string[] = []; + const bindings = { + runtimeSubjectHandleFactory: (id: string) => { + created.push(id); + return new Proxy( + {}, + { + get: (_target, property) => { + if (property === "then") { + return undefined; + } + return property === "destroy" + ? async () => { + destroyed.push(id); + } + : async () => {}; + }, + }, + ); + }, + } as ApiBindings; + const attemptAId = environmentPackageArtifactBuildSandboxId("01J0000000000000000000000B", 1, 1); + const attemptBId = environmentPackageArtifactBuildSandboxId("01J0000000000000000000000B", 1, 2); + + const attemptA = await getEphemeralUnversionedSandboxHandle(bindings, attemptAId, 15 * 60); + await getEphemeralUnversionedSandboxHandle(bindings, attemptBId, 15 * 60); + await attemptA.destroy(); + + expect(createEphemeralSandboxOptions(15 * 60)).toEqual({ + keepAlive: false, + normalizeId: true, + sleepAfter: 15 * 60, + }); + expect(created).toEqual([attemptAId, attemptBId]); + expect(destroyed).toEqual([attemptAId]); + }); }); diff --git a/apps/api/tests/external-tool-effect-migration.test.ts b/apps/api/tests/external-tool-effect-migration.test.ts new file mode 100644 index 00000000..1e0e53aa --- /dev/null +++ b/apps/api/tests/external-tool-effect-migration.test.ts @@ -0,0 +1,2327 @@ +import { describe, expect, test } from "bun:test"; + +import { + RUNTIME_COMMAND_MAX_UTF8_BYTES, + RUNTIME_COMMAND_TERMINAL_PAYLOAD_MAX_UTF8_BYTES, + measureRuntimeCommandJson, +} from "@mosoo/contracts/runtime-command"; +import type { DriverCommandId, DriverInstanceId, SessionRunId } from "@mosoo/id"; + +import { + acquireProdDeployLeaseStatements, + assertProdDeployLeaseOwned, + PROD_DEPLOY_LEASE_TABLE, +} from "../bin/prod-deploy-lease"; +import { + assertProtocolV3LossyMigrationInventory, + authorizeProtocolV3LegacyRewriteSql, + ENABLE_PROTOCOL_V3_COMMAND_FREEZE_SQL, + installProtocolV3CutoverSql, + parseProtocolV3LossyMigrationInventory, + parseProtocolV3LegacyTerminalIntegrity, + PROTOCOL_V3_CUTOVER_OBJECT_COUNT, + PROTOCOL_V3_CUTOVER_OBJECTS_SQL, + PROTOCOL_V3_CUTOVER_TABLE, + PROTOCOL_V3_LEGACY_TERMINAL_INTEGRITY_SQL, + PROTOCOL_V3_LOSSY_MIGRATION_INVENTORY_SQL, + REMOVE_PROTOCOL_V3_CUTOVER_SQL, + storeProtocolV3CutoverBookmarkSql, +} from "../bin/protocol-v3-cutover"; +import { + createRuntimeCommandRecord, + getRuntimeCommandRecord, + getRuntimeCommandStorageRecord, + updateRuntimeCommandRecord, +} from "../src/modules/runtime/infrastructure/session-runs/runtime-command-store.repository"; +import { + applyDrizzleMigrationAsync as applyMigration, + applyDrizzleMigrationsThrough, +} from "./helpers/drizzle-migrations"; +import { SqliteD1Database } from "./helpers/sqlite-d1"; + +const OLD_V2_PAYLOAD_MAX_UTF8_BYTES = 1024 * 1024; +const RELEASE_TREE_OID = "0123456789abcdef0123456789abcdef01234567"; +const INSTALL_PROTOCOL_V3_CUTOVER_SQL = installProtocolV3CutoverSql(RELEASE_TREE_OID); + +const EFFECT_UNKNOWN_ID = "01J0000000000000000000001A"; +const EFFECT_SUCCEEDED_ID = "01J0000000000000000000001B"; +const COMMAND_UNKNOWN_ID = "01J0000000000000000000001C" as DriverCommandId; +const DRIVER_ID = "01J0000000000000000000001D" as DriverInstanceId; +const SERVER_ID = "01J0000000000000000000001E"; +const RUN_ID = "01J0000000000000000000001F" as SessionRunId; +const COMMAND_SUCCEEDED_ID = "01J0000000000000000000001G" as DriverCommandId; +const SESSION_ID = "01J0000000000000000000001H"; +const AGENT_ID = "01J0000000000000000000001J"; +const ACCOUNT_ID = "01J0000000000000000000001K"; +const APP_ID = "01J0000000000000000000001M"; +const SANDBOX_ID = "01J0000000000000000000001N"; +const SANDBOX_SESSION_ID = "01J0000000000000000000001P"; +const COMMAND_ERROR_ID = "01J0000000000000000000001Q" as DriverCommandId; +const COMMAND_CANONICAL_BOUNDARY_ID = "01J0000000000000000000001R" as DriverCommandId; +const EFFECT_CANONICAL_BOUNDARY_ID = "01J0000000000000000000001S"; +const EFFECT_PROVIDER_RECEIPT_ID = "01J0000000000000000000001T"; +const COMMAND_PROVIDER_RECEIPT_ID = "01J0000000000000000000001V" as DriverCommandId; +const COMMAND_CONTROL_ID = "01J0000000000000000000001W" as DriverCommandId; +const COMMAND_PERMISSION_ID = "01J0000000000000000000001X" as DriverCommandId; +const COMMAND_GENERATION_SEVEN_ID = "01J0000000000000000000001Y" as DriverCommandId; +const COMMAND_GENERATION_EIGHT_ID = "01J0000000000000000000001Z" as DriverCommandId; +const LEGACY_MESSAGE_ID = "01J00000000000000000000020"; +const LEGACY_TERMINAL_EVENT_ID = "01J00000000000000000000021"; +const V3_REFERENCE_MESSAGE_ID = "01J00000000000000000000022"; +const DEPLOY_OWNER = "aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa"; +const OTHER_DEPLOY_OWNER = "bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb"; + +const utf8Encoder = new TextEncoder(); + +function jsonAtUtf8Size( + targetBytes: number, + createValue: (padding: string) => Value, +): string { + const empty = JSON.stringify(createValue("")); + const paddingBytes = targetBytes - utf8Encoder.encode(empty).byteLength; + if (paddingBytes < 0) { + throw new Error("JSON fixture base exceeds its requested byte size."); + } + + const value = JSON.stringify(createValue("x".repeat(paddingBytes))); + if (utf8Encoder.encode(value).byteLength !== targetBytes) { + throw new Error("JSON fixture did not reach its requested UTF-8 byte size."); + } + return value; +} + +function padJsonObjectToUtf8Size(value: string, targetBytes: number): string { + if (!value.endsWith("}")) throw new Error("JSON fixture must be an object."); + const paddingBytes = targetBytes - utf8Encoder.encode(value).byteLength; + if (paddingBytes < 0) throw new Error("JSON fixture base exceeds its requested byte size."); + const padded = `${value.slice(0, -1)}${" ".repeat(paddingBytes)}}`; + if (utf8Encoder.encode(padded).byteLength !== targetBytes) { + throw new Error("JSON fixture did not reach its requested UTF-8 byte size."); + } + return padded; +} + +async function readLegacyTerminalIntegrity(database: SqliteD1Database) { + const integrityRow = await database + .prepare(PROTOCOL_V3_LEGACY_TERMINAL_INTEGRITY_SQL) + .first>(); + return parseProtocolV3LegacyTerminalIntegrity( + JSON.stringify([{ results: [integrityRow], success: true }]), + ); +} + +async function readLossyMigrationInventory(database: SqliteD1Database) { + const row = await database + .prepare(PROTOCOL_V3_LOSSY_MIGRATION_INVENTORY_SQL) + .first>(); + if (row === null) throw new Error("Lossy migration inventory returned no row."); + return parseProtocolV3LossyMigrationInventory( + JSON.stringify([{ results: [row], success: true }]), + ); +} + +function installLegacyRewriteAuthorizationInfrastructure(database: SqliteD1Database): void { + database.execute(INSTALL_PROTOCOL_V3_CUTOVER_SQL); + database.execute(ENABLE_PROTOCOL_V3_COMMAND_FREEZE_SQL); + database.execute(storeProtocolV3CutoverBookmarkSql("test-bookmark")); + for (const statement of acquireProdDeployLeaseStatements(DEPLOY_OWNER)) { + database.execute(statement); + } +} + +async function authorizeLegacyTerminalRewrite(database: SqliteD1Database): Promise { + installLegacyRewriteAuthorizationInfrastructure(database); + const integrity = await readLegacyTerminalIntegrity(database); + database.execute( + authorizeProtocolV3LegacyRewriteSql( + DEPLOY_OWNER, + integrity.noncanonicalTerminalSources, + integrity.rewriteCandidateManifestJson, + ), + ); +} + +async function createV2Database(): Promise { + const database = new SqliteD1Database(); + applyDrizzleMigrationsThrough(database, "0012_agent-task-snapshot-state"); + + return database; +} + +async function insertV2Owners(database: SqliteD1Database): Promise { + database.execute(` + INSERT INTO driver_instance ( + boot_token_expires_at, boot_token_hash, created_at, expires_at, + heartbeat_count, id, protocol, protocol_version, runtime, sandbox_id, + sandbox_session_id, status, updated_at + ) VALUES ( + 1000, x'01', 1, 1000, 0, '${DRIVER_ID}', 'rpc', 2, 'codex', + '${SANDBOX_ID}', '${SANDBOX_SESSION_ID}', 'failed', 10 + ); + + INSERT INTO session ( + agent_id, created_at, creator_account_id, id, kind, model, app_id, + provider, renamed, runtime_id, status, updated_at + ) VALUES ( + '${AGENT_ID}', 1, '${ACCOUNT_ID}', '${SESSION_ID}', 'agent', 'model', + '${APP_ID}', 'provider', 0, 'codex', 'TERMINATED', 10 + ); + + INSERT INTO session_run ( + agent_id, completed_at, created_at, created_by_account_id, + driver_instance_id, error_code, error_details_json, error_message, id, + session_id, status, trace_id, trigger, updated_at + ) VALUES ( + '${AGENT_ID}', 10, 1, '${ACCOUNT_ID}', '${DRIVER_ID}', NULL, NULL, NULL, + '${RUN_ID}', '${SESSION_ID}', 'failed', 'trace-1', 'user_prompt', 10 + ); + `); +} + +async function insertLegacySessionTerminal( + database: SqliteD1Database, + input: { + eventId: string; + eventType: "run.cancelled" | "run.completed" | "run.failed"; + runId?: string; + seq?: number; + sessionId?: string; + sourceEventId: string; + }, +): Promise { + await database + .prepare( + `INSERT INTO session_event ( + agent_id, content_text, created_at, ended_at, event_type, family, id, + occurred_at, process_status, process_type, run_id, seq, session_id, + source_event_id, source, visibility + ) VALUES (?, ?, 3, 3, ?, 'run', ?, 3, 'available', ?, ?, ?, ?, ?, 'api', 'all_consumers')`, + ) + .bind( + AGENT_ID, + input.eventType, + input.eventType, + input.eventId, + input.eventType, + input.runId ?? RUN_ID, + input.seq ?? 1, + input.sessionId ?? SESSION_ID, + input.sourceEventId, + ) + .run(); +} + +async function insertV2Command( + database: SqliteD1Database, + input: { + errorJson?: string | null; + id: DriverCommandId; + kind: string; + payloadJson: string; + resultJson?: string | null; + seq: number; + status: string; + }, +): Promise { + await database + .prepare( + `INSERT INTO driver_command ( + acked_at, completed_at, driver_instance_id, error_json, id, issued_at, + kind, payload_json, result_json, seq, status + ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, + ) + .bind( + 2, + 3, + DRIVER_ID, + input.errorJson ?? null, + input.id, + 1, + input.kind, + input.payloadJson, + input.resultJson ?? null, + input.seq, + input.status, + ) + .run(); +} + +async function insertV2Effect( + database: SqliteD1Database, + input: { + commandId: DriverCommandId; + effectId: string; + providerReceiptJson?: string | null; + resultJson: string | null; + status: "executing" | "succeeded"; + toolName: string; + }, +): Promise { + await database + .prepare( + `INSERT INTO external_tool_effect ( + attempt_count, command_id, created_at, driver_instance_id, id, + idempotency_key, provider_receipt_json, result_json, server_id, + session_run_id, status, tool_name, updated_at + ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, + ) + .bind( + 1, + input.commandId, + 1, + DRIVER_ID, + input.effectId, + input.effectId, + input.providerReceiptJson === undefined + ? input.status === "succeeded" + ? '{"receipt":"legacy"}' + : null + : input.providerReceiptJson, + input.resultJson, + SERVER_ID, + RUN_ID, + input.status, + input.toolName, + 2, + ) + .run(); +} + +async function readMigratedEffectState( + database: SqliteD1Database, + commandId: DriverCommandId, +): Promise> { + const row = await database + .prepare("SELECT id, result_json, status FROM external_tool_effect WHERE command_id = ?") + .bind(commandId) + .first<{ id: string; result_json: string | null; status: string }>(); + if (row === null) { + throw new Error("Migrated external tool effect was not found."); + } + + return row.status === "succeeded" + ? { effectId: row.id, kind: row.status, result: JSON.parse(row.result_json ?? "null") } + : { effectId: row.id, kind: row.status }; +} + +async function insertV2Attempt( + database: SqliteD1Database, + input: { + effectId: string; + providerReceiptJson?: string | null; + resultJson: string | null; + status: "executing" | "succeeded"; + }, +): Promise { + await database + .prepare( + `INSERT INTO external_tool_effect_attempt ( + attempt, completed_at, created_at, effect_id, provider_receipt_json, + result_json, status + ) VALUES (?, ?, ?, ?, ?, ?, ?)`, + ) + .bind( + 1, + input.status === "succeeded" ? 3 : null, + 2, + input.effectId, + input.providerReceiptJson === undefined + ? input.status === "succeeded" + ? '{"receipt":"legacy"}' + : null + : input.providerReceiptJson, + input.resultJson, + input.status, + ) + .run(); +} + +async function expectV2MigrationRejection( + seed: (database: SqliteD1Database) => Promise, +): Promise { + const database = await createV2Database(); + await insertV2Owners(database); + await seed(database); + await expect(applyMigration(database, "0013_durable-mcp-effect-v3")).rejects.toThrow(); + return database; +} + +describe("durable MCP effect v3 migration", () => { + test("rejects every lossy v2 rewrite before changing schema or history", async () => { + const database = await createV2Database(); + await insertV2Owners(database); + + const unknownPayloadJson = jsonAtUtf8Size(OLD_V2_PAYLOAD_MAX_UTF8_BYTES, (argumentsJson) => ({ + argumentsJson, + commandId: COMMAND_UNKNOWN_ID, + kind: "mcp.execute", + requestId: "legacy-unknown-request", + serverId: SERVER_ID, + toolCallId: "legacy-unknown-tool-call", + toolName: "createIssue", + })); + await insertV2Command(database, { + id: COMMAND_UNKNOWN_ID, + kind: "mcp.execute", + payloadJson: unknownPayloadJson, + seq: 1, + status: "failed", + }); + await insertV2Effect(database, { + commandId: COMMAND_UNKNOWN_ID, + effectId: EFFECT_UNKNOWN_ID, + resultJson: null, + status: "executing", + toolName: "createIssue", + }); + await insertV2Attempt(database, { + effectId: EFFECT_UNKNOWN_ID, + resultJson: null, + status: "executing", + }); + + const succeededPayloadJson = jsonAtUtf8Size( + RUNTIME_COMMAND_MAX_UTF8_BYTES + 1, + (argumentsJson) => ({ + argumentsJson, + commandId: COMMAND_SUCCEEDED_ID, + kind: "mcp.execute", + requestId: "legacy-succeeded-request", + serverId: SERVER_ID, + toolCallId: "legacy-succeeded-tool-call", + toolName: "createIssue", + }), + ); + const historicalToolName = "createIssue"; + const createSucceededResult = (targetBytes: number) => + jsonAtUtf8Size(targetBytes, (outputText) => ({ + outputText, + requestId: "legacy-succeeded-request", + serverId: SERVER_ID, + toolName: historicalToolName, + })); + const effectResultJson = createSucceededResult(OLD_V2_PAYLOAD_MAX_UTF8_BYTES); + const attemptResultJson = createSucceededResult(OLD_V2_PAYLOAD_MAX_UTF8_BYTES - 1); + const commandResultJson = createSucceededResult(OLD_V2_PAYLOAD_MAX_UTF8_BYTES - 2); + await insertV2Command(database, { + id: COMMAND_SUCCEEDED_ID, + kind: "mcp.execute", + payloadJson: succeededPayloadJson, + resultJson: commandResultJson, + seq: 2, + status: "completed", + }); + await insertV2Effect(database, { + commandId: COMMAND_SUCCEEDED_ID, + effectId: EFFECT_SUCCEEDED_ID, + resultJson: effectResultJson, + status: "succeeded", + toolName: historicalToolName, + }); + await insertV2Attempt(database, { + effectId: EFFECT_SUCCEEDED_ID, + resultJson: attemptResultJson, + status: "succeeded", + }); + + const canonicalBoundaryPayloadJson = jsonAtUtf8Size( + RUNTIME_COMMAND_MAX_UTF8_BYTES + 1, + (argumentsJson) => ({ + argumentsJson, + commandId: COMMAND_CANONICAL_BOUNDARY_ID, + kind: "mcp.execute", + requestId: "legacy-boundary-request", + serverId: SERVER_ID, + toolCallId: "legacy-boundary-tool-call", + toolName: "createIssue", + }), + ); + const canonicalBoundaryResultJson = jsonAtUtf8Size( + RUNTIME_COMMAND_TERMINAL_PAYLOAD_MAX_UTF8_BYTES, + (outputText) => ({ + outputText, + requestId: "legacy-boundary-request", + serverId: SERVER_ID, + toolName: "createIssue", + }), + ); + await insertV2Command(database, { + errorJson: JSON.stringify({ + code: "legacy.finalization_race", + details: {}, + message: "The old finalizer raced the durable effect.", + retryable: false, + }), + id: COMMAND_CANONICAL_BOUNDARY_ID, + kind: "mcp.execute", + payloadJson: canonicalBoundaryPayloadJson, + resultJson: canonicalBoundaryResultJson, + seq: 3, + status: "failed", + }); + await insertV2Effect(database, { + commandId: COMMAND_CANONICAL_BOUNDARY_ID, + effectId: EFFECT_CANONICAL_BOUNDARY_ID, + resultJson: canonicalBoundaryResultJson, + status: "succeeded", + toolName: "createIssue", + }); + await insertV2Attempt(database, { + effectId: EFFECT_CANONICAL_BOUNDARY_ID, + resultJson: canonicalBoundaryResultJson, + status: "succeeded", + }); + + const providerReceiptResult = JSON.stringify({ + outputText: "provider response", + requestId: "legacy-provider-request", + serverId: SERVER_ID, + toolName: "createIssue", + }); + const oversizedProviderReceipt = "x".repeat(RUNTIME_COMMAND_TERMINAL_PAYLOAD_MAX_UTF8_BYTES); + await insertV2Command(database, { + id: COMMAND_PROVIDER_RECEIPT_ID, + kind: "mcp.execute", + payloadJson: JSON.stringify({ + argumentsJson: "{}", + commandId: COMMAND_PROVIDER_RECEIPT_ID, + kind: "mcp.execute", + requestId: "legacy-provider-request", + serverId: SERVER_ID, + toolCallId: "legacy-provider-tool-call", + toolName: "createIssue", + }), + resultJson: providerReceiptResult, + seq: 4, + status: "completed", + }); + await insertV2Effect(database, { + commandId: COMMAND_PROVIDER_RECEIPT_ID, + effectId: EFFECT_PROVIDER_RECEIPT_ID, + providerReceiptJson: oversizedProviderReceipt, + resultJson: providerReceiptResult, + status: "succeeded", + toolName: "createIssue", + }); + await insertV2Attempt(database, { + effectId: EFFECT_PROVIDER_RECEIPT_ID, + providerReceiptJson: oversizedProviderReceipt, + resultJson: providerReceiptResult, + status: "succeeded", + }); + + const commandErrorJson = jsonAtUtf8Size(OLD_V2_PAYLOAD_MAX_UTF8_BYTES, (context) => ({ + code: "legacy.command_error", + details: { context }, + message: "Legacy command failed.", + retryable: false, + })); + await insertV2Command(database, { + errorJson: commandErrorJson, + id: COMMAND_ERROR_ID, + kind: "input.start", + payloadJson: JSON.stringify({ + commandId: COMMAND_ERROR_ID, + input: { text: "hello" }, + kind: "input.start", + requestId: "legacy-input-request", + runId: RUN_ID, + }), + seq: 5, + status: "failed", + }); + await insertV2Command(database, { + id: COMMAND_CONTROL_ID, + kind: "turn.cancel", + payloadJson: JSON.stringify({ commandId: COMMAND_CONTROL_ID, kind: "turn.cancel" }), + resultJson: "null", + seq: 6, + status: "completed", + }); + await insertV2Command(database, { + id: COMMAND_PERMISSION_ID, + kind: "permission.resolve", + payloadJson: JSON.stringify({ + commandId: COMMAND_PERMISSION_ID, + decision: "allow_once", + kind: "permission.resolve", + requestId: "legacy-permission-request", + }), + seq: 7, + status: "completed", + }); + + const sessionErrorJson = jsonAtUtf8Size(OLD_V2_PAYLOAD_MAX_UTF8_BYTES, (context) => ({ + code: "legacy.session_error", + details: { context }, + message: "Legacy Session Run failed.", + retryable: false, + })); + const sessionError = JSON.parse(sessionErrorJson) as { + code: string; + details: Record; + message: string; + }; + await database + .prepare( + "UPDATE session_run SET error_code = ?, error_details_json = ?, error_message = ? WHERE id = ?", + ) + .bind(sessionError.code, JSON.stringify(sessionError.details), sessionError.message, RUN_ID) + .run(); + + // The same Driver instance may have been reused after these terminal commands. + // Its current generation is not evidence of the historical command generation. + await database + .prepare("UPDATE driver_instance SET generation = ? WHERE id = ?") + .bind(7, DRIVER_ID) + .run(); + + const inventory = await readLossyMigrationInventory(database); + expect(inventory).toEqual({ + attemptCompletionTimeFabrications: 0, + candidateIds: [ + { category: "command_error_omission", id: COMMAND_ERROR_ID }, + { category: "mcp_argument_omission", id: COMMAND_UNKNOWN_ID }, + { category: "mcp_argument_omission", id: COMMAND_SUCCEEDED_ID }, + { category: "mcp_argument_omission", id: COMMAND_CANONICAL_BOUNDARY_ID }, + { category: "mcp_command_terminal_conflict", id: EFFECT_CANONICAL_BOUNDARY_ID }, + { category: "mcp_result_omission", id: EFFECT_SUCCEEDED_ID }, + { category: "mcp_result_omission", id: EFFECT_CANONICAL_BOUNDARY_ID }, + { category: "provider_receipt_loss", id: EFFECT_PROVIDER_RECEIPT_ID }, + { category: "session_run_error_omission", id: RUN_ID }, + ], + commandErrorOmissions: 1, + commandPayloadConflicts: 0, + controlReasonOmissions: 0, + inputStartResultOmissions: 0, + inputTextOmissions: 0, + mcpArgumentOmissions: 3, + mcpCommandTerminalConflicts: 1, + mcpResultConflicts: 0, + mcpResultOmissions: 2, + orphanEffects: 0, + providerReceiptLosses: 1, + permissionPayloadRewrites: 0, + sessionRunErrorOmissions: 1, + totalCandidates: 9, + }); + expect(() => assertProtocolV3LossyMigrationInventory(inventory)).toThrow( + "No lossy migration candidates are authorized", + ); + + await expect(applyMigration(database, "0013_durable-mcp-effect-v3")).rejects.toThrow(); + + await expect( + database + .prepare("SELECT payload_json FROM driver_command WHERE id = ?") + .bind(COMMAND_UNKNOWN_ID) + .first(), + ).resolves.toEqual({ payload_json: unknownPayloadJson }); + await expect( + database + .prepare("SELECT error_json FROM driver_command WHERE id = ?") + .bind(COMMAND_ERROR_ID) + .first(), + ).resolves.toEqual({ error_json: commandErrorJson }); + await expect( + database + .prepare("SELECT provider_receipt_json FROM external_tool_effect WHERE id = ?") + .bind(EFFECT_PROVIDER_RECEIPT_ID) + .first(), + ).resolves.toEqual({ provider_receipt_json: oversizedProviderReceipt }); + await expect( + database + .prepare( + "SELECT error_code, error_details_json, error_message FROM session_run WHERE id = ?", + ) + .bind(RUN_ID) + .first(), + ).resolves.toEqual({ + error_code: sessionError.code, + error_details_json: JSON.stringify(sessionError.details), + error_message: sessionError.message, + }); + await expect( + database + .prepare( + "SELECT count(*) AS count FROM pragma_table_info('driver_command') WHERE name = 'driver_generation'", + ) + .first(), + ).resolves.toEqual({ count: 0 }); + }); + + test("rejects a real v2 database with an old-limit nonterminal command", async () => { + const database = await createV2Database(); + await insertV2Owners(database); + const payloadJson = jsonAtUtf8Size(OLD_V2_PAYLOAD_MAX_UTF8_BYTES, (text) => ({ + commandId: COMMAND_ERROR_ID, + input: { text }, + kind: "input.start", + requestId: "legacy-input-request", + runId: RUN_ID, + })); + await insertV2Command(database, { + id: COMMAND_ERROR_ID, + kind: "input.start", + payloadJson, + seq: 1, + status: "accepted", + }); + await expect( + database + .prepare("SELECT COUNT(*) AS count FROM driver_command WHERE status = 'accepted'") + .first(), + ).resolves.toEqual({ count: 1 }); + await expect(database.prepare("PRAGMA ignore_check_constraints").first()).resolves.toEqual({ + ignore_check_constraints: 0, + }); + + await expect(applyMigration(database, "0013_durable-mcp-effect-v3")).rejects.toThrow(); + await expect( + database + .prepare("SELECT payload_json, status FROM driver_command WHERE id = ?") + .bind(COMMAND_ERROR_ID) + .first(), + ).resolves.toEqual({ payload_json: payloadJson, status: "accepted" }); + }); + + test("rejects an orphan effect before its inner-join rebuild can delete it", async () => { + const database = new SqliteD1Database({ foreignKeys: false }); + applyDrizzleMigrationsThrough(database, "0012_agent-task-snapshot-state"); + await database + .prepare( + `INSERT INTO external_tool_effect ( + attempt_count, command_id, created_at, driver_instance_id, id, + idempotency_key, provider_receipt_json, result_json, server_id, + session_run_id, status, tool_name, updated_at + ) VALUES (0, ?, 1, ?, ?, ?, NULL, NULL, ?, ?, 'intent', 'createIssue', 2)`, + ) + .bind(COMMAND_UNKNOWN_ID, DRIVER_ID, EFFECT_UNKNOWN_ID, EFFECT_UNKNOWN_ID, SERVER_ID, RUN_ID) + .run(); + database.execute("PRAGMA foreign_keys = ON"); + await expect(database.prepare("PRAGMA foreign_keys").first()).resolves.toEqual({ + foreign_keys: 1, + }); + + const inventory = await readLossyMigrationInventory(database); + expect(inventory.orphanEffects).toBe(1); + expect(inventory.candidateIds).toEqual([{ category: "orphan_effect", id: EFFECT_UNKNOWN_ID }]); + await expect(applyMigration(database, "0013_durable-mcp-effect-v3")).rejects.toThrow(); + await expect( + database + .prepare("SELECT command_id, status FROM external_tool_effect WHERE id = ?") + .bind(EFFECT_UNKNOWN_ID) + .first(), + ).resolves.toEqual({ command_id: COMMAND_UNKNOWN_ID, status: "intent" }); + }); + + test("rejects missing terminal attempt times instead of fabricating migration time", async () => { + for (const status of ["succeeded", "unknown"] as const) { + const database = await createV2Database(); + await insertV2Owners(database); + const payloadJson = JSON.stringify({ + argumentsJson: "{}", + commandId: COMMAND_SUCCEEDED_ID, + kind: "mcp.execute", + requestId: "legacy-request", + serverId: SERVER_ID, + toolCallId: "legacy-tool-call", + toolName: "createIssue", + }); + const resultJson = + status === "succeeded" + ? JSON.stringify({ + outputText: "done", + requestId: "legacy-request", + serverId: SERVER_ID, + toolName: "createIssue", + }) + : null; + await insertV2Command(database, { + id: COMMAND_SUCCEEDED_ID, + kind: "mcp.execute", + payloadJson, + resultJson, + seq: 1, + status: status === "succeeded" ? "completed" : "failed", + }); + await insertV2Effect(database, { + commandId: COMMAND_SUCCEEDED_ID, + effectId: EFFECT_SUCCEEDED_ID, + providerReceiptJson: status === "succeeded" ? '{"receipt":"legacy"}' : null, + resultJson, + status: status === "succeeded" ? "succeeded" : "executing", + toolName: "createIssue", + }); + await insertV2Attempt(database, { + effectId: EFFECT_SUCCEEDED_ID, + providerReceiptJson: status === "succeeded" ? '{"receipt":"legacy"}' : null, + resultJson, + status: status === "succeeded" ? "succeeded" : "executing", + }); + await database + .prepare("UPDATE external_tool_effect SET status = ? WHERE id = ?") + .bind(status, EFFECT_SUCCEEDED_ID) + .run(); + await database + .prepare( + "UPDATE external_tool_effect_attempt SET completed_at = NULL, status = ? WHERE effect_id = ?", + ) + .bind(status, EFFECT_SUCCEEDED_ID) + .run(); + + const inventory = await readLossyMigrationInventory(database); + expect(inventory.attemptCompletionTimeFabrications).toBe(1); + expect(inventory.candidateIds).toEqual([ + { category: "attempt_completion_time_fabrication", id: EFFECT_SUCCEEDED_ID }, + ]); + await expect(applyMigration(database, "0013_durable-mcp-effect-v3")).rejects.toThrow(); + await expect( + database + .prepare( + "SELECT completed_at, status FROM external_tool_effect_attempt WHERE effect_id = ?", + ) + .bind(EFFECT_SUCCEEDED_ID) + .first(), + ).resolves.toEqual({ completed_at: null, status }); + } + }); + + test("keeps the executing-to-unknown transition time monotonic", async () => { + const database = await createV2Database(); + await insertV2Owners(database); + await insertV2Command(database, { + id: COMMAND_UNKNOWN_ID, + kind: "mcp.execute", + payloadJson: JSON.stringify({ + argumentsJson: "{}", + commandId: COMMAND_UNKNOWN_ID, + kind: "mcp.execute", + requestId: "legacy-request", + serverId: SERVER_ID, + toolCallId: "legacy-tool-call", + toolName: "createIssue", + }), + seq: 1, + status: "failed", + }); + await insertV2Effect(database, { + commandId: COMMAND_UNKNOWN_ID, + effectId: EFFECT_UNKNOWN_ID, + providerReceiptJson: null, + resultJson: null, + status: "executing", + toolName: "createIssue", + }); + await insertV2Attempt(database, { + effectId: EFFECT_UNKNOWN_ID, + providerReceiptJson: null, + resultJson: null, + status: "executing", + }); + const futureTimestamp = 9_007_199_254_740_000; + await database + .prepare("UPDATE external_tool_effect SET updated_at = ? WHERE id = ?") + .bind(futureTimestamp, EFFECT_UNKNOWN_ID) + .run(); + await database + .prepare("UPDATE external_tool_effect_attempt SET created_at = ? WHERE effect_id = ?") + .bind(futureTimestamp, EFFECT_UNKNOWN_ID) + .run(); + const inventory = await readLossyMigrationInventory(database); + expect(inventory.totalCandidates).toBe(0); + + await applyMigration(database, "0013_durable-mcp-effect-v3"); + const transition = await database + .prepare( + `SELECT + "attempt"."completed_at", + "attempt"."status" AS "attempt_status", + "effect"."status" AS "effect_status", + "effect"."updated_at" + FROM "external_tool_effect_attempt" AS "attempt" + INNER JOIN "external_tool_effect" AS "effect" ON "effect"."id" = "attempt"."effect_id" + WHERE "attempt"."effect_id" = ?`, + ) + .bind(EFFECT_UNKNOWN_ID) + .first(); + expect(transition).toEqual({ + attempt_status: "unknown", + completed_at: futureTimestamp, + effect_status: "unknown", + updated_at: futureTimestamp, + }); + }); + + test("keeps legacy terminal history separate from two new Driver generations", async () => { + const database = await createV2Database(); + await insertV2Owners(database); + await insertV2Command(database, { + id: COMMAND_CONTROL_ID, + kind: "turn.cancel", + payloadJson: JSON.stringify({ commandId: COMMAND_CONTROL_ID, kind: "turn.cancel" }), + seq: 1, + status: "completed", + }); + database.execute(` + UPDATE session + SET runtime_event_seq_cursor = 1 + WHERE id = '${SESSION_ID}'; + UPDATE session_run + SET error_code = 'legacy.failed', + error_details_json = '{}', + error_message = 'Legacy failure.', + status_event = 'run.fail' + WHERE id = '${RUN_ID}'; + `); + await insertLegacySessionTerminal(database, { + eventId: LEGACY_TERMINAL_EVENT_ID, + eventType: "run.failed", + sourceEventId: `session-run-terminal:${RUN_ID}:run.failed`, + }); + const inventory = await readLossyMigrationInventory(database); + expect(inventory.totalCandidates).toBe(0); + expect(() => assertProtocolV3LossyMigrationInventory(inventory)).not.toThrow(); + await applyMigration(database, "0013_durable-mcp-effect-v3"); + await applyMigration(database, "0014_session-event-stream-identity"); + await database + .prepare( + "UPDATE driver_instance SET command_seq_cursor = 10, connection_id = ?, generation = 7, status = 'ready' WHERE id = ?", + ) + .bind("generation-7", DRIVER_ID) + .run(); + await database + .prepare("UPDATE session_run SET completed_at = NULL, status = 'running' WHERE id = ?") + .bind(RUN_ID) + .run(); + + await createRuntimeCommandRecord(database, { + command: { + commandId: COMMAND_GENERATION_SEVEN_ID, + kind: "turn.cancel", + reason: "generation seven", + runId: RUN_ID, + }, + driverGeneration: 7, + driverInstanceId: DRIVER_ID, + status: "accepted", + }); + await updateRuntimeCommandRecord(database, { + commandId: COMMAND_GENERATION_SEVEN_ID, + driverGeneration: 7, + driverInstanceId: DRIVER_ID, + status: "cancelled", + }); + await database + .prepare("UPDATE driver_instance SET connection_id = ?, generation = 8 WHERE id = ?") + .bind("generation-8", DRIVER_ID) + .run(); + await createRuntimeCommandRecord(database, { + command: { + commandId: COMMAND_GENERATION_EIGHT_ID, + input: { text: "generation eight" }, + kind: "input.start", + requestId: "generation-8-request", + runId: RUN_ID, + }, + driverGeneration: 8, + driverInstanceId: DRIVER_ID, + }); + + await expect( + getRuntimeCommandStorageRecord(database, DRIVER_ID, COMMAND_CONTROL_ID), + ).resolves.toMatchObject({ driverGeneration: null, format: "legacy-v2-terminal" }); + await expect( + getRuntimeCommandRecord(database, DRIVER_ID, 7, COMMAND_GENERATION_SEVEN_ID), + ).resolves.toMatchObject({ status: "cancelled" }); + await expect( + getRuntimeCommandRecord(database, DRIVER_ID, 8, COMMAND_GENERATION_SEVEN_ID), + ).resolves.toBeNull(); + await expect( + getRuntimeCommandRecord(database, DRIVER_ID, 8, COMMAND_GENERATION_EIGHT_ID), + ).resolves.toMatchObject({ status: "queued" }); + await expect( + updateRuntimeCommandRecord(database, { + commandId: COMMAND_GENERATION_SEVEN_ID, + driverGeneration: 8, + driverInstanceId: DRIVER_ID, + status: "failed", + }), + ).resolves.toMatchObject({ kind: "rejected", reason: "command_not_found" }); + await expect( + database + .prepare( + "SELECT driver_generation, status FROM driver_command ORDER BY driver_generation, id", + ) + .all(), + ).resolves.toMatchObject({ + results: [ + { driver_generation: null, status: "completed" }, + { driver_generation: 7, status: "cancelled" }, + { driver_generation: 8, status: "queued" }, + ], + }); + }); + + test("preserves bounded MCP identities without inventing a 256-byte contract", async () => { + const database = await createV2Database(); + await insertV2Owners(database); + const toolName = "t".repeat(300); + const payloadJson = JSON.stringify({ + argumentsJson: "{}", + commandId: COMMAND_SUCCEEDED_ID, + kind: "mcp.execute", + requestId: "legacy-request", + serverId: SERVER_ID, + toolCallId: "legacy-tool-call", + toolName, + }); + const result = { + outputText: "done", + requestId: "legacy-request", + serverId: SERVER_ID, + toolName, + }; + const resultJson = JSON.stringify(result); + await insertV2Command(database, { + id: COMMAND_SUCCEEDED_ID, + kind: "mcp.execute", + payloadJson, + resultJson, + seq: 1, + status: "completed", + }); + await insertV2Effect(database, { + commandId: COMMAND_SUCCEEDED_ID, + effectId: EFFECT_SUCCEEDED_ID, + resultJson, + status: "succeeded", + toolName, + }); + await insertV2Attempt(database, { + effectId: EFFECT_SUCCEEDED_ID, + resultJson, + status: "succeeded", + }); + + await applyMigration(database, "0013_durable-mcp-effect-v3"); + + await expect(readMigratedEffectState(database, COMMAND_SUCCEEDED_ID)).resolves.toEqual({ + effectId: EFFECT_SUCCEEDED_ID, + kind: "succeeded", + result, + }); + const command = await getRuntimeCommandStorageRecord(database, DRIVER_ID, COMMAND_SUCCEEDED_ID); + expect(command?.record).toMatchObject({ payload: { toolName }, result: { toolName } }); + expect(measureRuntimeCommandJson(command?.record.payload)).toBeLessThanOrEqual( + RUNTIME_COMMAND_MAX_UTF8_BYTES, + ); + }); + + test("rejects an identity-only oversized MCP record instead of replacing its identity", async () => { + const payloadJson = jsonAtUtf8Size(RUNTIME_COMMAND_MAX_UTF8_BYTES + 1, (requestId) => ({ + argumentsJson: "{}", + commandId: COMMAND_UNKNOWN_ID, + kind: "mcp.execute", + requestId, + serverId: SERVER_ID, + toolCallId: "legacy-tool-call", + toolName: "createIssue", + })); + const database = await expectV2MigrationRejection(async (fixture) => { + await insertV2Command(fixture, { + id: COMMAND_UNKNOWN_ID, + kind: "mcp.execute", + payloadJson, + seq: 1, + status: "failed", + }); + await insertV2Effect(fixture, { + commandId: COMMAND_UNKNOWN_ID, + effectId: EFFECT_UNKNOWN_ID, + resultJson: null, + status: "executing", + toolName: "createIssue", + }); + await insertV2Attempt(fixture, { + effectId: EFFECT_UNKNOWN_ID, + resultJson: null, + status: "executing", + }); + }); + + await expect( + database + .prepare("SELECT payload_json FROM driver_command WHERE id = ?") + .bind(COMMAND_UNKNOWN_ID) + .first(), + ).resolves.toEqual({ payload_json: payloadJson }); + }); + + test("rejects oversized input text without changing attachments or Run identity", async () => { + const payloadJson = jsonAtUtf8Size(RUNTIME_COMMAND_MAX_UTF8_BYTES + 1, (text) => ({ + commandId: COMMAND_ERROR_ID, + input: { attachmentIds: ["file-1", "file-2"], text }, + kind: "input.start", + requestId: "legacy-input-request", + runId: RUN_ID, + })); + const database = await createV2Database(); + await insertV2Owners(database); + await insertV2Command(database, { + id: COMMAND_ERROR_ID, + kind: "input.start", + payloadJson, + seq: 1, + status: "failed", + }); + + const inventory = await readLossyMigrationInventory(database); + expect(inventory.inputTextOmissions).toBe(1); + expect(inventory.candidateIds).toEqual([ + { category: "input_text_omission", id: COMMAND_ERROR_ID }, + ]); + await expect(applyMigration(database, "0013_durable-mcp-effect-v3")).rejects.toThrow(); + + await expect( + database + .prepare("SELECT payload_json FROM driver_command WHERE id = ?") + .bind(COMMAND_ERROR_ID) + .first(), + ).resolves.toEqual({ payload_json: payloadJson }); + }); + + test("rejects an oversized input result even when its command payload is bounded", async () => { + const resultJson = jsonAtUtf8Size( + RUNTIME_COMMAND_TERMINAL_PAYLOAD_MAX_UTF8_BYTES + 1, + (requestId) => ({ requestId }), + ); + const database = await createV2Database(); + await insertV2Owners(database); + await insertV2Command(database, { + id: COMMAND_ERROR_ID, + kind: "input.start", + payloadJson: JSON.stringify({ + commandId: COMMAND_ERROR_ID, + input: { text: "hello" }, + kind: "input.start", + requestId: "legacy-input-request", + runId: RUN_ID, + }), + resultJson, + seq: 1, + status: "completed", + }); + + const inventory = await readLossyMigrationInventory(database); + expect(inventory.inputStartResultOmissions).toBe(1); + expect(inventory.candidateIds).toEqual([ + { category: "input_start_result_omission", id: COMMAND_ERROR_ID }, + ]); + await expect(applyMigration(database, "0013_durable-mcp-effect-v3")).rejects.toThrow(); + await expect( + database + .prepare("SELECT result_json FROM driver_command WHERE id = ?") + .bind(COMMAND_ERROR_ID) + .first(), + ).resolves.toEqual({ result_json: resultJson }); + }); + + test("rejects oversized control reasons instead of replacing them", async () => { + const database = await createV2Database(); + await insertV2Owners(database); + const fixtures = [ + { id: COMMAND_CONTROL_ID, kind: "turn.cancel" }, + { id: COMMAND_PERMISSION_ID, kind: "session.stop" }, + ] as const; + for (const [index, fixture] of fixtures.entries()) { + await insertV2Command(database, { + id: fixture.id, + kind: fixture.kind, + payloadJson: jsonAtUtf8Size(RUNTIME_COMMAND_MAX_UTF8_BYTES + 1, (reason) => ({ + commandId: fixture.id, + kind: fixture.kind, + reason, + })), + seq: index + 1, + status: "failed", + }); + } + + const inventory = await readLossyMigrationInventory(database); + expect(inventory.controlReasonOmissions).toBe(2); + expect(inventory.candidateIds).toEqual([ + { category: "control_reason_omission", id: COMMAND_CONTROL_ID }, + { category: "control_reason_omission", id: COMMAND_PERMISSION_ID }, + ]); + await expect(applyMigration(database, "0013_durable-mcp-effect-v3")).rejects.toThrow(); + }); + + test("rejects duplicate command payload keys before SQLite can choose a different copy", async () => { + const database = await createV2Database(); + await insertV2Owners(database); + const inputPayloadJson = `{"commandId":"${COMMAND_ERROR_ID}","input":{"text":"first","text":"last"},"kind":"input.start","requestId":"legacy-input-request","runId":"${RUN_ID}"}`; + const permissionPayloadJson = padJsonObjectToUtf8Size( + `{"commandId":"${COMMAND_PERMISSION_ID}","decision":"allow_once","decision":"reject_once","kind":"permission.resolve","requestId":"legacy-permission-request"}`, + RUNTIME_COMMAND_MAX_UTF8_BYTES + 1, + ); + expect(JSON.parse(inputPayloadJson)).toMatchObject({ input: { text: "last" } }); + expect(JSON.parse(permissionPayloadJson)).toMatchObject({ decision: "reject_once" }); + + await insertV2Command(database, { + id: COMMAND_ERROR_ID, + kind: "input.start", + payloadJson: inputPayloadJson, + seq: 1, + status: "failed", + }); + await insertV2Command(database, { + id: COMMAND_PERMISSION_ID, + kind: "permission.resolve", + payloadJson: permissionPayloadJson, + seq: 2, + status: "completed", + }); + + const inventory = await readLossyMigrationInventory(database); + expect(inventory.commandPayloadConflicts).toBe(2); + expect(inventory.permissionPayloadRewrites).toBe(1); + expect(inventory.candidateIds).toEqual([ + { category: "command_payload_conflict", id: COMMAND_ERROR_ID }, + { category: "command_payload_conflict", id: COMMAND_PERMISSION_ID }, + { category: "permission_payload_rewrite", id: COMMAND_PERMISSION_ID }, + ]); + await expect(applyMigration(database, "0013_durable-mcp-effect-v3")).rejects.toThrow(); + await expect( + database + .prepare("SELECT payload_json FROM driver_command WHERE id = ?") + .bind(COMMAND_ERROR_ID) + .first(), + ).resolves.toEqual({ payload_json: inputPayloadJson }); + await expect( + database + .prepare("SELECT payload_json FROM driver_command WHERE id = ?") + .bind(COMMAND_PERMISSION_ID) + .first(), + ).resolves.toEqual({ payload_json: permissionPayloadJson }); + }); + + test("rejects duplicate MCP result keys before SQLite can erase JS-visible conflicts", async () => { + const database = await createV2Database(); + await insertV2Owners(database); + const payloadJson = JSON.stringify({ + argumentsJson: "{}", + commandId: COMMAND_SUCCEEDED_ID, + kind: "mcp.execute", + requestId: "legacy-request", + serverId: SERVER_ID, + toolCallId: "legacy-tool-call", + toolName: "createIssue", + }); + const result = (lastOutput: string) => + `{"outputText":"shared-first","outputText":"${lastOutput}","requestId":"legacy-request","serverId":"${SERVER_ID}","toolName":"createIssue"}`; + const commandResultJson = result("command-last"); + const effectResultJson = result("effect-last"); + const attemptResultJson = result("attempt-last"); + + await insertV2Command(database, { + id: COMMAND_SUCCEEDED_ID, + kind: "mcp.execute", + payloadJson, + resultJson: commandResultJson, + seq: 1, + status: "completed", + }); + await insertV2Effect(database, { + commandId: COMMAND_SUCCEEDED_ID, + effectId: EFFECT_SUCCEEDED_ID, + resultJson: effectResultJson, + status: "succeeded", + toolName: "createIssue", + }); + await insertV2Attempt(database, { + effectId: EFFECT_SUCCEEDED_ID, + resultJson: attemptResultJson, + status: "succeeded", + }); + + const inventory = await readLossyMigrationInventory(database); + expect(inventory.mcpResultConflicts).toBe(1); + expect(inventory.candidateIds).toEqual([ + { category: "mcp_result_conflict", id: EFFECT_SUCCEEDED_ID }, + ]); + await expect(applyMigration(database, "0013_durable-mcp-effect-v3")).rejects.toThrow(); + expect(JSON.parse(commandResultJson)).toMatchObject({ outputText: "command-last" }); + expect(JSON.parse(effectResultJson)).toMatchObject({ outputText: "effect-last" }); + expect(JSON.parse(attemptResultJson)).toMatchObject({ outputText: "attempt-last" }); + await expect( + database + .prepare("SELECT result_json FROM driver_command WHERE id = ?") + .bind(COMMAND_SUCCEEDED_ID) + .first(), + ).resolves.toEqual({ result_json: commandResultJson }); + await expect( + database + .prepare("SELECT result_json FROM external_tool_effect WHERE id = ?") + .bind(EFFECT_SUCCEEDED_ID) + .first(), + ).resolves.toEqual({ result_json: effectResultJson }); + await expect( + database + .prepare("SELECT result_json FROM external_tool_effect_attempt WHERE effect_id = ?") + .bind(EFFECT_SUCCEEDED_ID) + .first(), + ).resolves.toEqual({ result_json: attemptResultJson }); + }); + + test("preserves the authoritative MCP result without a SQLite JSON round trip", async () => { + const database = await createV2Database(); + await insertV2Owners(database); + const payloadJson = JSON.stringify({ + argumentsJson: "{}", + commandId: COMMAND_SUCCEEDED_ID, + kind: "mcp.execute", + requestId: "legacy-request", + serverId: SERVER_ID, + toolCallId: "legacy-tool-call", + toolName: "createIssue", + }); + const resultJson = JSON.stringify({ + outputText: "\ud800", + requestId: "legacy-request", + serverId: SERVER_ID, + toolName: "createIssue", + }); + expect((JSON.parse(resultJson) as { outputText: string }).outputText.charCodeAt(0)).toBe( + 0xd800, + ); + await insertV2Command(database, { + id: COMMAND_SUCCEEDED_ID, + kind: "mcp.execute", + payloadJson, + resultJson, + seq: 1, + status: "completed", + }); + await insertV2Effect(database, { + commandId: COMMAND_SUCCEEDED_ID, + effectId: EFFECT_SUCCEEDED_ID, + resultJson, + status: "succeeded", + toolName: "createIssue", + }); + await insertV2Attempt(database, { + effectId: EFFECT_SUCCEEDED_ID, + resultJson, + status: "succeeded", + }); + expect((await readLossyMigrationInventory(database)).totalCandidates).toBe(0); + + await applyMigration(database, "0013_durable-mcp-effect-v3"); + for (const [table, predicate] of [ + ["driver_command", "id"], + ["external_tool_effect", "id"], + ["external_tool_effect_attempt", "effect_id"], + ] as const) { + const row = await database + .prepare(`SELECT result_json FROM ${table} WHERE ${predicate} = ?`) + .bind(table === "driver_command" ? COMMAND_SUCCEEDED_ID : EFFECT_SUCCEEDED_ID) + .first<{ result_json: string }>(); + expect(row?.result_json).toBe(resultJson); + expect( + (JSON.parse(row?.result_json ?? "null") as { outputText: string }).outputText.charCodeAt(0), + ).toBe(0xd800); + } + }); + + test("rejects conflicting MCP result and provider-receipt histories", async () => { + const database = await createV2Database(); + await insertV2Owners(database); + const payloadJson = JSON.stringify({ + argumentsJson: "{}", + commandId: COMMAND_SUCCEEDED_ID, + kind: "mcp.execute", + requestId: "legacy-request", + serverId: SERVER_ID, + toolCallId: "legacy-tool-call", + toolName: "createIssue", + }); + const result = (outputText: string) => + JSON.stringify({ + outputText, + requestId: "legacy-request", + serverId: SERVER_ID, + toolName: "createIssue", + }); + await insertV2Command(database, { + id: COMMAND_SUCCEEDED_ID, + kind: "mcp.execute", + payloadJson, + resultJson: result("command"), + seq: 1, + status: "completed", + }); + await insertV2Effect(database, { + commandId: COMMAND_SUCCEEDED_ID, + effectId: EFFECT_SUCCEEDED_ID, + providerReceiptJson: "effect-receipt", + resultJson: result("effect"), + status: "succeeded", + toolName: "createIssue", + }); + await insertV2Attempt(database, { + effectId: EFFECT_SUCCEEDED_ID, + providerReceiptJson: "attempt-receipt", + resultJson: result("attempt"), + status: "succeeded", + }); + + const inventory = await readLossyMigrationInventory(database); + expect(inventory.mcpResultConflicts).toBe(1); + expect(inventory.providerReceiptLosses).toBe(1); + expect(inventory.candidateIds).toEqual([ + { category: "mcp_result_conflict", id: EFFECT_SUCCEEDED_ID }, + { category: "provider_receipt_loss", id: EFFECT_SUCCEEDED_ID }, + ]); + await expect(applyMigration(database, "0013_durable-mcp-effect-v3")).rejects.toThrow(); + }); + + test("rejects result and receipt data attached to an unsettled MCP effect", async () => { + const database = await createV2Database(); + await insertV2Owners(database); + const resultJson = JSON.stringify({ + outputText: "unsettled output", + requestId: "legacy-request", + serverId: SERVER_ID, + toolName: "createIssue", + }); + await insertV2Command(database, { + id: COMMAND_UNKNOWN_ID, + kind: "mcp.execute", + payloadJson: JSON.stringify({ + argumentsJson: "{}", + commandId: COMMAND_UNKNOWN_ID, + kind: "mcp.execute", + requestId: "legacy-request", + serverId: SERVER_ID, + toolCallId: "legacy-tool-call", + toolName: "createIssue", + }), + seq: 1, + status: "failed", + }); + await insertV2Effect(database, { + commandId: COMMAND_UNKNOWN_ID, + effectId: EFFECT_UNKNOWN_ID, + providerReceiptJson: "unsettled-effect-receipt", + resultJson, + status: "executing", + toolName: "createIssue", + }); + await insertV2Attempt(database, { + effectId: EFFECT_UNKNOWN_ID, + providerReceiptJson: "unsettled-attempt-receipt", + resultJson, + status: "executing", + }); + + const inventory = await readLossyMigrationInventory(database); + expect(inventory.mcpResultConflicts).toBe(1); + expect(inventory.providerReceiptLosses).toBe(1); + await expect(applyMigration(database, "0013_durable-mcp-effect-v3")).rejects.toThrow(); + }); + + test("rejects a real v2 terminal MCP command without its effect fence", async () => { + const database = await createV2Database(); + await insertV2Owners(database); + const payloadJson = JSON.stringify({ + argumentsJson: "{}", + commandId: COMMAND_UNKNOWN_ID, + kind: "mcp.execute", + requestId: "legacy-request", + serverId: SERVER_ID, + toolCallId: "legacy-tool-call", + toolName: "createIssue", + }); + await insertV2Command(database, { + id: COMMAND_UNKNOWN_ID, + kind: "mcp.execute", + payloadJson, + seq: 1, + status: "failed", + }); + await expect( + database + .prepare( + "SELECT COUNT(*) AS count FROM driver_command AS command WHERE command.kind = 'mcp.execute' AND NOT EXISTS (SELECT 1 FROM external_tool_effect AS effect WHERE effect.command_id = command.id)", + ) + .first(), + ).resolves.toEqual({ count: 1 }); + + await expect(applyMigration(database, "0013_durable-mcp-effect-v3")).rejects.toThrow(); + await expect( + database + .prepare("SELECT payload_json FROM driver_command WHERE id = ?") + .bind(COMMAND_UNKNOWN_ID) + .first(), + ).resolves.toEqual({ payload_json: payloadJson }); + }); + + test("rejects an oversized source error with non-primitive details before omission", async () => { + const errorJson = jsonAtUtf8Size(OLD_V2_PAYLOAD_MAX_UTF8_BYTES, (padding) => ({ + code: "legacy.invalid_error", + details: { nested: { padding } }, + message: "Invalid legacy error.", + retryable: false, + })); + const database = await expectV2MigrationRejection(async (fixture) => { + await insertV2Command(fixture, { + errorJson, + id: COMMAND_ERROR_ID, + kind: "input.start", + payloadJson: JSON.stringify({ + commandId: COMMAND_ERROR_ID, + input: { text: "hello" }, + kind: "input.start", + requestId: "legacy-input-request", + runId: RUN_ID, + }), + seq: 1, + status: "failed", + }); + }); + + await expect( + database + .prepare("SELECT error_json FROM driver_command WHERE id = ?") + .bind(COMMAND_ERROR_ID) + .first(), + ).resolves.toEqual({ error_json: errorJson }); + }); + + test("rejects one malformed MCP result copy before authoritative synchronization", async () => { + const payloadJson = JSON.stringify({ + argumentsJson: "{}", + commandId: COMMAND_SUCCEEDED_ID, + kind: "mcp.execute", + requestId: "legacy-request", + serverId: SERVER_ID, + toolCallId: "legacy-tool-call", + toolName: "createIssue", + }); + const validResultJson = JSON.stringify({ + outputText: "done", + requestId: "legacy-request", + serverId: SERVER_ID, + toolName: "createIssue", + }); + const invalidAttemptResultJson = JSON.stringify({ + requestId: "legacy-request", + serverId: SERVER_ID, + toolName: "createIssue", + }); + const database = await expectV2MigrationRejection(async (fixture) => { + await insertV2Command(fixture, { + id: COMMAND_SUCCEEDED_ID, + kind: "mcp.execute", + payloadJson, + resultJson: validResultJson, + seq: 1, + status: "completed", + }); + await insertV2Effect(fixture, { + commandId: COMMAND_SUCCEEDED_ID, + effectId: EFFECT_SUCCEEDED_ID, + resultJson: validResultJson, + status: "succeeded", + toolName: "createIssue", + }); + await insertV2Attempt(fixture, { + effectId: EFFECT_SUCCEEDED_ID, + resultJson: invalidAttemptResultJson, + status: "succeeded", + }); + }); + + await expect( + database + .prepare("SELECT result_json FROM external_tool_effect_attempt WHERE effect_id = ?") + .bind(EFFECT_SUCCEEDED_ID) + .first(), + ).resolves.toEqual({ result_json: invalidAttemptResultJson }); + }); + + test("rejects undeclared MCP result fields before authoritative synchronization", async () => { + const payloadJson = JSON.stringify({ + argumentsJson: "{}", + commandId: COMMAND_SUCCEEDED_ID, + kind: "mcp.execute", + requestId: "legacy-request", + serverId: SERVER_ID, + toolCallId: "legacy-tool-call", + toolName: "createIssue", + }); + const resultJson = JSON.stringify({ + debug: "provider-specific data must not enter the public result", + outputText: "done", + requestId: "legacy-request", + serverId: SERVER_ID, + toolName: "createIssue", + }); + await expectV2MigrationRejection(async (fixture) => { + await insertV2Command(fixture, { + id: COMMAND_SUCCEEDED_ID, + kind: "mcp.execute", + payloadJson, + resultJson, + seq: 1, + status: "completed", + }); + await insertV2Effect(fixture, { + commandId: COMMAND_SUCCEEDED_ID, + effectId: EFFECT_SUCCEEDED_ID, + resultJson, + status: "succeeded", + toolName: "createIssue", + }); + await insertV2Attempt(fixture, { + effectId: EFFECT_SUCCEEDED_ID, + resultJson, + status: "succeeded", + }); + }); + }); + + test("rejects a missing input Run identity instead of fabricating one", async () => { + const payloadJson = jsonAtUtf8Size(OLD_V2_PAYLOAD_MAX_UTF8_BYTES, (text) => ({ + commandId: COMMAND_ERROR_ID, + input: { text }, + kind: "input.start", + requestId: "legacy-input-request", + })); + await expectV2MigrationRejection(async (fixture) => { + await insertV2Command(fixture, { + id: COMMAND_ERROR_ID, + kind: "input.start", + payloadJson, + seq: 1, + status: "failed", + }); + }); + }); + + test("rejects empty input attachment identities that Driver cannot execute", async () => { + await expectV2MigrationRejection(async (fixture) => { + await insertV2Command(fixture, { + id: COMMAND_ERROR_ID, + kind: "input.start", + payloadJson: JSON.stringify({ + commandId: COMMAND_ERROR_ID, + input: { attachmentIds: [""], text: "hello" }, + kind: "input.start", + requestId: "legacy-input-request", + runId: RUN_ID, + }), + seq: 1, + status: "failed", + }); + }); + }); + + test("rejects an invalid effect attempt audit instead of inventing a current claim", async () => { + const payloadJson = JSON.stringify({ + argumentsJson: "{}", + commandId: COMMAND_UNKNOWN_ID, + kind: "mcp.execute", + requestId: "legacy-request", + serverId: SERVER_ID, + toolCallId: "legacy-tool-call", + toolName: "createIssue", + }); + await expectV2MigrationRejection(async (fixture) => { + await insertV2Command(fixture, { + id: COMMAND_UNKNOWN_ID, + kind: "mcp.execute", + payloadJson, + seq: 1, + status: "failed", + }); + await insertV2Effect(fixture, { + commandId: COMMAND_UNKNOWN_ID, + effectId: EFFECT_UNKNOWN_ID, + resultJson: null, + status: "executing", + toolName: "createIssue", + }); + await insertV2Attempt(fixture, { + effectId: EFFECT_UNKNOWN_ID, + resultJson: null, + status: "executing", + }); + await fixture + .prepare("UPDATE external_tool_effect SET attempt_count = 2 WHERE id = ?") + .bind(EFFECT_UNKNOWN_ID) + .run(); + }); + }); + + test("rejects a completed command whose effect remains unknown", async () => { + const payloadJson = JSON.stringify({ + argumentsJson: "{}", + commandId: COMMAND_UNKNOWN_ID, + kind: "mcp.execute", + requestId: "legacy-request", + serverId: SERVER_ID, + toolCallId: "legacy-tool-call", + toolName: "createIssue", + }); + const resultJson = JSON.stringify({ + outputText: "unproven", + requestId: "legacy-request", + serverId: SERVER_ID, + toolName: "createIssue", + }); + await expectV2MigrationRejection(async (fixture) => { + await insertV2Command(fixture, { + id: COMMAND_UNKNOWN_ID, + kind: "mcp.execute", + payloadJson, + resultJson, + seq: 1, + status: "completed", + }); + await insertV2Effect(fixture, { + commandId: COMMAND_UNKNOWN_ID, + effectId: EFFECT_UNKNOWN_ID, + resultJson: null, + status: "executing", + toolName: "createIssue", + }); + await insertV2Attempt(fixture, { + effectId: EFFECT_UNKNOWN_ID, + resultJson: null, + status: "executing", + }); + }); + }); + + test("rejects nested Session Run error details before oversized repair", async () => { + await expectV2MigrationRejection(async (fixture) => { + await fixture + .prepare( + "UPDATE session_run SET error_code = ?, error_details_json = ?, error_message = ? WHERE id = ?", + ) + .bind( + "legacy.invalid_session_error", + JSON.stringify({ nested: { invalid: true } }), + "Invalid Session Run error.", + RUN_ID, + ) + .run(); + }); + }); + + test("rejects missing permission decisions and RunError retryability", async () => { + await expectV2MigrationRejection(async (fixture) => { + await insertV2Command(fixture, { + errorJson: JSON.stringify({ + code: "legacy.missing_retryable", + details: {}, + message: "Missing retryability.", + }), + id: COMMAND_ERROR_ID, + kind: "permission.resolve", + payloadJson: JSON.stringify({ + commandId: COMMAND_ERROR_ID, + kind: "permission.resolve", + requestId: "legacy-permission-request", + }), + seq: 1, + status: "failed", + }); + }); + }); +}); + +describe("session event v3 migration", () => { + test("leaves the canonical deploy lease table on a direct no-candidate migration", async () => { + const database = await createV2Database(); + await applyMigration(database, "0013_durable-mcp-effect-v3"); + await applyMigration(database, "0014_session-event-stream-identity"); + + const results = await database.batch( + acquireProdDeployLeaseStatements(DEPLOY_OWNER).map((sql) => database.prepare(sql)), + ); + expect(() => assertProdDeployLeaseOwned(JSON.stringify(results), DEPLOY_OWNER)).not.toThrow(); + }); + + for (const fixture of [ + { eventType: "run.completed", status: "completed", statusEvent: "run.complete" }, + { eventType: "run.cancelled", status: "cancelled", statusEvent: "run.cancel" }, + { eventType: "run.failed", status: "failed", statusEvent: "run.fail" }, + ] as const) { + test(`normalizes a provider-source ${fixture.eventType} through the exact cutover gate`, async () => { + const database = await createV2Database(); + await insertV2Owners(database); + await database + .prepare( + `UPDATE session_run + SET error_code = ?, + error_details_json = ?, + error_message = ?, + status = ?, + status_event = ? + WHERE id = ?`, + ) + .bind( + fixture.status === "failed" ? "legacy.failed" : null, + fixture.status === "failed" ? "{}" : null, + fixture.status === "failed" ? "Legacy failure." : null, + fixture.status, + fixture.statusEvent, + RUN_ID, + ) + .run(); + database.execute( + `UPDATE session SET runtime_event_seq_cursor = 1 WHERE id = '${SESSION_ID}'`, + ); + await insertLegacySessionTerminal(database, { + eventId: LEGACY_TERMINAL_EVENT_ID, + eventType: fixture.eventType, + sourceEventId: `provider-${fixture.status}-event`, + }); + + await applyMigration(database, "0013_durable-mcp-effect-v3"); + await authorizeLegacyTerminalRewrite(database); + await applyMigration(database, "0014_session-event-stream-identity"); + + await expect(database.prepare(PROTOCOL_V3_CUTOVER_OBJECTS_SQL).first()).resolves.toEqual({ + exact_object_count: PROTOCOL_V3_CUTOVER_OBJECT_COUNT, + object_count: PROTOCOL_V3_CUTOVER_OBJECT_COUNT, + }); + + await expect( + database + .prepare( + "SELECT count(*) AS count FROM sqlite_master WHERE name = '__protocol_v3_legacy_rewrite_authorization'", + ) + .first(), + ).resolves.toEqual({ count: 0 }); + + await expect( + database + .prepare("SELECT id, semantic_hash, seq, source_event_id FROM session_event WHERE id = ?") + .bind(LEGACY_TERMINAL_EVENT_ID) + .first(), + ).resolves.toEqual({ + id: LEGACY_TERMINAL_EVENT_ID, + semantic_hash: null, + seq: 1, + source_event_id: `session-run-terminal:${RUN_ID}:${fixture.eventType}`, + }); + }); + } + + test("rejects an otherwise valid provider-source rewrite without deploy authorization", async () => { + const database = await createV2Database(); + await insertV2Owners(database); + database.execute(` + UPDATE session SET runtime_event_seq_cursor = 1 WHERE id = '${SESSION_ID}'; + UPDATE session_run + SET error_code = NULL, error_details_json = NULL, error_message = NULL, + status = 'completed', status_event = 'run.complete' + WHERE id = '${RUN_ID}'; + `); + await insertLegacySessionTerminal(database, { + eventId: LEGACY_TERMINAL_EVENT_ID, + eventType: "run.completed", + sourceEventId: "provider-completed-event", + }); + await applyMigration(database, "0013_durable-mcp-effect-v3"); + + await expect(applyMigration(database, "0014_session-event-stream-identity")).rejects.toThrow(); + await expect( + database + .prepare("SELECT source_event_id FROM session_event WHERE id = ?") + .bind(LEGACY_TERMINAL_EVENT_ID) + .first(), + ).resolves.toEqual({ source_event_id: "provider-completed-event" }); + }); + + test("does not authorize a candidate set that changed after the integrity preflight", async () => { + const database = await createV2Database(); + await insertV2Owners(database); + database.execute(` + UPDATE session SET runtime_event_seq_cursor = 1 WHERE id = '${SESSION_ID}'; + UPDATE session_run + SET error_code = NULL, error_details_json = NULL, error_message = NULL, + status = 'completed', status_event = 'run.complete' + WHERE id = '${RUN_ID}'; + `); + await insertLegacySessionTerminal(database, { + eventId: LEGACY_TERMINAL_EVENT_ID, + eventType: "run.completed", + sourceEventId: "provider-completed-event", + }); + await applyMigration(database, "0013_durable-mcp-effect-v3"); + const integrity = await readLegacyTerminalIntegrity(database); + installLegacyRewriteAuthorizationInfrastructure(database); + + database.execute( + `UPDATE session_event SET source_event_id = 'provider-changed-event' WHERE id = '${LEGACY_TERMINAL_EVENT_ID}'`, + ); + database.execute( + authorizeProtocolV3LegacyRewriteSql( + DEPLOY_OWNER, + integrity.noncanonicalTerminalSources, + integrity.rewriteCandidateManifestJson, + ), + ); + + await expect( + database + .prepare( + "SELECT count(*) AS count FROM __protocol_v3_legacy_rewrite_authorization WHERE id = 1", + ) + .first(), + ).resolves.toEqual({ count: 0 }); + await expect(applyMigration(database, "0014_session-event-stream-identity")).rejects.toThrow(); + }); + + test("revokes stale authorization when a retry no longer proves the frozen gate", async () => { + const database = await createV2Database(); + await insertV2Owners(database); + database.execute(` + UPDATE session SET runtime_event_seq_cursor = 1 WHERE id = '${SESSION_ID}'; + UPDATE session_run + SET error_code = NULL, error_details_json = NULL, error_message = NULL, + status = 'completed', status_event = 'run.complete' + WHERE id = '${RUN_ID}'; + `); + await insertLegacySessionTerminal(database, { + eventId: LEGACY_TERMINAL_EVENT_ID, + eventType: "run.completed", + sourceEventId: "provider-completed-event", + }); + await applyMigration(database, "0013_durable-mcp-effect-v3"); + installLegacyRewriteAuthorizationInfrastructure(database); + const integrity = await readLegacyTerminalIntegrity(database); + const authorizationSql = authorizeProtocolV3LegacyRewriteSql( + DEPLOY_OWNER, + integrity.noncanonicalTerminalSources, + integrity.rewriteCandidateManifestJson, + ); + database.execute(authorizationSql); + database.execute(`UPDATE "${PROTOCOL_V3_CUTOVER_TABLE}" SET "command_freeze" = 0`); + database.execute(authorizationSql); + + await expect( + database + .prepare( + "SELECT count(*) AS count FROM __protocol_v3_legacy_rewrite_authorization WHERE id = 1", + ) + .first(), + ).resolves.toEqual({ count: 0 }); + await expect(applyMigration(database, "0014_session-event-stream-identity")).rejects.toThrow(); + }); + + for (const fixture of [ + { + authorizationRemains: true, + label: "the authorized candidate identity changes", + mutate: (database: SqliteD1Database) => + database.execute( + `UPDATE session_event SET source_event_id = 'provider-changed-event' WHERE id = '${LEGACY_TERMINAL_EVENT_ID}'`, + ), + }, + { + authorizationRemains: true, + label: "the durable deploy mutex owner changes", + mutate: (database: SqliteD1Database) => + database.execute( + `UPDATE "${PROD_DEPLOY_LEASE_TABLE}" SET "owner" = '${OTHER_DEPLOY_OWNER}'`, + ), + }, + { + authorizationRemains: true, + label: "the rewrite authorization expires", + mutate: (database: SqliteD1Database) => + database.execute( + "UPDATE __protocol_v3_legacy_rewrite_authorization SET expires_at = 0 WHERE id = 1", + ), + }, + { + authorizationRemains: true, + label: "the terminal projection falls behind its Session cursor proof", + mutate: (database: SqliteD1Database) => + database.execute( + `UPDATE session SET runtime_event_seq_cursor = 0 WHERE id = '${SESSION_ID}'`, + ), + }, + { + authorizationRemains: false, + label: "the admission freeze is disabled", + mutate: (database: SqliteD1Database) => + database.execute(`UPDATE "${PROTOCOL_V3_CUTOVER_TABLE}" SET command_freeze = 0`), + }, + { + authorizationRemains: false, + label: "the bound bookmark changes", + mutate: (database: SqliteD1Database) => + database.execute( + `UPDATE "${PROTOCOL_V3_CUTOVER_TABLE}" SET pre_migration_bookmark = 'changed-bookmark'`, + ), + }, + { + authorizationRemains: false, + label: "the bound release tree changes", + mutate: (database: SqliteD1Database) => + database.execute( + `UPDATE "${PROTOCOL_V3_CUTOVER_TABLE}" SET release_tree_oid = '89abcdef0123456789abcdef0123456789abcdef'`, + ), + }, + { + authorizationRemains: false, + label: "the admission gate is removed", + mutate: (database: SqliteD1Database) => database.execute(REMOVE_PROTOCOL_V3_CUTOVER_SQL), + }, + { + authorizationRemains: true, + label: "an extra trigger is attached to an admission table", + mutate: (database: SqliteD1Database) => + database.execute(` + CREATE TRIGGER disable_protocol_v3_gate + BEFORE INSERT ON session_run + WHEN 0 + BEGIN + SELECT 1; + END + `), + }, + { + authorizationRemains: true, + label: "an extra trigger is attached to App deployment admission", + mutate: (database: SqliteD1Database) => + database.execute(` + CREATE TRIGGER disable_app_deployment_gate + BEFORE INSERT ON app_deployment_run + WHEN 0 + BEGIN + SELECT 1; + END + `), + }, + { + authorizationRemains: true, + label: "an extra trigger is attached to the rewrite authorization table", + mutate: (database: SqliteD1Database) => + database.execute(` + CREATE TRIGGER retain_protocol_v3_authorization + AFTER DELETE ON __protocol_v3_legacy_rewrite_authorization + WHEN 0 + BEGIN + SELECT 1; + END + `), + }, + { + authorizationRemains: true, + label: "an extra trigger is attached to the deploy lease table", + mutate: (database: SqliteD1Database) => + database.execute(` + CREATE TRIGGER spoof_protocol_v3_deploy_lease + AFTER UPDATE ON "${PROD_DEPLOY_LEASE_TABLE}" + WHEN 0 + BEGIN + SELECT 1; + END + `), + }, + { + authorizationRemains: true, + label: "a canonical admission trigger is replaced by WHEN 0", + mutate: (database: SqliteD1Database) => + database.execute(` + DROP TRIGGER "__protocol_v3_cutover_session_run_insert"; + CREATE TRIGGER "__protocol_v3_cutover_session_run_insert" + BEFORE INSERT ON session_run + WHEN 0 + BEGIN + SELECT 1; + END + `), + }, + ]) { + test(`rejects the legacy rewrite after ${fixture.label}`, async () => { + const database = await createV2Database(); + await insertV2Owners(database); + database.execute(` + UPDATE session SET runtime_event_seq_cursor = 1 WHERE id = '${SESSION_ID}'; + UPDATE session_run + SET error_code = NULL, error_details_json = NULL, error_message = NULL, + status = 'completed', status_event = 'run.complete' + WHERE id = '${RUN_ID}'; + `); + await insertLegacySessionTerminal(database, { + eventId: LEGACY_TERMINAL_EVENT_ID, + eventType: "run.completed", + sourceEventId: "provider-completed-event", + }); + await applyMigration(database, "0013_durable-mcp-effect-v3"); + await authorizeLegacyTerminalRewrite(database); + fixture.mutate(database); + + await expect( + applyMigration(database, "0014_session-event-stream-identity"), + ).rejects.toThrow(); + await expect( + database + .prepare( + "SELECT candidate_count FROM __protocol_v3_legacy_rewrite_authorization WHERE id = 1", + ) + .first(), + ).resolves.toEqual(fixture.authorizationRemains ? { candidate_count: 1 } : null); + }); + } + + test("leaves an already-canonical legacy terminal identity unchanged", async () => { + const database = await createV2Database(); + await insertV2Owners(database); + await database + .prepare( + `UPDATE session_run + SET error_code = 'legacy.failed', error_details_json = '{}', + error_message = 'Legacy failure.', status_event = 'run.fail' + WHERE id = ?`, + ) + .bind(RUN_ID) + .run(); + database.execute(`UPDATE session SET runtime_event_seq_cursor = 1 WHERE id = '${SESSION_ID}'`); + const canonicalSource = `session-run-terminal:${RUN_ID}:run.failed`; + await insertLegacySessionTerminal(database, { + eventId: LEGACY_TERMINAL_EVENT_ID, + eventType: "run.failed", + sourceEventId: canonicalSource, + }); + + await applyMigration(database, "0013_durable-mcp-effect-v3"); + await applyMigration(database, "0014_session-event-stream-identity"); + + await expect( + database + .prepare("SELECT id, semantic_hash, seq, source_event_id FROM session_event WHERE id = ?") + .bind(LEGACY_TERMINAL_EVENT_ID) + .first(), + ).resolves.toEqual({ + id: LEGACY_TERMINAL_EVENT_ID, + semantic_hash: null, + seq: 1, + source_event_id: canonicalSource, + }); + }); + + test("rejects a legacy terminal source rewrite collision atomically", async () => { + const database = await createV2Database(); + await insertV2Owners(database); + await database + .prepare( + "UPDATE session_run SET status = 'completed', status_event = 'run.complete' WHERE id = ?", + ) + .bind(RUN_ID) + .run(); + await insertLegacySessionTerminal(database, { + eventId: LEGACY_TERMINAL_EVENT_ID, + eventType: "run.completed", + sourceEventId: "provider-completed-event", + }); + await database + .prepare( + `INSERT INTO session_event ( + agent_id, content_text, created_at, ended_at, event_type, family, id, + occurred_at, process_status, process_type, run_id, seq, session_id, + source_event_id, source, visibility + ) VALUES (?, '', 4, 4, 'message.added', 'message', ?, 4, 'available', + 'agent.message.delta', ?, 2, ?, ?, 'api', 'all_consumers')`, + ) + .bind( + AGENT_ID, + "01J00000000000000000000022", + RUN_ID, + SESSION_ID, + `session-run-terminal:${RUN_ID}:run.completed`, + ) + .run(); + + await applyMigration(database, "0013_durable-mcp-effect-v3"); + await expect(applyMigration(database, "0014_session-event-stream-identity")).rejects.toThrow(); + await expect( + database + .prepare("SELECT source_event_id FROM session_event WHERE id = ?") + .bind(LEGACY_TERMINAL_EVENT_ID) + .first(), + ).resolves.toEqual({ source_event_id: "provider-completed-event" }); + }); + + test("rejects multiple legacy terminal winners before rewriting either source", async () => { + const database = await createV2Database(); + await insertV2Owners(database); + await database + .prepare( + "UPDATE session_run SET status = 'completed', status_event = 'run.complete' WHERE id = ?", + ) + .bind(RUN_ID) + .run(); + await insertLegacySessionTerminal(database, { + eventId: LEGACY_TERMINAL_EVENT_ID, + eventType: "run.completed", + sourceEventId: "provider-completed-event-1", + }); + await insertLegacySessionTerminal(database, { + eventId: "01J00000000000000000000022", + eventType: "run.completed", + seq: 2, + sourceEventId: "provider-completed-event-2", + }); + + await applyMigration(database, "0013_durable-mcp-effect-v3"); + await expect(applyMigration(database, "0014_session-event-stream-identity")).rejects.toThrow(); + await expect( + database + .prepare("SELECT source_event_id FROM session_event WHERE run_id = ? ORDER BY seq") + .bind(RUN_ID) + .all(), + ).resolves.toMatchObject({ + results: [ + { source_event_id: "provider-completed-event-1" }, + { source_event_id: "provider-completed-event-2" }, + ], + }); + }); + + for (const fixture of [ + { + eventType: "run.completed", + label: "a terminal kind that conflicts with the Run status", + runId: RUN_ID, + }, + { + eventType: "run.failed", + label: "a terminal event whose Run link does not exist", + runId: "01J00000000000000000000023", + }, + ] as const) { + test(`rejects ${fixture.label} before source normalization`, async () => { + const database = await createV2Database(); + await insertV2Owners(database); + await database + .prepare("UPDATE session_run SET status_event = 'run.fail' WHERE id = ?") + .bind(RUN_ID) + .run(); + await insertLegacySessionTerminal(database, { + eventId: LEGACY_TERMINAL_EVENT_ID, + eventType: fixture.eventType, + runId: fixture.runId, + sourceEventId: "provider-terminal-event", + }); + + await applyMigration(database, "0013_durable-mcp-effect-v3"); + await expect( + applyMigration(database, "0014_session-event-stream-identity"), + ).rejects.toThrow(); + await expect( + database + .prepare("SELECT source_event_id FROM session_event WHERE id = ?") + .bind(LEGACY_TERMINAL_EVENT_ID) + .first(), + ).resolves.toEqual({ source_event_id: "provider-terminal-event" }); + }); + } + + test("labels legacy projections and backfills retryable Run errors without inventing receipts", async () => { + const database = await createV2Database(); + await insertV2Owners(database); + database.execute(` + INSERT INTO session_message ( + content_text, created_at, created_by_account_id, id, plan_json, role, + segments_json, seq, session_id, session_run_id + ) VALUES ( + 'legacy materialized message', 2, '${ACCOUNT_ID}', '${LEGACY_MESSAGE_ID}', + NULL, 'user', NULL, 1, '${SESSION_ID}', NULL + ); + + UPDATE session_run + SET error_code = 'legacy.failed', + error_details_json = NULL, + error_message = 'Legacy failure.', + status_event = 'run.fail' + WHERE id = '${RUN_ID}'; + + INSERT INTO session_event ( + agent_id, content_text, created_at, ended_at, event_type, family, id, + occurred_at, process_status, process_type, run_id, seq, session_id, + source_event_id, source, visibility + ) VALUES ( + '${AGENT_ID}', 'Legacy failure.', 3, 3, 'run.failed', 'run', + '${LEGACY_TERMINAL_EVENT_ID}', 3, 'error', 'run.failed', '${RUN_ID}', 1, + '${SESSION_ID}', 'legacy-terminal-source', 'api', 'all_consumers' + ); + UPDATE session SET runtime_event_seq_cursor = 1 WHERE id = '${SESSION_ID}'; + `); + + await applyMigration(database, "0013_durable-mcp-effect-v3"); + await authorizeLegacyTerminalRewrite(database); + await applyMigration(database, "0014_session-event-stream-identity"); + + await expect( + database + .prepare("SELECT projection_format FROM session_message WHERE id = ?") + .bind(LEGACY_MESSAGE_ID) + .first(), + ).resolves.toEqual({ projection_format: "materialized" }); + await expect( + database + .prepare("SELECT error_details_json, error_retryable FROM session_run WHERE id = ?") + .bind(RUN_ID) + .first(), + ).resolves.toEqual({ error_details_json: "{}", error_retryable: 0 }); + await expect( + database + .prepare("SELECT semantic_hash, source_event_id FROM session_event WHERE id = ?") + .bind(LEGACY_TERMINAL_EVENT_ID) + .first(), + ).resolves.toEqual({ + semantic_hash: null, + source_event_id: `session-run-terminal:${RUN_ID}:run.failed`, + }); + await expect( + database + .prepare("UPDATE session_message SET projection_format = 'invalid' WHERE id = ?") + .bind(LEGACY_MESSAGE_ID) + .run(), + ).rejects.toThrow(); + await database + .prepare( + `INSERT INTO session_message ( + content_text, created_at, created_by_account_id, id, plan_json, + projection_format, role, segments_json, seq, session_id, session_run_id + ) VALUES ('', 4, ?, ?, NULL, 'event_stream_v3', 'assistant', NULL, 2, ?, ?)`, + ) + .bind(ACCOUNT_ID, V3_REFERENCE_MESSAGE_ID, SESSION_ID, RUN_ID) + .run(); + for (const statement of [ + "UPDATE session_message SET role = 'user' WHERE id = ?", + "UPDATE session_message SET session_run_id = NULL WHERE id = ?", + "UPDATE session_message SET content_text = 'materialized' WHERE id = ?", + "UPDATE session_message SET plan_json = '{}' WHERE id = ?", + "UPDATE session_message SET segments_json = '[]' WHERE id = ?", + ]) { + await expect( + database.prepare(statement).bind(V3_REFERENCE_MESSAGE_ID).run(), + ).rejects.toThrow(); + } + await expect(database.prepare("PRAGMA foreign_key_check").all()).resolves.toMatchObject({ + results: [], + success: true, + }); + }); +}); diff --git a/apps/api/tests/external-tool-effect-store.test.ts b/apps/api/tests/external-tool-effect-store.test.ts new file mode 100644 index 00000000..e6b5ea97 --- /dev/null +++ b/apps/api/tests/external-tool-effect-store.test.ts @@ -0,0 +1,711 @@ +import { describe, expect, test } from "bun:test"; + +import { + MCP_EXTERNAL_TOOL_EFFECT_SETTLEMENT_MAX_UTF8_BYTES, + measureMcpExternalToolEffectSettlement, +} from "@mosoo/contracts/external-tool-effect"; +import { + RUNTIME_COMMAND_MAX_UTF8_BYTES, + measureRuntimeCommandJson, +} from "@mosoo/contracts/runtime-command"; +import type { RuntimeCommand } from "@mosoo/contracts/runtime-command"; +import type { + DriverCommandId, + DriverInstanceId, + ExternalToolEffectId, + SessionRunId, +} from "@mosoo/id"; + +import { + claimExternalToolEffect as claimExternalToolEffectRecord, + getExternalToolEffectForCommand as getExternalToolEffectForCommandRecord, + markClaimedExternalToolEffectsUnknownForDriver, + settleExternalToolEffect as settleExternalToolEffectRecord, +} from "../src/modules/runtime/infrastructure/session-runs/external-tool-effect-store.repository"; +import { + createRuntimeCommandRecord as persistRuntimeCommandRecord, + getRuntimeCommandRecord as readRuntimeCommandRecord, + listAcceptedInputStartCommandRepairsForTerminalDriver, + listAcceptedMcpCommandRepairsForTerminalDriver, + repairAcceptedRuntimeCommandsForTerminalDriver, +} from "../src/modules/runtime/infrastructure/session-runs/runtime-command-store.repository"; +import { + createPublicHttpContractDatabase, + insertActiveSandboxSessionFixture, + insertOwnerSession, + PUBLIC_API_TEST_IDS, +} from "./helpers/public-api-http-test-fixture"; +import type { SqliteD1Database } from "./helpers/public-api-http-test-fixture"; + +const RUN_ID = "01J0000000000000000000000T" as SessionRunId; +const NEXT_RUN_ID = "01J0000000000000000000000V" as SessionRunId; +const COMMAND_ID = "01J0000000000000000000000X" as DriverCommandId; +const BOUNDARY_COMMAND_ID = "01J0000000000000000000000Z" as DriverCommandId; +const OVERSIZED_COMMAND_ID = "01J00000000000000000000010" as DriverCommandId; +const DRIVER_INSTANCE_ID = PUBLIC_API_TEST_IDS.driverOwner as DriverInstanceId; +const DRIVER_GENERATION = 0; +const CLAIM_TOKEN = "123e4567-e89b-42d3-a456-426614174000"; + +function createRuntimeCommandRecord( + database: D1Database, + input: Omit[1], "driverGeneration">, +) { + return persistRuntimeCommandRecord(database, { ...input, driverGeneration: DRIVER_GENERATION }); +} + +function getRuntimeCommandRecord( + database: D1Database, + driverInstanceId: DriverInstanceId, + commandId: DriverCommandId, +) { + return readRuntimeCommandRecord(database, driverInstanceId, DRIVER_GENERATION, commandId); +} + +function claimExternalToolEffect( + database: D1Database, + input: Omit[1], "driverGeneration">, +) { + return claimExternalToolEffectRecord(database, { ...input, driverGeneration: DRIVER_GENERATION }); +} + +function settleExternalToolEffect( + database: D1Database, + input: Omit[1], "driverGeneration">, +) { + return settleExternalToolEffectRecord(database, { + ...input, + driverGeneration: DRIVER_GENERATION, + }); +} + +function getExternalToolEffectForCommand( + database: D1Database, + input: Omit[1], "driverGeneration">, +) { + return getExternalToolEffectForCommandRecord(database, { + ...input, + driverGeneration: DRIVER_GENERATION, + }); +} + +function mcpExecuteCommand( + runId: SessionRunId, + commandId: DriverCommandId = COMMAND_ID, +): Extract { + return { + argumentsJson: '{"title":"durable"}', + commandId, + kind: "mcp.execute", + requestId: "request-1", + runId, + serverId: "01J0000000000000000000000Y", + toolCallId: "tool-1", + toolName: "createIssue", + }; +} + +function mcpExecuteCommandAtSize( + targetBytes: number, + commandId: DriverCommandId = BOUNDARY_COMMAND_ID, +) { + const command = { + ...mcpExecuteCommand(RUN_ID, commandId), + argumentsJson: "", + }; + return { + ...command, + argumentsJson: "x".repeat(targetBytes - measureRuntimeCommandJson(command)), + }; +} + +function succeededSettlementAtSize(targetBytes: number) { + const settlement = { + kind: "succeeded" as const, + result: { + outputText: "", + requestId: "request-1", + serverId: "01J0000000000000000000000Y", + toolName: "createIssue", + }, + }; + return { + ...settlement, + result: { + ...settlement.result, + outputText: "x".repeat(targetBytes - measureMcpExternalToolEffectSettlement(settlement)), + }, + }; +} + +async function insertSessionRun(database: SqliteD1Database, runId: SessionRunId): Promise { + await database + .prepare( + "INSERT INTO session_run (id, session_id, agent_id, created_by_account_id, driver_instance_id, trigger, status, trace_id, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", + ) + .bind( + runId, + PUBLIC_API_TEST_IDS.ownerSession, + PUBLIC_API_TEST_IDS.agent, + PUBLIC_API_TEST_IDS.ownerAccount, + DRIVER_INSTANCE_ID, + "user_prompt", + "running", + `trace-${runId}`, + 1, + 1, + ) + .run(); +} + +async function createEffectStoreFixture(): Promise { + const database = await createPublicHttpContractDatabase(); + await insertOwnerSession(database); + await insertActiveSandboxSessionFixture(database, { + ownerAccountId: PUBLIC_API_TEST_IDS.ownerAccount, + sandboxId: PUBLIC_API_TEST_IDS.sandbox, + sandboxSessionId: "01J0000000000000000000000W", + sessionId: PUBLIC_API_TEST_IDS.ownerSession, + }); + await database + .prepare( + "INSERT INTO driver_instance (id, boot_token_expires_at, boot_token_hash, connection_id, created_at, expires_at, heartbeat_count, protocol, protocol_version, runtime, sandbox_id, sandbox_incarnation, sandbox_session_id, status, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", + ) + .bind( + DRIVER_INSTANCE_ID, + 1, + new Uint8Array([1]), + "connection-current", + 1, + Date.now() + 60_000, + 0, + "orpc-ws", + 3, + "openai-runtime", + PUBLIC_API_TEST_IDS.sandbox, + 1, + PUBLIC_API_TEST_IDS.ownerSession, + "ready", + 1, + ) + .run(); + await insertSessionRun(database, RUN_ID); + return database; +} + +describe("external tool effect store", () => { + test("atomically rejects commands after the exact Driver generation becomes terminal", async () => { + const database = await createEffectStoreFixture(); + await database + .prepare("UPDATE driver_instance SET status = 'failed' WHERE id = ?") + .bind(DRIVER_INSTANCE_ID) + .run(); + + await expect( + createRuntimeCommandRecord(database, { + command: { + commandId: COMMAND_ID, + kind: "session.stop", + reason: "already terminal", + }, + driverInstanceId: DRIVER_INSTANCE_ID, + }), + ).rejects.toThrow("Driver generation is no longer current."); + await expect( + database + .prepare( + "SELECT command_seq_cursor, (SELECT count(*) FROM driver_command) AS command_count FROM driver_instance WHERE id = ?", + ) + .bind(DRIVER_INSTANCE_ID) + .first(), + ).resolves.toEqual({ command_count: 0, command_seq_cursor: 0 }); + }); + + test("atomically rejects a command for an inactive exact Run", async () => { + const database = await createEffectStoreFixture(); + await database + .prepare("UPDATE session_run SET status = 'failed' WHERE id = ?") + .bind(RUN_ID) + .run(); + await insertSessionRun(database, NEXT_RUN_ID); + + await expect( + createRuntimeCommandRecord(database, { + command: mcpExecuteCommand(RUN_ID), + driverInstanceId: DRIVER_INSTANCE_ID, + status: "accepted", + }), + ).rejects.toThrow("MCP external tool effects require the command's active Session Run."); + await expect( + getRuntimeCommandRecord(database, DRIVER_INSTANCE_ID, COMMAND_ID), + ).resolves.toBeNull(); + await expect( + database.prepare("SELECT COUNT(*) AS count FROM external_tool_effect").first(), + ).resolves.toEqual({ count: 0 }); + }); + + test("does not claim an intent after its exact Run becomes terminal", async () => { + const database = await createEffectStoreFixture(); + await createRuntimeCommandRecord(database, { + command: mcpExecuteCommand(RUN_ID), + driverInstanceId: DRIVER_INSTANCE_ID, + status: "accepted", + }); + await database + .prepare("UPDATE session_run SET status = 'failed' WHERE id = ?") + .bind(RUN_ID) + .run(); + + await expect( + claimExternalToolEffect(database, { + claimToken: CLAIM_TOKEN, + commandId: COMMAND_ID, + driverInstanceId: DRIVER_INSTANCE_ID, + }), + ).rejects.toThrow("External tool effect claim did not reach a stable state."); + await expect( + getExternalToolEffectForCommand(database, { + commandId: COMMAND_ID, + driverInstanceId: DRIVER_INSTANCE_ID, + }), + ).resolves.toMatchObject({ attemptCount: 0, claimToken: null, status: "intent" }); + await expect( + database.prepare("SELECT COUNT(*) AS count FROM external_tool_effect_attempt").first(), + ).resolves.toEqual({ count: 0 }); + }); + + test("rejects malformed claim tokens before claim or settlement mutation", async () => { + const database = await createEffectStoreFixture(); + await createRuntimeCommandRecord(database, { + command: mcpExecuteCommand(RUN_ID), + driverInstanceId: DRIVER_INSTANCE_ID, + status: "accepted", + }); + + await expect( + claimExternalToolEffect(database, { + claimToken: "x".repeat(1_000_000), + commandId: COMMAND_ID, + driverInstanceId: DRIVER_INSTANCE_ID, + }), + ).rejects.toThrow(); + const claim = await claimExternalToolEffect(database, { + claimToken: CLAIM_TOKEN, + commandId: COMMAND_ID, + driverInstanceId: DRIVER_INSTANCE_ID, + }); + if (claim.kind !== "claimed") { + throw new Error("Expected the external effect to be claimed."); + } + + await expect( + settleExternalToolEffect(database, { + claimToken: "not-a-uuid", + commandId: COMMAND_ID, + driverInstanceId: DRIVER_INSTANCE_ID, + effectId: claim.effectId as ExternalToolEffectId, + settlement: { kind: "unknown" }, + }), + ).rejects.toThrow(); + await expect( + getExternalToolEffectForCommand(database, { + commandId: COMMAND_ID, + driverInstanceId: DRIVER_INSTANCE_ID, + }), + ).resolves.toMatchObject({ claimToken: CLAIM_TOKEN, status: "claimed" }); + }); + + test("rejects an invalid settlement before mutating the effect or attempt", async () => { + const database = await createEffectStoreFixture(); + await createRuntimeCommandRecord(database, { + command: mcpExecuteCommand(RUN_ID), + driverInstanceId: DRIVER_INSTANCE_ID, + status: "accepted", + }); + const claim = await claimExternalToolEffect(database, { + claimToken: CLAIM_TOKEN, + commandId: COMMAND_ID, + driverInstanceId: DRIVER_INSTANCE_ID, + }); + if (claim.kind !== "claimed") { + throw new Error("Expected the external effect to be claimed."); + } + + await expect( + settleExternalToolEffect(database, { + claimToken: CLAIM_TOKEN, + commandId: COMMAND_ID, + driverInstanceId: DRIVER_INSTANCE_ID, + effectId: claim.effectId as ExternalToolEffectId, + settlement: { + kind: "succeeded", + result: { + requestId: "request-1", + serverId: "01J0000000000000000000000Y", + toolName: "createIssue", + }, + } as never, + }), + ).rejects.toThrow(); + await expect( + settleExternalToolEffect(database, { + claimToken: CLAIM_TOKEN, + commandId: COMMAND_ID, + driverInstanceId: DRIVER_INSTANCE_ID, + effectId: claim.effectId as ExternalToolEffectId, + settlement: { + kind: "succeeded", + result: { + debug: "must not enter durable state", + outputText: "done", + requestId: "request-1", + serverId: "01J0000000000000000000000Y", + toolName: "createIssue", + }, + } as never, + }), + ).rejects.toThrow(); + await expect( + database + .prepare( + `SELECT + command.error_json AS command_error_json, + command.result_json AS command_result_json, + command.status AS command_status, + effect.status, + effect.result_json, + attempt.status AS attempt_status, + attempt.completed_at, + attempt.result_json AS attempt_result_json + FROM external_tool_effect AS effect + JOIN external_tool_effect_attempt AS attempt ON attempt.effect_id = effect.id + JOIN driver_command AS command ON command.id = effect.command_id + WHERE effect.command_id = ?`, + ) + .bind(COMMAND_ID) + .first(), + ).resolves.toEqual({ + attempt_result_json: null, + attempt_status: "claimed", + command_error_json: null, + command_result_json: null, + command_status: "accepted", + completed_at: null, + result_json: null, + status: "claimed", + }); + }); + + test("classifies an intent as unexecuted and fences a claimed effect before repair", async () => { + const database = await createEffectStoreFixture(); + const command = mcpExecuteCommand(RUN_ID); + await createRuntimeCommandRecord(database, { + command, + driverInstanceId: DRIVER_INSTANCE_ID, + status: "accepted", + }); + + await expect( + listAcceptedMcpCommandRepairsForTerminalDriver(database, { + driverGeneration: DRIVER_GENERATION, + driverInstanceId: DRIVER_INSTANCE_ID, + }), + ).resolves.toMatchObject([ + { + commandId: COMMAND_ID, + terminal: { + error: { code: "driver.external_tool_effect_not_executed", retryable: true }, + status: "failed", + }, + }, + ]); + await claimExternalToolEffect(database, { + claimToken: CLAIM_TOKEN, + commandId: COMMAND_ID, + driverInstanceId: DRIVER_INSTANCE_ID, + }); + await expect( + listAcceptedMcpCommandRepairsForTerminalDriver(database, { + driverGeneration: DRIVER_GENERATION, + driverInstanceId: DRIVER_INSTANCE_ID, + }), + ).rejects.toThrow("must fence claimed MCP effects"); + + await markClaimedExternalToolEffectsUnknownForDriver(database, { + driverGeneration: DRIVER_GENERATION + 1, + driverInstanceId: DRIVER_INSTANCE_ID, + }); + await expect( + getExternalToolEffectForCommand(database, { + commandId: COMMAND_ID, + driverInstanceId: DRIVER_INSTANCE_ID, + }), + ).resolves.toMatchObject({ status: "claimed" }); + + await markClaimedExternalToolEffectsUnknownForDriver(database, { + driverGeneration: DRIVER_GENERATION, + driverInstanceId: DRIVER_INSTANCE_ID, + }); + const effect = await getExternalToolEffectForCommand(database, { + commandId: COMMAND_ID, + driverInstanceId: DRIVER_INSTANCE_ID, + }); + if (effect === null) { + throw new Error("Expected the exact-generation external effect."); + } + const message = `External effect ${effect.id} for MCP tool createIssue has an unknown outcome and will not be replayed.`; + await expect( + listAcceptedMcpCommandRepairsForTerminalDriver(database, { + driverGeneration: DRIVER_GENERATION, + driverInstanceId: DRIVER_INSTANCE_ID, + }), + ).resolves.toMatchObject([ + { + command, + commandId: COMMAND_ID, + terminal: { + error: { + code: "driver.external_tool_effect_unknown", + details: { + commandId: COMMAND_ID, + effectId: effect.id, + requestId: "request-1", + runId: RUN_ID, + serverId: "01J0000000000000000000000Y", + toolName: "createIssue", + }, + message, + retryable: false, + }, + status: "failed", + }, + }, + ]); + }); + + test("stores one maximum command with one maximum settlement", async () => { + const database = await createEffectStoreFixture(); + const command = mcpExecuteCommandAtSize(RUNTIME_COMMAND_MAX_UTF8_BYTES); + const settlement = succeededSettlementAtSize( + MCP_EXTERNAL_TOOL_EFFECT_SETTLEMENT_MAX_UTF8_BYTES, + ); + expect(measureRuntimeCommandJson(command)).toBe(RUNTIME_COMMAND_MAX_UTF8_BYTES); + expect(measureMcpExternalToolEffectSettlement(settlement)).toBe( + MCP_EXTERNAL_TOOL_EFFECT_SETTLEMENT_MAX_UTF8_BYTES, + ); + + await expect( + createRuntimeCommandRecord(database, { + command: mcpExecuteCommandAtSize(RUNTIME_COMMAND_MAX_UTF8_BYTES + 1, OVERSIZED_COMMAND_ID), + driverInstanceId: DRIVER_INSTANCE_ID, + status: "accepted", + }), + ).rejects.toThrow(`${RUNTIME_COMMAND_MAX_UTF8_BYTES} UTF-8 bytes`); + await expect( + database + .prepare( + "SELECT (SELECT count(*) FROM driver_command WHERE id = ?) AS command_count, (SELECT count(*) FROM external_tool_effect WHERE command_id = ?) AS effect_count", + ) + .bind(OVERSIZED_COMMAND_ID, OVERSIZED_COMMAND_ID) + .first(), + ).resolves.toEqual({ command_count: 0, effect_count: 0 }); + + await createRuntimeCommandRecord(database, { + command, + driverInstanceId: DRIVER_INSTANCE_ID, + status: "accepted", + }); + const claim = await claimExternalToolEffect(database, { + claimToken: CLAIM_TOKEN, + commandId: BOUNDARY_COMMAND_ID, + driverInstanceId: DRIVER_INSTANCE_ID, + }); + if (claim.kind !== "claimed") { + throw new Error("Expected the boundary external effect to be claimed."); + } + + await expect( + settleExternalToolEffect(database, { + claimToken: CLAIM_TOKEN, + commandId: BOUNDARY_COMMAND_ID, + driverInstanceId: DRIVER_INSTANCE_ID, + effectId: claim.effectId as ExternalToolEffectId, + settlement: { + ...settlement, + result: { ...settlement.result, outputText: `${settlement.result.outputText}x` }, + }, + }), + ).rejects.toThrow(`${MCP_EXTERNAL_TOOL_EFFECT_SETTLEMENT_MAX_UTF8_BYTES} UTF-8 bytes`); + await expect( + getExternalToolEffectForCommand(database, { + commandId: BOUNDARY_COMMAND_ID, + driverInstanceId: DRIVER_INSTANCE_ID, + }), + ).resolves.toMatchObject({ status: "claimed" }); + await expect( + getRuntimeCommandRecord(database, DRIVER_INSTANCE_ID, BOUNDARY_COMMAND_ID), + ).resolves.toMatchObject({ result: null, status: "accepted" }); + + await expect( + settleExternalToolEffect(database, { + claimToken: CLAIM_TOKEN, + commandId: BOUNDARY_COMMAND_ID, + driverInstanceId: DRIVER_INSTANCE_ID, + effectId: claim.effectId as ExternalToolEffectId, + settlement, + }), + ).resolves.toMatchObject({ kind: "succeeded", result: settlement.result }); + await expect( + listAcceptedMcpCommandRepairsForTerminalDriver(database, { + driverGeneration: DRIVER_GENERATION + 1, + driverInstanceId: DRIVER_INSTANCE_ID, + }), + ).resolves.toEqual([]); + await expect( + listAcceptedMcpCommandRepairsForTerminalDriver(database, { + driverGeneration: DRIVER_GENERATION, + driverInstanceId: DRIVER_INSTANCE_ID, + }), + ).resolves.toEqual([ + expect.objectContaining({ + command, + commandId: BOUNDARY_COMMAND_ID, + runtimeId: "openai-runtime", + sessionId: PUBLIC_API_TEST_IDS.ownerSession, + terminal: { result: settlement.result, status: "completed" }, + }), + ]); + await expect( + database + .prepare( + `SELECT + length(CAST(command.payload_json AS BLOB)) AS payload_bytes, + effect.result_json = attempt.result_json AS effect_matches_attempt, + command.status AS command_status, + effect.status AS effect_status, + attempt.status AS attempt_status + FROM driver_command AS command + JOIN external_tool_effect AS effect ON effect.command_id = command.id + JOIN external_tool_effect_attempt AS attempt ON attempt.effect_id = effect.id + WHERE command.id = ?`, + ) + .bind(BOUNDARY_COMMAND_ID) + .first(), + ).resolves.toEqual({ + attempt_status: "succeeded", + command_status: "accepted", + effect_matches_attempt: 1, + effect_status: "succeeded", + payload_bytes: RUNTIME_COMMAND_MAX_UTF8_BYTES, + }); + }); + + test("derives accepted input completion only from its exact terminal Session Run", async () => { + const database = await createEffectStoreFixture(); + const command = { + commandId: COMMAND_ID, + input: { text: "continue" }, + kind: "input.start" as const, + requestId: "request-1", + runId: RUN_ID, + }; + await createRuntimeCommandRecord(database, { + command, + driverInstanceId: DRIVER_INSTANCE_ID, + status: "accepted", + }); + await database + .prepare("UPDATE session_run SET completed_at = 2, status = 'completed' WHERE id = ?") + .bind(RUN_ID) + .run(); + + await expect( + listAcceptedInputStartCommandRepairsForTerminalDriver(database, { + driverGeneration: DRIVER_GENERATION + 1, + driverInstanceId: DRIVER_INSTANCE_ID, + }), + ).resolves.toEqual([]); + await expect( + listAcceptedInputStartCommandRepairsForTerminalDriver(database, { + driverGeneration: DRIVER_GENERATION, + driverInstanceId: DRIVER_INSTANCE_ID, + }), + ).resolves.toEqual([ + { + command, + commandId: COMMAND_ID, + runtimeId: "openai-runtime", + sessionId: PUBLIC_API_TEST_IDS.ownerSession, + terminal: { result: { requestId: "request-1" }, status: "completed" }, + }, + ]); + + await database + .prepare("UPDATE driver_instance SET status = 'failed' WHERE id = ?") + .bind(DRIVER_INSTANCE_ID) + .run(); + await expect( + repairAcceptedRuntimeCommandsForTerminalDriver(database, { + driverGeneration: DRIVER_GENERATION, + driverInstanceId: DRIVER_INSTANCE_ID, + }), + ).rejects.toThrow("event-first terminal reconciliation"); + await expect( + getRuntimeCommandRecord(database, DRIVER_INSTANCE_ID, COMMAND_ID), + ).resolves.toMatchObject({ result: null, status: "accepted" }); + }); + + test("requires an authoritative error before repairing an accepted failed input", async () => { + const database = await createEffectStoreFixture(); + const command = { + commandId: COMMAND_ID, + input: { text: "continue" }, + kind: "input.start" as const, + requestId: "request-1", + runId: RUN_ID, + }; + await createRuntimeCommandRecord(database, { + command, + driverInstanceId: DRIVER_INSTANCE_ID, + status: "accepted", + }); + await database + .prepare( + "UPDATE session_run SET completed_at = 2, error_code = 'driver.failed', error_details_json = ?, error_message = 'Driver failed', status = 'failed' WHERE id = ?", + ) + .bind(JSON.stringify({ driverInstanceId: DRIVER_INSTANCE_ID }), RUN_ID) + .run(); + + await expect( + listAcceptedInputStartCommandRepairsForTerminalDriver(database, { + driverGeneration: DRIVER_GENERATION, + driverInstanceId: DRIVER_INSTANCE_ID, + }), + ).resolves.toMatchObject([ + { + command, + terminal: { + error: { + code: "driver.failed", + details: { driverInstanceId: DRIVER_INSTANCE_ID }, + message: "Driver failed", + retryable: false, + }, + status: "failed", + }, + }, + ]); + + await database + .prepare( + "UPDATE session_run SET error_code = NULL, error_details_json = NULL, error_message = NULL WHERE id = ?", + ) + .bind(RUN_ID) + .run(); + await expect( + listAcceptedInputStartCommandRepairsForTerminalDriver(database, { + driverGeneration: DRIVER_GENERATION, + driverInstanceId: DRIVER_INSTANCE_ID, + }), + ).rejects.toThrow("missing its authoritative durable error"); + }); +}); diff --git a/apps/api/tests/file-upload-access.test.ts b/apps/api/tests/file-upload-access.test.ts index 3268ee5e..1088e844 100644 --- a/apps/api/tests/file-upload-access.test.ts +++ b/apps/api/tests/file-upload-access.test.ts @@ -74,6 +74,7 @@ function createFileUploadAccessDatabase(): SqliteD1Database { parent_path text NOT NULL, path text NOT NULL, purpose text NOT NULL, + runtime_event_seq integer, scope_id text NOT NULL, scope_kind text NOT NULL, session_kind text, diff --git a/apps/api/tests/file-upload-recovery.test.ts b/apps/api/tests/file-upload-recovery.test.ts index 4dd7a74c..ad3a8350 100644 --- a/apps/api/tests/file-upload-recovery.test.ts +++ b/apps/api/tests/file-upload-recovery.test.ts @@ -207,6 +207,7 @@ function createUploadRecoveryDatabase(): SqliteD1Database { parent_path text NOT NULL, path text NOT NULL, purpose text NOT NULL, + runtime_event_seq integer, size integer NOT NULL, updated_at integer NOT NULL, version integer NOT NULL diff --git a/apps/api/tests/helpers/api-test-fixture.ts b/apps/api/tests/helpers/api-test-fixture.ts index b07dec7e..7e259f6b 100644 --- a/apps/api/tests/helpers/api-test-fixture.ts +++ b/apps/api/tests/helpers/api-test-fixture.ts @@ -7,6 +7,7 @@ import type { AuthenticatedViewer } from "../../src/modules/auth/application/vie import { getViewer } from "../../src/modules/users/application/viewer-context.service"; import { storeVendorCredentialSecret } from "../../src/modules/vendor-credentials/application/vendor-credential.secret-resolution"; import type { ApiBindings } from "../../src/platform/cloudflare/worker-types"; +import { applyDrizzleMigrations } from "./drizzle-migrations"; import { createPublicHttpTestBindings } from "./public-api-http-test-fixture"; import { SqliteD1Database } from "./sqlite-d1"; @@ -187,9 +188,9 @@ export class ApiTestClient { } export async function createApiTestFixture(): Promise { - const database = new SqliteD1Database({ foreignKeys: false }); + const database = new SqliteD1Database(); - createApiTestSchema(database); + applyDrizzleMigrations(database); await seedApiTestFixture(database); const bindings = { @@ -255,463 +256,6 @@ export async function insertTestVendorCredential( .run(); } -function createApiTestSchema(database: SqliteD1Database): void { - database.execute(` - CREATE TABLE account ( - created_at integer NOT NULL, - email text NOT NULL, - email_verified integer NOT NULL, - id text PRIMARY KEY NOT NULL, - image_url text, - last_active_organization_id text, - name text NOT NULL, - system_agent_model text, - updated_at integer NOT NULL - ); - CREATE UNIQUE INDEX account_email_idx ON account (email); - - CREATE TABLE auth_account ( - access_token text, - access_token_expires_at integer, - provider_account_id text NOT NULL, - created_at integer NOT NULL, - id text PRIMARY KEY NOT NULL, - id_token text, - password text, - provider_id text NOT NULL, - refresh_token text, - refresh_token_expires_at integer, - scope text, - updated_at integer NOT NULL, - account_id text NOT NULL - ); - - CREATE TABLE auth_session ( - created_at integer NOT NULL, - expires_at integer NOT NULL, - id text PRIMARY KEY NOT NULL, - ip_address text, - token text NOT NULL, - updated_at integer NOT NULL, - user_agent text, - account_id text NOT NULL - ); - CREATE UNIQUE INDEX auth_session_token_idx ON auth_session (token); - - CREATE TABLE auth_verification ( - created_at integer NOT NULL, - expires_at integer NOT NULL, - id text PRIMARY KEY NOT NULL, - identifier text NOT NULL, - updated_at integer NOT NULL, - value text NOT NULL - ); - - CREATE TABLE organization ( - avatar_url text, - created_at integer NOT NULL, - creator_account_id text, - id text PRIMARY KEY NOT NULL, - name text NOT NULL, - updated_at integer NOT NULL - ); - - CREATE TABLE app ( - created_at integer NOT NULL, - default_environment_id text, - id text PRIMARY KEY NOT NULL, - name text NOT NULL, - organization_id text NOT NULL, - owner_account_id text NOT NULL, - updated_at integer NOT NULL - ); - - CREATE TABLE app_deployment ( - app_id text NOT NULL, - created_at integer NOT NULL, - default_branch text NOT NULL, - deleted_at integer, - id text PRIMARY KEY NOT NULL, - last_successful_url text, - latest_run_id text, - mosoo_subdomain text NOT NULL, - owner_account_id text NOT NULL, - repo_name text NOT NULL, - repo_owner text NOT NULL, - repo_url text NOT NULL, - source_kind text NOT NULL, - updated_at integer NOT NULL - ); - - CREATE TABLE app_deployment_run ( - app_id text NOT NULL, - created_at integer NOT NULL, - deployment_id text NOT NULL, - error_code text, - error_message text, - external_deployment_id text, - external_project_id text, - external_version_id text, - generated_wrangler_config_json text, - id text PRIMARY KEY NOT NULL, - mosoo_config_json text, - plan_json text, - source_branch text NOT NULL, - source_commit_sha text NOT NULL, - status text NOT NULL, - target_kind text, - target_project_name text, - target_script_name text, - updated_at integer NOT NULL, - url text - ); - - CREATE TABLE api_command ( - attempt_count integer DEFAULT 0 NOT NULL, - claim_expires_at integer, - claim_owner text, - completed_at integer, - created_at integer NOT NULL, - dedupe_key text NOT NULL, - id text PRIMARY KEY NOT NULL, - kind text NOT NULL, - last_error_code text, - last_error_message text, - payload_json text NOT NULL, - status text NOT NULL, - updated_at integer NOT NULL - ); - CREATE UNIQUE INDEX api_command_dedupe_idx ON api_command (dedupe_key); - - CREATE TABLE agent ( - config_json text NOT NULL, - created_at integer NOT NULL, - description text, - environment_id text, - id text PRIMARY KEY NOT NULL, - kind text DEFAULT 'pet' NOT NULL, - live_deployment_version_id text, - model text NOT NULL, - name text NOT NULL, - owner_account_id text NOT NULL, - app_id text NOT NULL, - prompt text NOT NULL, - provider text NOT NULL, - runtime_id text NOT NULL, - status text DEFAULT 'draft' NOT NULL, - updated_at integer NOT NULL, - visibility text DEFAULT 'private' NOT NULL - ); - - CREATE TABLE environment ( - created_at integer NOT NULL, - current_revision_id text NOT NULL, - description text NOT NULL, - forked_from_environment_id text, - forked_from_environment_name text, - forked_from_owner_name text, - id text PRIMARY KEY NOT NULL, - name text NOT NULL, - owner_account_id text, - app_id text NOT NULL, - updated_at integer NOT NULL - ); - - CREATE TABLE environment_revision ( - allow_mcp_servers integer NOT NULL, - allow_package_managers integer NOT NULL, - allowed_hosts_json text NOT NULL, - created_at integer NOT NULL, - created_by_account_id text, - env_vars_json text NOT NULL, - environment_id text NOT NULL, - id text PRIMARY KEY NOT NULL, - network_policy text NOT NULL, - packages_json text NOT NULL, - app_id text NOT NULL, - setup_script text NOT NULL - ); - - CREATE TABLE mcp_server ( - auth_type text NOT NULL, - byo_client_id text, - byo_client_secret_secret_id text, - created_at integer NOT NULL, - credential_scope text NOT NULL, - description text, - enabled integer DEFAULT true NOT NULL, - icon_url text, - id text PRIMARY KEY NOT NULL, - name text NOT NULL, - oauth_metadata_json text, - owner_account_id text NOT NULL, - app_id text NOT NULL, - source text NOT NULL, - updated_at integer NOT NULL, - url text NOT NULL - ); - - CREATE TABLE skill ( - author text NOT NULL, - created_at integer NOT NULL, - current_snapshot_id text NOT NULL, - description text NOT NULL, - forked_from_owner_name text, - forked_from_skill_id text, - forked_from_skill_name text, - id text PRIMARY KEY NOT NULL, - name text NOT NULL, - owner_account_id text NOT NULL, - app_id text NOT NULL, - source_kind text NOT NULL, - updated_at integer NOT NULL, - version text - ); - - CREATE TABLE file_record ( - committed integer NOT NULL, - created_at integer NOT NULL, - created_by_account_id text NOT NULL, - etag text, - expires_at integer, - id text PRIMARY KEY NOT NULL, - mime_type text, - name text NOT NULL, - object_key text NOT NULL, - owner_id text NOT NULL, - owner_kind text NOT NULL, - parent_path text NOT NULL, - path text NOT NULL, - purpose text NOT NULL, - scope_id text, - scope_kind text NOT NULL, - session_kind text, - size integer NOT NULL, - status text NOT NULL, - updated_at integer NOT NULL, - version integer NOT NULL - ); - - CREATE TABLE file_upload ( - content_type text NOT NULL, - created_at integer NOT NULL, - created_by_account_id text NOT NULL, - expected_size integer NOT NULL, - expires_at integer NOT NULL, - file_id text NOT NULL, - id text PRIMARY KEY NOT NULL, - if_match_etag text, - multipart_upload_id text, - overwrite integer NOT NULL, - part_size integer, - scope_id text, - scope_kind text NOT NULL, - status text NOT NULL, - strategy text NOT NULL, - updated_at integer NOT NULL - ); - - CREATE TABLE agent_deployment_version ( - agent_id text NOT NULL, - config_json text NOT NULL, - created_at integer NOT NULL, - created_by_account_id text NOT NULL, - environment_id text, - id text PRIMARY KEY NOT NULL, - kind text NOT NULL, - mcp_bindings_json text NOT NULL, - model text NOT NULL, - prompt text NOT NULL, - provider text NOT NULL, - runtime_id text NOT NULL, - skills_json text NOT NULL, - summary text NOT NULL, - version_number integer NOT NULL - ); - - CREATE TABLE agent_skill ( - agent_id text NOT NULL, - created_at integer NOT NULL, - skill_id text NOT NULL, - sort_order integer NOT NULL, - PRIMARY KEY (agent_id, skill_id) - ); - - CREATE TABLE session ( - agent_id text NOT NULL, - archived_at integer, - attributed_user_id text, - created_at integer NOT NULL, - creator_account_id text NOT NULL, - deployment_version_id text, - deployment_version_number integer, - id text PRIMARY KEY NOT NULL, - kind text NOT NULL, - last_message_at integer, - last_run_id text, - message_seq_cursor integer DEFAULT 0 NOT NULL, - metadata_json text DEFAULT '{}' NOT NULL, - model text NOT NULL, - app_id text NOT NULL, - provider text NOT NULL, - renamed integer NOT NULL, - runtime_id text NOT NULL, - status text NOT NULL, - status_operation_id text, - status_seq integer DEFAULT 0 NOT NULL, - runtime_event_seq_cursor integer DEFAULT 0 NOT NULL, - workspace_checkpoint_required integer DEFAULT 0 NOT NULL, - title text, - type text DEFAULT 'preview' NOT NULL, - updated_at integer NOT NULL - ); - - CREATE TABLE session_run ( - agent_id text NOT NULL, - completed_at integer, - created_at integer NOT NULL, - created_by_account_id text NOT NULL, - deployment_version_id text, - deployment_version_number integer, - driver_instance_id text, - error_code text, - error_details_json text, - error_message text, - id text PRIMARY KEY NOT NULL, - model text, - provider text, - runtime_id text, - session_id text NOT NULL, - started_at integer, - status text NOT NULL, - status_changed_at integer DEFAULT 0 NOT NULL, - status_event text DEFAULT 'run.queue' NOT NULL, - status_operation_id text, - status_seq integer DEFAULT 0 NOT NULL, - status_source text DEFAULT 'system' NOT NULL, - trace_id text NOT NULL, - trigger text NOT NULL, - updated_at integer NOT NULL - ); - - CREATE TABLE sandbox_session ( - cloudflare_session_id text NOT NULL, - created_at integer NOT NULL, - cwd text NOT NULL, - origin_json text NOT NULL, - sandbox_id text NOT NULL, - session_id text PRIMARY KEY NOT NULL, - status text NOT NULL, - updated_at integer NOT NULL - ); - - CREATE TABLE sandbox_backup ( - created_at integer NOT NULL, - dir text NOT NULL, - error_message text, - id text PRIMARY KEY NOT NULL, - keep integer DEFAULT 0 NOT NULL, - sandbox_id text NOT NULL, - session_run_id text, - status text NOT NULL, - ttl_seconds integer NOT NULL, - updated_at integer NOT NULL - ); - - CREATE TABLE driver_instance ( - boot_token_expires_at integer NOT NULL, - boot_token_hash blob NOT NULL, - boot_token_used_at integer, - close_code integer, - close_reason text, - connection_id text, - created_at integer NOT NULL, - command_seq_cursor integer DEFAULT 0 NOT NULL, - driver_pid integer, - driver_started_at integer, - driver_version text, - error_message text, - expires_at integer NOT NULL, - heartbeat_count integer NOT NULL, - generation integer DEFAULT 0 NOT NULL, - id text PRIMARY KEY NOT NULL, - last_heartbeat_at integer, - process_id text, - protocol text NOT NULL, - protocol_version integer NOT NULL, - restart_count integer DEFAULT 0 NOT NULL, - runtime text NOT NULL, - sandbox_id text NOT NULL, - sandbox_session_id text NOT NULL, - status text NOT NULL, - status_changed_at integer DEFAULT 0 NOT NULL, - status_event text DEFAULT 'driver.provision' NOT NULL, - status_operation_id text, - status_seq integer DEFAULT 0 NOT NULL, - status_source text DEFAULT 'system' NOT NULL, - updated_at integer NOT NULL - ); - - CREATE TABLE agent_mcp_binding ( - agent_credential_id text, - agent_id text NOT NULL, - created_at integer DEFAULT 1 NOT NULL, - credential_mode text DEFAULT 'runtime_resolved' NOT NULL, - enabled integer DEFAULT 1 NOT NULL, - id text PRIMARY KEY DEFAULT '01J00000000000000000000999' NOT NULL, - server_id text NOT NULL, - sort_order integer DEFAULT 0 NOT NULL, - updated_at integer DEFAULT 1 NOT NULL - ); - - CREATE TABLE mcp_credential ( - account_id text, - agent_id text, - auth_type text NOT NULL, - created_at integer NOT NULL, - expires_at integer, - id text PRIMARY KEY NOT NULL, - last_refreshed_at integer, - oauth_client_id text, - oauth_client_secret_secret_id text, - app_id text NOT NULL, - refresh_secret_id text, - scope text NOT NULL, - scope_values_json text, - secret_id text NOT NULL, - server_id text NOT NULL, - status text NOT NULL, - subject_label text, - updated_at integer NOT NULL - ); - - CREATE TABLE vendor_credential ( - api_base text, - api_key_secret_id text NOT NULL, - created_at integer NOT NULL, - id text PRIMARY KEY NOT NULL, - is_default integer DEFAULT false NOT NULL, - models text, - name text NOT NULL, - app_id text NOT NULL, - updated_at integer NOT NULL, - vendor_id text NOT NULL - ); - - CREATE TABLE vault_secret ( - algorithm text NOT NULL DEFAULT 'AES-GCM', - ciphertext text NOT NULL, - ciphertext_iv text NOT NULL, - created_at integer NOT NULL, - id text PRIMARY KEY NOT NULL, - kind text NOT NULL, - updated_at integer NOT NULL, - wrapped_dek text NOT NULL, - wrapped_dek_iv text NOT NULL - ); - `); -} - async function seedApiTestFixture(database: D1Database): Promise { await database .prepare( @@ -824,7 +368,7 @@ async function seedApiTestFixture(database: D1Database): Promise { "[]", API_TEST_IDS.environmentId, API_TEST_IDS.environmentRevisionId, - "sandbox", + "full", "[]", API_TEST_IDS.appId, "", diff --git a/apps/api/tests/helpers/drizzle-migrations.ts b/apps/api/tests/helpers/drizzle-migrations.ts new file mode 100644 index 00000000..f398fcf4 --- /dev/null +++ b/apps/api/tests/helpers/drizzle-migrations.ts @@ -0,0 +1 @@ +export * from "../../../../pkgs/db/scripts/drizzle-migrations"; diff --git a/apps/api/tests/helpers/public-api-http-core-schema.sql b/apps/api/tests/helpers/public-api-http-core-schema.sql deleted file mode 100644 index 5eebffcc..00000000 --- a/apps/api/tests/helpers/public-api-http-core-schema.sql +++ /dev/null @@ -1,241 +0,0 @@ -CREATE TABLE account ( - id text PRIMARY KEY NOT NULL, - email text NOT NULL, - email_verified integer NOT NULL, - image_url text, - last_active_organization_id text, - name text NOT NULL, - system_agent_model text, - created_at integer NOT NULL, - updated_at integer NOT NULL -); - -CREATE TABLE personal_access_token ( - id text PRIMARY KEY NOT NULL, - account_id text NOT NULL, - label text NOT NULL, - token_hash text NOT NULL, - created_at integer NOT NULL, - updated_at integer NOT NULL, - last_used_at integer, - revoked_at integer -); - -CREATE TABLE organization ( - id text PRIMARY KEY NOT NULL, - name text NOT NULL, - avatar_url text, - creator_account_id text, - created_at integer NOT NULL, - updated_at integer NOT NULL -); - -CREATE TABLE app ( - id text PRIMARY KEY NOT NULL, - organization_id text NOT NULL, - owner_account_id text NOT NULL, - name text NOT NULL, - default_environment_id text, - created_at integer NOT NULL, - updated_at integer NOT NULL -); - -CREATE TABLE agent ( - id text PRIMARY KEY NOT NULL, - app_id text NOT NULL, - owner_account_id text NOT NULL, - name text NOT NULL, - description text, - environment_id text, - live_deployment_version_id text, - kind text NOT NULL, - runtime_id text NOT NULL, - provider text NOT NULL, - model text NOT NULL, - prompt text NOT NULL, - created_at integer NOT NULL, - updated_at integer NOT NULL, - config_json text NOT NULL, - status text NOT NULL, - visibility text NOT NULL -); - -CREATE TABLE agent_deployment_version ( - id text PRIMARY KEY NOT NULL, - agent_id text NOT NULL, - version_number integer NOT NULL, - summary text NOT NULL, - kind text NOT NULL, - runtime_id text NOT NULL, - provider text NOT NULL, - model text NOT NULL, - prompt text NOT NULL, - config_json text NOT NULL, - environment_id text, - skills_json text NOT NULL, - mcp_bindings_json text NOT NULL, - created_by_account_id text NOT NULL, - created_at integer NOT NULL -); - -CREATE TABLE skill ( - author text NOT NULL, - created_at integer NOT NULL, - current_snapshot_id text NOT NULL, - description text NOT NULL, - forked_from_owner_name text, - forked_from_skill_id text, - forked_from_skill_name text, - id text PRIMARY KEY NOT NULL, - name text NOT NULL, - owner_account_id text NOT NULL, - app_id text NOT NULL, - source_kind text NOT NULL, - updated_at integer NOT NULL, - version text -); - -CREATE TABLE agent_skill ( - agent_id text NOT NULL, - skill_id text NOT NULL, - sort_order integer NOT NULL, - created_at integer NOT NULL, - PRIMARY KEY (agent_id, skill_id) -); - -CREATE TABLE mcp_server ( - id text PRIMARY KEY NOT NULL, - owner_account_id text NOT NULL, - name text NOT NULL, - description text, - source text DEFAULT 'app' NOT NULL, - auth_type text DEFAULT 'bearer' NOT NULL, - credential_scope text DEFAULT 'app' NOT NULL, - icon_url text, - byo_client_id text, - byo_client_secret_secret_id text, - oauth_metadata_json text, - app_id text NOT NULL, - url text NOT NULL, - enabled integer DEFAULT 1 NOT NULL, - created_at integer NOT NULL, - updated_at integer NOT NULL -); - -CREATE TABLE agent_mcp_binding ( - id text PRIMARY KEY NOT NULL, - agent_id text NOT NULL, - server_id text NOT NULL, - agent_credential_id text, - credential_mode text DEFAULT 'runtime_resolved' NOT NULL, - enabled integer DEFAULT 1 NOT NULL, - sort_order integer DEFAULT 0 NOT NULL, - created_at integer NOT NULL, - updated_at integer NOT NULL -); - -CREATE TABLE environment ( - id text PRIMARY KEY NOT NULL, - app_id text NOT NULL, - owner_account_id text, - current_revision_id text NOT NULL, - name text NOT NULL, - description text NOT NULL, - forked_from_environment_id text, - forked_from_environment_name text, - forked_from_owner_name text, - created_at integer NOT NULL, - updated_at integer NOT NULL -); - -CREATE TABLE environment_revision ( - id text PRIMARY KEY NOT NULL, - environment_id text NOT NULL, - app_id text NOT NULL, - created_by_account_id text, - setup_script text NOT NULL, - packages_json text NOT NULL, - env_vars_json text NOT NULL, - network_policy text NOT NULL, - allowed_hosts_json text NOT NULL, - allow_mcp_servers integer NOT NULL, - allow_package_managers integer NOT NULL, - created_at integer NOT NULL -); - -CREATE TABLE vendor_credential ( - id text PRIMARY KEY NOT NULL, - app_id text NOT NULL, - vendor_id text NOT NULL, - name text NOT NULL, - api_key_secret_id text NOT NULL, - api_base text, - is_default integer DEFAULT false NOT NULL, - models text, - created_at integer NOT NULL, - updated_at integer NOT NULL -); - -CREATE TABLE sandbox ( - id text PRIMARY KEY NOT NULL, - agent_id text, - app_id text, - kind text NOT NULL, - subject_kind text NOT NULL, - subject_id text NOT NULL, - status text NOT NULL, - status_event text DEFAULT 'runtime_subject.cold' NOT NULL, - status_source text DEFAULT 'system' NOT NULL, - status_seq integer DEFAULT 0 NOT NULL, - status_operation_id text, - status_changed_at integer DEFAULT 0 NOT NULL, - last_error text, - last_error_code text, - last_backup_id text, - last_restore_backup_id text, - bind_mount_ready integer DEFAULT 0 NOT NULL, - global_mounts_json text DEFAULT '[]' NOT NULL, - claim_owner text, - claim_expires_at integer, - inactive_deadline_at integer, - owner_account_id text, - created_at integer NOT NULL, - updated_at integer NOT NULL -); - -CREATE TABLE vault_secret ( - id text PRIMARY KEY NOT NULL, - kind text NOT NULL, - algorithm text DEFAULT 'AES-GCM' NOT NULL, - ciphertext text NOT NULL, - ciphertext_iv text NOT NULL, - wrapped_dek text NOT NULL, - wrapped_dek_iv text NOT NULL, - created_at integer NOT NULL, - updated_at integer NOT NULL -); - -CREATE TABLE api_command ( - id text PRIMARY KEY NOT NULL, - kind text NOT NULL, - dedupe_key text NOT NULL, - payload_json text NOT NULL, - status text NOT NULL, - attempt_count integer DEFAULT 0 NOT NULL, - claim_owner text, - claim_expires_at integer, - last_error_code text, - last_error_message text, - completed_at integer, - created_at integer NOT NULL, - updated_at integer NOT NULL -); - -CREATE UNIQUE INDEX api_command_dedupe_idx - ON api_command (dedupe_key); - -CREATE INDEX api_command_status_updated_idx - ON api_command (status, updated_at); - -CREATE INDEX api_command_claim_idx - ON api_command (status, claim_expires_at); diff --git a/apps/api/tests/helpers/public-api-http-runtime-schema.sql b/apps/api/tests/helpers/public-api-http-runtime-schema.sql deleted file mode 100644 index d457e45c..00000000 --- a/apps/api/tests/helpers/public-api-http-runtime-schema.sql +++ /dev/null @@ -1,352 +0,0 @@ -CREATE TABLE public_api_rate_limit_window ( - bucket_key text NOT NULL, - request_count integer DEFAULT 0 NOT NULL, - shard integer NOT NULL, - updated_at integer NOT NULL, - window_start integer NOT NULL, - PRIMARY KEY (bucket_key, window_start, shard) -); - -CREATE TABLE public_api_idempotency_key ( - id text PRIMARY KEY NOT NULL, - token_id text NOT NULL, - idempotency_key text NOT NULL, - method text NOT NULL, - route text NOT NULL, - body_hash text, - response_status integer, - response_json text, - created_at integer NOT NULL, - updated_at integer NOT NULL -); - -CREATE TABLE bound_agent_call_idempotency_key ( - id text PRIMARY KEY NOT NULL, - subject_hash text NOT NULL, - idempotency_key text NOT NULL, - body_hash text NOT NULL, - session_id text NOT NULL, - run_id text, - created_at integer NOT NULL, - updated_at integer NOT NULL -); - -CREATE UNIQUE INDEX bound_agent_call_idempotency_subject_key_idx - ON bound_agent_call_idempotency_key (subject_hash, idempotency_key); - -CREATE INDEX bound_agent_call_idempotency_updated_idx - ON bound_agent_call_idempotency_key (updated_at); - -CREATE TABLE session ( - id text PRIMARY KEY NOT NULL, - app_id text NOT NULL, - creator_account_id text NOT NULL, - attributed_user_id text, - end_user_id text, - agent_id text NOT NULL, - deployment_version_id text, - deployment_version_number integer, - kind text NOT NULL, - title text, - provider text NOT NULL, - model text NOT NULL, - runtime_id text NOT NULL, - status text NOT NULL, - type text DEFAULT 'ui' NOT NULL, - metadata_json text DEFAULT '{}' NOT NULL, - last_run_id text, - last_message_at integer, - message_seq_cursor integer DEFAULT 0 NOT NULL, - runtime_event_seq_cursor integer DEFAULT 0 NOT NULL, - workspace_checkpoint_required integer DEFAULT 0 NOT NULL, - archived_at integer, - renamed integer DEFAULT 0 NOT NULL, - status_operation_id text, - status_seq integer DEFAULT 0 NOT NULL, - created_at integer NOT NULL, - updated_at integer NOT NULL -); - -CREATE TABLE session_run ( - id text PRIMARY KEY NOT NULL, - session_id text NOT NULL, - agent_id text NOT NULL, - bound_capability_agent_id text, - bound_capability_app_id text, - bound_capability_binding_env text, - bound_capability_binding_name text, - bound_capability_deployment_id text, - bound_capability_deployment_run_id text, - created_by_account_id text NOT NULL, - deployment_version_id text, - deployment_version_number integer, - driver_instance_id text, - trigger text NOT NULL, - status text NOT NULL, - provider text, - model text, - runtime_id text, - trace_id text, - error_code text, - error_message text, - error_details_json text, - started_at integer, - completed_at integer, - created_at integer, - status_changed_at integer DEFAULT 0 NOT NULL, - status_event text DEFAULT 'run.queue' NOT NULL, - status_operation_id text, - status_seq integer DEFAULT 0 NOT NULL, - status_source text DEFAULT 'system' NOT NULL, - updated_at integer -); - -CREATE TABLE session_run_skill ( - session_run_id text NOT NULL, - skill_id text NOT NULL, - skill_name text NOT NULL, - snapshot_id text, - blob_sha256 text, - mount_path text NOT NULL, - resolution_mode text NOT NULL, - materialization_status text NOT NULL, - warning_code text, - created_at integer NOT NULL, - updated_at integer NOT NULL, - PRIMARY KEY (session_run_id, skill_id) -); - -CREATE TABLE session_execution_snapshot ( - session_id text PRIMARY KEY NOT NULL, - plan_json text NOT NULL, - created_at integer NOT NULL -); - -CREATE TABLE session_message ( - id text PRIMARY KEY NOT NULL, - session_id text NOT NULL, - session_run_id text, - seq integer NOT NULL, - role text NOT NULL, - content_text text NOT NULL, - segments_json text, - plan_json text, - created_by_account_id text NOT NULL, - created_at integer NOT NULL -); - -CREATE TABLE session_event ( - id text PRIMARY KEY NOT NULL, - session_id text NOT NULL, - run_id text, - agent_id text NOT NULL, - seq integer NOT NULL, - content_text text NOT NULL, - ended_at integer NOT NULL, - event_type text NOT NULL, - family text NOT NULL, - process_status text NOT NULL, - process_type text NOT NULL, - source text NOT NULL, - source_event_id text NOT NULL, - tool_call_id text, - tool_input_json text, - tool_name text, - tokens integer, - trace_id text, - visibility text NOT NULL, - occurred_at integer NOT NULL, - created_at integer NOT NULL -); - -CREATE TABLE session_agent_task_snapshot ( - driver_instance_id text NOT NULL, - run_id text NOT NULL, - seq integer NOT NULL, - session_id text PRIMARY KEY NOT NULL, - tasks_json text NOT NULL, - FOREIGN KEY (run_id) REFERENCES session_run (id) ON DELETE CASCADE, - FOREIGN KEY (session_id) REFERENCES session (id) ON DELETE CASCADE -); - -CREATE TABLE session_permission_request ( - created_at integer NOT NULL, - driver_instance_id text NOT NULL, - raw_input text, - request_id text NOT NULL, - run_id text NOT NULL, - session_id text NOT NULL, - title text NOT NULL, - tool_call_id text, - tool_kind text, - updated_at integer NOT NULL, - PRIMARY KEY (session_id, request_id) -); - -CREATE INDEX session_permission_request_run_idx - ON session_permission_request (session_id, run_id); - -CREATE TABLE session_readiness_snapshot ( - readiness_json text NOT NULL, - session_id text PRIMARY KEY NOT NULL, - updated_at integer NOT NULL -); - -CREATE UNIQUE INDEX session_message_session_seq_idx - ON session_message (session_id, seq); -CREATE UNIQUE INDEX session_event_session_seq_idx - ON session_event (session_id, seq); -CREATE UNIQUE INDEX session_event_session_source_idx - ON session_event (session_id, source_event_id); - -CREATE TABLE sandbox_session ( - cloudflare_session_id text NOT NULL, - created_at integer NOT NULL, - cwd text NOT NULL, - origin_json text NOT NULL, - sandbox_id text NOT NULL, - session_id text PRIMARY KEY NOT NULL, - status text NOT NULL, - updated_at integer NOT NULL -); - -CREATE INDEX sandbox_session_sandbox_status_idx - ON sandbox_session (sandbox_id, status, updated_at); -CREATE UNIQUE INDEX sandbox_session_cloudflare_session_idx - ON sandbox_session (cloudflare_session_id); - -CREATE TABLE sandbox_backup ( - created_at integer NOT NULL, - dir text NOT NULL, - error_message text, - id text PRIMARY KEY NOT NULL, - keep integer DEFAULT 0 NOT NULL, - sandbox_id text NOT NULL, - session_run_id text, - status text NOT NULL, - ttl_seconds integer NOT NULL, - updated_at integer NOT NULL -); - -CREATE INDEX sandbox_backup_sandbox_status_created_idx - ON sandbox_backup (sandbox_id, status, created_at); - -CREATE UNIQUE INDEX sandbox_backup_terminal_checkpoint_idx - ON sandbox_backup (sandbox_id, dir, session_run_id) - WHERE session_run_id IS NOT NULL AND status = 'ready'; - -CREATE TABLE driver_instance ( - id text PRIMARY KEY NOT NULL, - sandbox_id text NOT NULL, - sandbox_session_id text NOT NULL, - runtime text NOT NULL, - protocol text NOT NULL, - protocol_version integer NOT NULL, - status text NOT NULL, - status_changed_at integer DEFAULT 0 NOT NULL, - status_event text DEFAULT 'driver.provision' NOT NULL, - status_operation_id text, - status_seq integer DEFAULT 0 NOT NULL, - status_source text DEFAULT 'system' NOT NULL, - process_id text, - connection_id text, - command_seq_cursor integer DEFAULT 0 NOT NULL, - boot_token_hash blob NOT NULL, - boot_token_expires_at integer NOT NULL, - boot_token_used_at integer, - driver_pid integer, - driver_started_at integer, - driver_version text, - close_code integer, - close_reason text, - error_message text, - generation integer DEFAULT 0 NOT NULL, - heartbeat_count integer NOT NULL, - last_heartbeat_at integer, - restart_count integer DEFAULT 0 NOT NULL, - expires_at integer NOT NULL, - created_at integer NOT NULL, - updated_at integer NOT NULL -); - -CREATE TABLE external_tool_effect ( - attempt_count integer DEFAULT 0 NOT NULL, - command_id text NOT NULL, - created_at integer NOT NULL, - driver_instance_id text NOT NULL, - id text PRIMARY KEY NOT NULL, - idempotency_key text NOT NULL, - provider_receipt_json text, - result_json text, - server_id text NOT NULL, - session_run_id text NOT NULL, - status text NOT NULL, - tool_name text NOT NULL, - updated_at integer NOT NULL -); - -CREATE UNIQUE INDEX external_tool_effect_command_idx - ON external_tool_effect (command_id); -CREATE UNIQUE INDEX external_tool_effect_idempotency_key_idx - ON external_tool_effect (idempotency_key); -CREATE INDEX external_tool_effect_run_status_idx - ON external_tool_effect (session_run_id, status, id); -CREATE INDEX external_tool_effect_driver_status_idx - ON external_tool_effect (driver_instance_id, status); - -CREATE TABLE external_tool_effect_attempt ( - attempt integer NOT NULL, - completed_at integer, - created_at integer NOT NULL, - effect_id text NOT NULL, - provider_receipt_json text, - result_json text, - status text NOT NULL, - PRIMARY KEY (effect_id, attempt) -); - -CREATE INDEX external_tool_effect_attempt_status_idx - ON external_tool_effect_attempt (status, created_at); - -CREATE TABLE file_record ( - id text PRIMARY KEY NOT NULL, - scope_kind text NOT NULL, - scope_id text, - session_kind text, - status text NOT NULL, - name text NOT NULL, - path text NOT NULL, - parent_path text NOT NULL, - object_key text NOT NULL, - owner_id text NOT NULL, - owner_kind text NOT NULL, - purpose text NOT NULL, - expires_at integer, - mime_type text, - size integer NOT NULL, - etag text, - committed integer NOT NULL, - version integer NOT NULL, - created_by_account_id text NOT NULL, - created_at integer NOT NULL, - updated_at integer NOT NULL -); - -CREATE TABLE file_upload ( - id text PRIMARY KEY NOT NULL, - file_id text NOT NULL, - scope_kind text NOT NULL, - scope_id text NOT NULL, - strategy text NOT NULL, - status text NOT NULL, - content_type text NOT NULL, - expected_size integer NOT NULL, - overwrite integer NOT NULL, - if_match_etag text, - multipart_upload_id text, - part_size integer, - created_by_account_id text NOT NULL, - expires_at integer NOT NULL, - created_at integer NOT NULL, - updated_at integer NOT NULL -); diff --git a/apps/api/tests/helpers/public-api-http-test-fixture.ts b/apps/api/tests/helpers/public-api-http-test-fixture.ts index 18fc2bd2..7222a4b9 100644 --- a/apps/api/tests/helpers/public-api-http-test-fixture.ts +++ b/apps/api/tests/helpers/public-api-http-test-fixture.ts @@ -1,5 +1,3 @@ -import { readFileSync } from "node:fs"; - import type { SessionSummary } from "@mosoo/contracts/session"; import { accountsTable, @@ -9,18 +7,34 @@ import { environmentsTable, organizationsTable, personalAccessTokensTable, + sandboxSessionsTable, + sandboxesTable, appsTable, sessionExecutionSnapshotsTable, sessionsTable, vendorCredentialsTable, } from "@mosoo/db"; -import type { VendorCredentialId } from "@mosoo/id"; +import { parsePlatformId } from "@mosoo/id"; +import type { + AccountId, + AgentId, + AppId, + SandboxId, + SandboxSessionId, + SessionId, + VendorCredentialId, +} from "@mosoo/id"; import { hashTokenValue } from "../../src/modules/auth/application/personal-access-token.service"; import { storeVendorCredentialSecret } from "../../src/modules/vendor-credentials/application/vendor-credential.secret-resolution"; import type { ApiBindings } from "../../src/platform/cloudflare/worker-types"; import type { ApiCommandQueueStub } from "./api-command-queue-fixture"; import { createApiCommandQueueStub } from "./api-command-queue-fixture"; +import { + applyDrizzleMigrations, + applyDrizzleMigrationsFrom, + applyDrizzleMigrationsThrough, +} from "./drizzle-migrations"; import { SqliteD1Database } from "./sqlite-d1"; export { SqliteD1Database } from "./sqlite-d1"; export { @@ -32,13 +46,6 @@ export { type RecordedQueueMessageAction, } from "./api-command-queue-fixture"; -const CONTRACT_SCHEMA_SQL = readFileSync( - new URL("./public-api-http-core-schema.sql", import.meta.url), - "utf8", -) - .concat("\n") - .concat(readFileSync(new URL("./public-api-http-runtime-schema.sql", import.meta.url), "utf8")); - const INITIAL_AGENT_CONFIG_JSON = JSON.stringify({ packageMcpServers: [], packageResolution: null, @@ -75,7 +82,7 @@ export const PUBLIC_API_TEST_IDS = { driverOwner: "01J0000000000000000000000F", } as const; -const PUBLIC_API_VENDOR_CREDENTIAL_ID = "vendor-openai-app" as VendorCredentialId; +const PUBLIC_API_VENDOR_CREDENTIAL_ID = "01J0000000000000000000000S" as VendorCredentialId; export function createTestExecutionContext(): ExecutionContext { return { @@ -100,8 +107,9 @@ export function nowMsForTest(): number { } interface StoredObject { - body: string; + body: Uint8Array; contentType: string; + customMetadata: Record; etag: string; key: string; } @@ -129,6 +137,7 @@ export class PublicApiMemoryFileBucket { body: ReadableStream | ArrayBuffer | ArrayBufferView | string | Blob | null, options?: R2PutOptions, ): Promise { + const bytes = await this.#readBody(body); const existing = this.objects.get(key); const ifNoneMatch = this.#readOnlyIfHeader(options?.onlyIf, "If-None-Match"); const ifMatch = this.#readOnlyIfHeader(options?.onlyIf, "If-Match"); @@ -142,8 +151,9 @@ export class PublicApiMemoryFileBucket { } const stored: StoredObject = { - body: await this.#readBody(body), + body: bytes, contentType: options?.httpMetadata?.contentType ?? "application/octet-stream", + customMetadata: { ...options?.customMetadata }, etag: this.#createEtag(), key, }; @@ -160,27 +170,25 @@ export class PublicApiMemoryFileBucket { async #readBody( body: ReadableStream | ArrayBuffer | ArrayBufferView | string | Blob | null, - ): Promise { + ): Promise { if (body === null) { - return ""; + return new Uint8Array(); } if (typeof body === "string") { - return body; + return new TextEncoder().encode(body); } if (body instanceof Blob) { - return body.text(); + return new Uint8Array(await body.arrayBuffer()); } if (body instanceof ArrayBuffer) { - return new TextDecoder().decode(body); + return new Uint8Array(body).slice(); } if (ArrayBuffer.isView(body)) { - return new TextDecoder().decode( - new Uint8Array(body.buffer, body.byteOffset, body.byteLength), - ); + return new Uint8Array(body.buffer, body.byteOffset, body.byteLength).slice(); } const chunks: Uint8Array[] = []; @@ -205,7 +213,7 @@ export class PublicApiMemoryFileBucket { offset += chunk.byteLength; } - return new TextDecoder().decode(bytes); + return bytes; } #readOnlyIfHeader(onlyIf: R2PutOptions["onlyIf"] | undefined, name: string): string | null { @@ -214,14 +222,14 @@ export class PublicApiMemoryFileBucket { #toObject(stored: StoredObject): R2Object { return { - customMetadata: {}, + customMetadata: { ...stored.customMetadata }, etag: stored.etag, httpEtag: `"${stored.etag}"`, httpMetadata: { contentType: stored.contentType, }, key: stored.key, - size: new TextEncoder().encode(stored.body).byteLength, + size: stored.body.byteLength, uploaded: new Date(0), version: "", writeHttpMetadata(headers: Headers) { @@ -231,7 +239,7 @@ export class PublicApiMemoryFileBucket { } #toObjectBody(stored: StoredObject): R2ObjectBody { - const bytes = new TextEncoder().encode(stored.body); + const bytes = stored.body.slice(); return { ...this.#toObject(stored), @@ -249,10 +257,10 @@ export class PublicApiMemoryFileBucket { }), bodyUsed: false, async json() { - return JSON.parse(stored.body) as T; + return JSON.parse(new TextDecoder().decode(bytes)) as T; }, async text() { - return stored.body; + return new TextDecoder().decode(bytes); }, } as R2ObjectBody; } @@ -289,11 +297,14 @@ export function createPublicHttpTestBindings( }; } -export async function createPublicHttpContractDatabase(): Promise { - const database = new SqliteD1Database(); - const nowMs = nowMsForTest(); +export function migratePre0014PublicHttpContractDatabase(database: SqliteD1Database): void { + applyDrizzleMigrationsFrom(database, "0014_session-event-stream-identity"); +} - database.execute(CONTRACT_SCHEMA_SQL); +async function seedPublicHttpContractDatabase( + database: SqliteD1Database, +): Promise { + const nowMs = nowMsForTest(); const db = database.app(); await db @@ -490,6 +501,94 @@ export async function createPublicHttpContractDatabase(): Promise { + const database = new SqliteD1Database(); + applyDrizzleMigrations(database); + await seedPublicHttpContractDatabase(database); + return database.serialize(); +})(); + +export async function createPublicHttpContractDatabase(): Promise { + return new SqliteD1Database({ serialized: await publicHttpContractDatabaseTemplate }); +} + +export async function createPre0014PublicHttpContractDatabase(): Promise { + const database = new SqliteD1Database(); + applyDrizzleMigrationsThrough(database, "0013_durable-mcp-effect-v3"); + return seedPublicHttpContractDatabase(database); +} + +export async function insertActiveSandboxSessionFixture( + database: SqliteD1Database, + input: { + agentId?: string; + appId?: string; + cwd?: string; + inactiveDeadlineAt?: number | null; + kind?: "cattle" | "pet"; + ownerAccountId: string; + sandboxId: string; + sandboxSessionId?: string; + sessionId: string; + timestampMs?: number; + }, +): Promise { + const agentId = parsePlatformId( + input.agentId ?? PUBLIC_API_TEST_IDS.agent, + "fixture agent id", + ); + const appId = parsePlatformId(input.appId ?? PUBLIC_API_TEST_IDS.app, "fixture app id"); + const ownerAccountId = parsePlatformId(input.ownerAccountId, "fixture account id"); + const sandboxId = parsePlatformId(input.sandboxId, "fixture sandbox id"); + const sandboxSessionId = parsePlatformId( + input.sandboxSessionId ?? input.sandboxId, + "fixture sandbox session id", + ); + const sessionId = parsePlatformId(input.sessionId, "fixture session id"); + const kind = input.kind ?? "pet"; + const timestampMs = input.timestampMs ?? nowMsForTest(); + + await database + .app() + .insert(sandboxesTable) + .values({ + agentId, + appId, + createdAt: timestampMs, + id: sandboxId, + inactiveDeadlineAt: input.inactiveDeadlineAt ?? null, + incarnation: 1, + kind, + networkConstraintsHash: "0".repeat(64), + ownerAccountId, + status: "active", + subjectId: kind === "pet" ? agentId : sessionId, + subjectKind: kind === "pet" ? "agent" : "session", + updatedAt: timestampMs, + }) + .run(); + await database + .app() + .insert(sandboxSessionsTable) + .values({ + createdAt: timestampMs, + cwd: input.cwd ?? `/workspace/se/${sessionId}`, + originJson: JSON.stringify({ + callerUserId: ownerAccountId, + entrypoint: "api", + executionOwnerUserId: ownerAccountId, + type: "agent", + }), + sandboxId, + sandboxIncarnation: 1, + sandboxSessionId, + sessionId, + status: "active", + updatedAt: timestampMs, + }) + .run(); +} + export async function insertNonOwnerSession(database: SqliteD1Database): Promise { await insertSession(database, { creatorAccountId: PUBLIC_API_TEST_IDS.nonOwnerAccount, @@ -622,6 +721,7 @@ function createOkDurableObjectNamespace() { destroy: async () => {}, fetch: async () => new Response(null, { status: 204 }), publishEvents: async () => {}, + syncViewers: async () => {}, }), idFromName: (name: string) => name, }; diff --git a/apps/api/tests/helpers/runtime-output-sandbox.ts b/apps/api/tests/helpers/runtime-output-sandbox.ts new file mode 100644 index 00000000..617759ef --- /dev/null +++ b/apps/api/tests/helpers/runtime-output-sandbox.ts @@ -0,0 +1,104 @@ +import type { SandboxHandle } from "../../src/modules/runtime/infrastructure/sandbox-handles"; + +const encoder = new TextEncoder(); + +export interface RuntimeOutputSandboxOptions { + readonly files?: ReadonlyMap; + readonly fileSizes?: ReadonlyMap; + readonly listError?: Error; + readonly onExec?: (command: string) => void; + readonly onRead?: (path: string) => void; + readonly readError?: Error; + readonly root?: string; +} + +export function createRuntimeOutputSandbox( + options: RuntimeOutputSandboxOptions = {}, +): SandboxHandle { + const root = (options.root ?? "/workspace/outputs").replace(/\/+$/, ""); + const entries = () => + [...(options.files ?? [])] + .map(([path, content]) => { + const absolutePath = path.startsWith("/") ? path : `${root}/${path}`; + return { + absolutePath, + bytes: typeof content === "string" ? encoder.encode(content) : content, + path, + relativePath: absolutePath.slice(root.length + 1), + }; + }) + .filter(({ absolutePath }) => absolutePath.startsWith(`${root}/`)); + const sizeOf = (entry: ReturnType[number]) => + options.fileSizes?.get(entry.path) ?? + options.fileSizes?.get(entry.absolutePath) ?? + entry.bytes.byteLength; + const findCommandEntry = (command: string) => + entries().find(({ absolutePath }) => command.includes(absolutePath)); + const failed = (stderr: string, exitCode = 1) => ({ + exitCode, + stderr, + stdout: "", + success: false, + }); + const succeeded = (stdout = "") => ({ exitCode: 0, stderr: "", stdout, success: true }); + const unavailable = (): Promise => + Promise.reject(new Error("Unexpected sandbox test method call.")); + const exec: SandboxHandle["exec"] = async (command) => { + options.onExec?.(command); + if (command.includes("find . -type f")) { + if (options.listError !== undefined) { + return failed(options.listError.message); + } + return succeeded( + entries() + .toSorted((left, right) => left.relativePath.localeCompare(right.relativePath)) + .map((entry) => `./${entry.relativePath}\0${sizeOf(entry)}\0`) + .join(""), + ); + } + if (command.includes("stat --printf=")) { + const entry = findCommandEntry(command); + return entry === undefined + ? failed("missing test file", 44) + : succeeded(String(sizeOf(entry))); + } + if (command.includes("head -c")) { + const entry = findCommandEntry(command); + if (entry === undefined) { + return failed("missing test file"); + } + options.onRead?.(entry.absolutePath); + if (options.readError !== undefined) { + return failed(options.readError.message); + } + return succeeded( + entry.bytes.subarray(0, Number(command.match(/head -c (\d+)/)?.[1])).toBase64(), + ); + } + return succeeded(); + }; + const session = { + exec, + mkdir: unavailable, + readFile: unavailable, + startProcess: unavailable, + watch: unavailable, + writeFile: unavailable, + }; + + return { + ...session, + configureNetworkConstraints: unavailable, + createBackup: unavailable, + createSession: unavailable, + deleteSession: unavailable, + destroy: unavailable, + getSession: async () => session, + mountBucket: unavailable, + restoreBackup: unavailable, + setKeepAlive: unavailable, + terminal: unavailable, + unmountBucket: unavailable, + wsConnect: unavailable, + }; +} diff --git a/apps/api/tests/helpers/sqlite-d1.ts b/apps/api/tests/helpers/sqlite-d1.ts index be3340bb..632595d7 100644 --- a/apps/api/tests/helpers/sqlite-d1.ts +++ b/apps/api/tests/helpers/sqlite-d1.ts @@ -8,11 +8,17 @@ type RawOptions = { columnNames?: boolean } | undefined; const statementQueries = new WeakMap(); export class SqliteD1Database implements D1Database { - readonly #database = new Database(":memory:"); + readonly #database: Database; readonly #maxBoundParams: number | undefined; #batchTail: Promise = Promise.resolve(); - constructor(input: { foreignKeys?: boolean; maxBoundParams?: number } = {}) { + constructor( + input: { foreignKeys?: boolean; maxBoundParams?: number; serialized?: Uint8Array } = {}, + ) { + this.#database = + input.serialized === undefined + ? new Database(":memory:") + : Database.deserialize(input.serialized); this.#maxBoundParams = input.maxBoundParams; this.#database.run(`PRAGMA foreign_keys = ${input.foreignKeys === false ? "OFF" : "ON"}`); } @@ -25,6 +31,10 @@ export class SqliteD1Database implements D1Database { return createSqliteD1Statement(this.#database, query, [], this.#maxBoundParams); } + serialize(): Uint8Array { + return this.#database.serialize(); + } + app(): AppDatabase { return getAppDatabase(this); } diff --git a/apps/api/tests/lease-ownership-renewal.test.ts b/apps/api/tests/lease-ownership-renewal.test.ts new file mode 100644 index 00000000..58b1fed3 --- /dev/null +++ b/apps/api/tests/lease-ownership-renewal.test.ts @@ -0,0 +1,44 @@ +import { describe, expect, test } from "bun:test"; + +import { createLeaseOwnershipRenewal } from "../src/modules/runtime/infrastructure/runtime-subject-lifecycle/lease-ownership-renewal"; + +describe("lease ownership renewal", () => { + test("serializes concurrent renewals and latches ownership loss", async () => { + const result = Promise.withResolvers(); + let renewalCalls = 0; + let destructiveCalls = 0; + const requireOwnership = createLeaseOwnershipRenewal(() => { + renewalCalls += 1; + return result.promise; + }, "lost ownership"); + + const heartbeat = requireOwnership(); + const destructive = requireOwnership().then(() => { + destructiveCalls += 1; + }); + expect(renewalCalls).toBe(1); + + const settled = Promise.allSettled([heartbeat, destructive]); + result.resolve(false); + expect(await settled).toEqual([ + { reason: expect.objectContaining({ message: "lost ownership" }), status: "rejected" }, + { reason: expect.objectContaining({ message: "lost ownership" }), status: "rejected" }, + ]); + await expect(requireOwnership()).rejects.toThrow("lost ownership"); + expect(renewalCalls).toBe(1); + expect(destructiveCalls).toBe(0); + }); + + test("latches an uncertain renewal failure", async () => { + const cause = new Error("renewal failed"); + let renewalCalls = 0; + const requireOwnership = createLeaseOwnershipRenewal(async () => { + renewalCalls += 1; + throw cause; + }, "lost ownership"); + + await expect(requireOwnership()).rejects.toMatchObject({ cause, message: "lost ownership" }); + await expect(requireOwnership()).rejects.toMatchObject({ cause, message: "lost ownership" }); + expect(renewalCalls).toBe(1); + }); +}); diff --git a/apps/api/tests/native-resume-ref.test.ts b/apps/api/tests/native-resume-ref.test.ts index d8917675..11a8f41d 100644 --- a/apps/api/tests/native-resume-ref.test.ts +++ b/apps/api/tests/native-resume-ref.test.ts @@ -1,13 +1,25 @@ import { describe, expect, test } from "bun:test"; +import { parsePlatformId } from "@mosoo/id"; +import type { DriverInstanceId, SessionId, SessionRunId } from "@mosoo/id"; + import { deleteNativeResumeRefsForSessions, getNativeResumeRefForRuntime, + upsertNativeResumeRef, } from "../src/modules/runtime/infrastructure/native-resume-ref.repository"; import { SqliteD1Database } from "./helpers/sqlite-d1"; const SESSION_ID_1 = "01J000000000000000000000G1"; const SESSION_ID_2 = "01J000000000000000000000G2"; +const DRIVER_INSTANCE_ID = parsePlatformId( + "01J000000000000000000000G4", + "driver instance ID", +); +const SESSION_RUN_ID = parsePlatformId( + "01J000000000000000000000G3", + "session run ID", +); describe("native resume refs", () => { test("uses only the cursor committed with a Cattle checkpoint", async () => { @@ -23,6 +35,7 @@ describe("native resume refs", () => { created_at integer NOT NULL, kind text NOT NULL, observed_driver_instance_id text, + observed_event_seq integer DEFAULT 0 NOT NULL, observed_session_run_id text, runtime_id text NOT NULL, session_id text PRIMARY KEY NOT NULL, @@ -72,6 +85,7 @@ describe("native resume refs", () => { created_at integer NOT NULL, kind text NOT NULL, observed_driver_instance_id text, + observed_event_seq integer DEFAULT 0 NOT NULL, observed_session_run_id text, runtime_id text NOT NULL, session_id text PRIMARY KEY NOT NULL, @@ -92,4 +106,62 @@ describe("native resume refs", () => { .all<{ session_id: string; value: string }>(); expect(rows.results).toEqual([{ session_id: SESSION_ID_2, value: "thread-2" }]); }); + + test("only a higher durable event seq can replace an observed ref", async () => { + const database = new SqliteD1Database({ foreignKeys: false }); + database.execute(` + CREATE TABLE native_resume_ref ( + committed_session_run_id text, + committed_value text, + created_at integer NOT NULL, + kind text NOT NULL, + observed_driver_instance_id text, + observed_event_seq integer DEFAULT 0 NOT NULL, + observed_session_run_id text, + runtime_id text NOT NULL, + session_id text PRIMARY KEY NOT NULL, + updated_at integer NOT NULL, + value text NOT NULL + ); + `); + + const observation = { + driverInstanceId: DRIVER_INSTANCE_ID, + nativeResumeRef: { + kind: "openai_thread_id" as const, + runtimeId: "openai-runtime" as const, + value: "thread-5", + }, + observedEventSeq: 5, + sessionId: SESSION_ID_1 as SessionId, + sessionRunId: SESSION_RUN_ID, + }; + + await upsertNativeResumeRef(database, observation); + await upsertNativeResumeRef(database, { + ...observation, + nativeResumeRef: { ...observation.nativeResumeRef, value: "stale-thread" }, + observedEventSeq: 4, + }); + await upsertNativeResumeRef(database, observation); + await expect( + upsertNativeResumeRef(database, { + ...observation, + nativeResumeRef: { ...observation.nativeResumeRef, value: "conflicting-thread" }, + }), + ).rejects.toThrow("replayed with conflicting content"); + + await upsertNativeResumeRef(database, { + ...observation, + nativeResumeRef: { ...observation.nativeResumeRef, value: "thread-6" }, + observedEventSeq: 6, + }); + + expect( + await database + .prepare("SELECT observed_event_seq, value FROM native_resume_ref WHERE session_id = ?") + .bind(SESSION_ID_1) + .first<{ observed_event_seq: number; value: string }>(), + ).toEqual({ observed_event_seq: 6, value: "thread-6" }); + }); }); diff --git a/apps/api/tests/owner-debug-terminal.test.ts b/apps/api/tests/owner-debug-terminal.test.ts index f3f733c0..9859f076 100644 --- a/apps/api/tests/owner-debug-terminal.test.ts +++ b/apps/api/tests/owner-debug-terminal.test.ts @@ -37,24 +37,33 @@ interface TerminalSpy { createSessionCalls: { cwd?: string; id?: string }[]; handle: SandboxHandle; mkdirCalls: string[]; + sessionDisposeCalls: number; setKeepAliveCalls: boolean[]; + subjectDisposeCalls: number; } function createTerminalSandboxHandleSpy(): TerminalSpy { const mkdirCalls: string[] = []; const setKeepAliveCalls: boolean[] = []; const createSessionCalls: { cwd?: string; id?: string }[] = []; + let sessionDisposeCalls = 0; + let subjectDisposeCalls = 0; const unavailable = async () => { throw new Error("Unexpected sandbox test method call."); }; const sessionResponse = new Response("ok", { status: 200 }); const handle = { + activateRuntimeSubjectIncarnation: async () => {}, configureNetworkConstraints: async () => {}, createBackup: unavailable, + createRuntimeSubjectBackup: unavailable, createSession: async (options) => { createSessionCalls.push({ cwd: options?.cwd, id: options?.id }); return { + [Symbol.dispose]: () => { + sessionDisposeCalls++; + }, exec: unavailable, mkdir: unavailable, readFile: unavailable, @@ -66,12 +75,15 @@ function createTerminalSandboxHandleSpy(): TerminalSpy { }, deleteSession: unavailable, destroy: unavailable, + destroyRuntimeSubjectIncarnation: unavailable, exec: unavailable, getSession: unavailable, + inspectRuntimeSubjectIncarnation: unavailable, mkdir: async (path) => { mkdirCalls.push(path); }, mountBucket: unavailable, + markRuntimeSubjectIncarnationReady: async () => {}, readFile: unavailable, restoreBackup: unavailable, setKeepAlive: async (value) => { @@ -83,9 +95,23 @@ function createTerminalSandboxHandleSpy(): TerminalSpy { watch: unavailable, writeFile: unavailable, wsConnect: unavailable, + [Symbol.dispose]: () => { + subjectDisposeCalls++; + }, } as unknown as SandboxHandle; - return { createSessionCalls, handle, mkdirCalls, setKeepAliveCalls }; + return { + createSessionCalls, + handle, + mkdirCalls, + get sessionDisposeCalls() { + return sessionDisposeCalls; + }, + setKeepAliveCalls, + get subjectDisposeCalls() { + return subjectDisposeCalls; + }, + }; } describe("owner debug terminal", () => { @@ -137,5 +163,7 @@ describe("owner debug terminal", () => { ); expect(spy.createSessionCalls).toHaveLength(1); expect(spy.createSessionCalls[0]?.cwd).toBe("/workspace"); + expect(spy.sessionDisposeCalls).toBe(1); + expect(spy.subjectDisposeCalls).toBe(1); }); }); diff --git a/apps/api/tests/pet-stranded-recycle.test.ts b/apps/api/tests/pet-stranded-recycle.test.ts index 93e01ebc..22a372f5 100644 --- a/apps/api/tests/pet-stranded-recycle.test.ts +++ b/apps/api/tests/pet-stranded-recycle.test.ts @@ -8,7 +8,9 @@ import { repairStrandedRuntimeSubjectDeadlines } from "../src/modules/runtime/in import { listInactiveRuntimeSubjects } from "../src/modules/runtime/infrastructure/runtime-subject-lifecycle/runtime-subject-store"; import type { ApiBindings } from "../src/platform/cloudflare/worker-types"; import { + PUBLIC_API_TEST_IDS, createPublicHttpContractDatabase, + insertActiveSandboxSessionFixture, insertNonOwnerSession, } from "./helpers/public-api-http-test-fixture"; import type { SqliteD1Database } from "./helpers/sqlite-d1"; @@ -26,36 +28,21 @@ function createBindings(database: D1Database): ApiBindings { async function insertSandbox( database: SqliteD1Database, - input: { - readonly id?: string; - readonly kind?: "cattle" | "pet"; - readonly subjectId?: string; - readonly subjectKind?: "agent" | "session"; - } = {}, + kind: "cattle" | "pet" = "pet", ): Promise { - await database - .prepare( - ` - INSERT INTO sandbox (id, kind, subject_kind, subject_id, status, inactive_deadline_at, created_at, updated_at) - VALUES (?, ?, ?, ?, 'active', NULL, 1, 1) - `, - ) - .bind( - input.id ?? SANDBOX_ID, - input.kind ?? "pet", - input.subjectKind ?? "agent", - input.subjectId ?? AGENT_ID, - ) - .run(); + await insertActiveSandboxSessionFixture(database, { + kind, + ownerAccountId: PUBLIC_API_TEST_IDS.nonOwnerAccount, + sandboxId: SANDBOX_ID, + sessionId: SESSION_ID, + timestampMs: 1, + }); } async function insertDriver( database: SqliteD1Database, input: { - readonly id?: string; readonly lastHeartbeatAt: number; - readonly sandboxId?: string; - readonly status?: string; }, ): Promise { await database @@ -64,6 +51,7 @@ async function insertDriver( INSERT INTO driver_instance ( id, sandbox_id, + sandbox_incarnation, sandbox_session_id, runtime, protocol, @@ -79,17 +67,18 @@ async function insertDriver( created_at, updated_at ) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) `, ) .bind( - input.id ?? DRIVER_ID, - input.sandboxId ?? SANDBOX_ID, + DRIVER_ID, + SANDBOX_ID, + 1, SESSION_ID, "cloudflare-container", "driver-ws", 1, - input.status ?? "ready", + "ready", new Uint8Array([1]), Date.now() + 10_000, Date.now(), @@ -107,8 +96,6 @@ async function insertRun( database: SqliteD1Database, input: { readonly driverInstanceId?: string | null; - readonly id?: string; - readonly sessionId?: string; readonly status?: string; } = {}, ): Promise { @@ -134,8 +121,8 @@ async function insertRun( `, ) .bind( - input.id ?? RUN_ID, - input.sessionId ?? SESSION_ID, + RUN_ID, + SESSION_ID, AGENT_ID, "01J00000000000000000000002", "user_prompt", @@ -237,43 +224,46 @@ describe("pet stranded recycle", () => { ).resolves.toEqual([{ id: SANDBOX_ID, kind: "pet" }]); }); - test("leaves live, busy, and resident subjects alone", async () => { + test("leaves a pet with a live Driver alone", async () => { const database = await createPublicHttpContractDatabase(); await insertNonOwnerSession(database); const nowMs = Date.now(); - // A pet with a live driver is not reclaimable. - await insertSandbox(database, { id: "01J0000000000000000000000V" }); + await insertSandbox(database); await insertDriver(database, { - id: "01J0000000000000000000000W", lastHeartbeatAt: nowMs, - sandboxId: "01J0000000000000000000000V", }); - // A pet whose subject still has an active run is not reclaimable, even - // when no driver row links the run to the sandbox yet. - await insertSandbox(database, { - id: "01J0000000000000000000000X", - subjectId: SESSION_ID, - subjectKind: "session", + + await expect(repairStrandedRuntimeSubjectDeadlines(database, { now: nowMs })).resolves.toEqual({ + cattle: 0, + pet: 0, }); + await expect(readSandboxDeadline(database)).resolves.toBeNull(); + }); + + test("leaves a pet with an active subject Run alone", async () => { + const database = await createPublicHttpContractDatabase(); + await insertNonOwnerSession(database); + const nowMs = Date.now(); + await insertSandbox(database); await insertRun(database, { driverInstanceId: null, status: "queued" }); - // A cattle subject with an active conversation stays resident. - await insertSandbox(database, { id: "01J0000000000000000000000Y", kind: "cattle" }); - await database - .prepare( - ` - INSERT INTO sandbox_session (cloudflare_session_id, created_at, cwd, origin_json, sandbox_id, session_id, status, updated_at) - VALUES ('cf-session-1', 1, '/workspace', '{}', '01J0000000000000000000000Y', '01J0000000000000000000000C', 'active', 1) - `, - ) - .run(); await expect(repairStrandedRuntimeSubjectDeadlines(database, { now: nowMs })).resolves.toEqual({ cattle: 0, pet: 0, }); + await expect(readSandboxDeadline(database)).resolves.toBeNull(); + }); + + test("leaves cattle with an active conversation resident", async () => { + const database = await createPublicHttpContractDatabase(); + await insertNonOwnerSession(database); + const nowMs = Date.now(); + await insertSandbox(database, "cattle"); - await expect(readSandboxDeadline(database, "01J0000000000000000000000V")).resolves.toBeNull(); - await expect(readSandboxDeadline(database, "01J0000000000000000000000X")).resolves.toBeNull(); - await expect(readSandboxDeadline(database, "01J0000000000000000000000Y")).resolves.toBeNull(); + await expect(repairStrandedRuntimeSubjectDeadlines(database, { now: nowMs })).resolves.toEqual({ + cattle: 0, + pet: 0, + }); + await expect(readSandboxDeadline(database)).resolves.toBeNull(); }); }); diff --git a/apps/api/tests/prod-deploy-lease.test.ts b/apps/api/tests/prod-deploy-lease.test.ts new file mode 100644 index 00000000..45c1c139 --- /dev/null +++ b/apps/api/tests/prod-deploy-lease.test.ts @@ -0,0 +1,209 @@ +import { describe, expect, test } from "bun:test"; + +import { acquireProdDeployLease } from "../bin/deploy-prod"; +import { + acquireProdDeployLeaseStatements, + assertProdDeployLeaseOwned, + assertProdDeployLeaseReleased, + PROD_DEPLOY_LEASE_TABLE, + PROD_DEPLOY_LEASE_TABLE_SQL, + releaseProdDeployLeaseStatements, + verifyProdDeployLeaseStatements, +} from "../bin/prod-deploy-lease"; +import { SqliteD1Database } from "./helpers/sqlite-d1"; + +const OWNER_A = "aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa"; +const OWNER_B = "bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb"; +const OWNED_ROW = { + owner: OWNER_A, + table_sql: PROD_DEPLOY_LEASE_TABLE_SQL, + trigger_count: 0, +}; + +async function executeBatch(database: SqliteD1Database, statements: readonly string[]) { + return JSON.stringify(await database.batch(statements.map((sql) => database.prepare(sql)))); +} + +async function readOwner(database: SqliteD1Database): Promise { + const row = await database + .prepare(`SELECT "owner" FROM "${PROD_DEPLOY_LEASE_TABLE}" WHERE "id" = 1`) + .first<{ owner: string }>(); + return row?.owner ?? null; +} + +describe("production deploy lease", () => { + test("keeps a duplicate same-owner batch idempotent while the mutex remains held", async () => { + const database = new SqliteD1Database(); + await executeBatch(database, acquireProdDeployLeaseStatements(OWNER_A)); + + const retried = await executeBatch(database, acquireProdDeployLeaseStatements(OWNER_A)); + expect(() => + assertProdDeployLeaseOwned(`[wrangler warning]\n${retried}\n[trailing warning]`, OWNER_A), + ).not.toThrow(); + expect(await readOwner(database)).toBe(OWNER_A); + }); + + test("verifies ownership without mutating the durable mutex", async () => { + const database = new SqliteD1Database(); + assertProdDeployLeaseOwned( + await executeBatch(database, acquireProdDeployLeaseStatements(OWNER_A)), + OWNER_A, + ); + + assertProdDeployLeaseOwned( + await executeBatch(database, verifyProdDeployLeaseStatements()), + OWNER_A, + ); + expect(await readOwner(database)).toBe(OWNER_A); + }); + + test.each([ + ["no statement", JSON.stringify([])], + [ + "wrong statement count", + JSON.stringify([ + { results: [], success: true }, + { results: [OWNED_ROW], success: true }, + ]), + ], + ["no row", JSON.stringify([{ results: [], success: true }])], + ["multiple rows", JSON.stringify([{ results: [OWNED_ROW, OWNED_ROW], success: true }])], + [ + "malformed owner", + JSON.stringify([{ results: [{ ...OWNED_ROW, owner: null }], success: true }]), + ], + ])("rejects an owned-mutex readback with %s", (_label, raw) => { + expect(() => assertProdDeployLeaseOwned(raw, OWNER_A)).toThrow(); + }); + + test("retries release after the owner delete response is lost", async () => { + const database = new SqliteD1Database(); + assertProdDeployLeaseOwned( + await executeBatch(database, acquireProdDeployLeaseStatements(OWNER_A)), + OWNER_A, + ); + await executeBatch(database, releaseProdDeployLeaseStatements(OWNER_A)); + + const retried = await executeBatch(database, releaseProdDeployLeaseStatements(OWNER_A)); + expect(() => assertProdDeployLeaseReleased(retried)).not.toThrow(); + expect(await readOwner(database)).toBeNull(); + }); + + test("never transfers ownership until the current owner explicitly releases", async () => { + const database = new SqliteD1Database(); + assertProdDeployLeaseOwned( + await executeBatch(database, acquireProdDeployLeaseStatements(OWNER_A)), + OWNER_A, + ); + + const contended = await executeBatch(database, acquireProdDeployLeaseStatements(OWNER_B)); + expect(() => assertProdDeployLeaseOwned(contended, OWNER_B)).toThrow("another owner"); + expect(await readOwner(database)).toBe(OWNER_A); + + assertProdDeployLeaseOwned( + await executeBatch(database, verifyProdDeployLeaseStatements()), + OWNER_A, + ); + assertProdDeployLeaseReleased( + await executeBatch(database, releaseProdDeployLeaseStatements(OWNER_A)), + ); + + assertProdDeployLeaseOwned( + await executeBatch(database, acquireProdDeployLeaseStatements(OWNER_B)), + OWNER_B, + ); + const staleVerification = await executeBatch(database, verifyProdDeployLeaseStatements()); + expect(() => assertProdDeployLeaseOwned(staleVerification, OWNER_A)).toThrow("another owner"); + + const lateRelease = await executeBatch(database, releaseProdDeployLeaseStatements(OWNER_A)); + expect(() => assertProdDeployLeaseReleased(lateRelease)).toThrow(); + expect(await readOwner(database)).toBe(OWNER_B); + + const release = await executeBatch(database, releaseProdDeployLeaseStatements(OWNER_B)); + expect(() => assertProdDeployLeaseReleased(release)).not.toThrow(); + expect(await readOwner(database)).toBeNull(); + }); + + test("sends one acquisition request and retains a late commit for exact-owner recovery", async () => { + const database = new SqliteD1Database(); + const lostResponse = new Error("acquisition response was lost"); + let calls = 0; + let commitLate: (() => Promise) | undefined; + + expect(() => + acquireProdDeployLease(OWNER_A, (statements) => { + calls += 1; + commitLate = () => executeBatch(database, statements); + throw lostResponse; + }), + ).toThrow(lostResponse); + expect(calls).toBe(1); + + await commitLate?.(); + expect(await readOwner(database)).toBe(OWNER_A); + assertProdDeployLeaseReleased( + await executeBatch(database, releaseProdDeployLeaseStatements(OWNER_A)), + ); + }); + + test("retains the mutex after deployment failure and releases it only after success", async () => { + const source = await Bun.file(new URL("../bin/deploy-prod.ts", import.meta.url)).text(); + const retained = source.indexOf("✗ Retaining production deploy lease"); + const rethrown = source.indexOf("throw error;", retained); + const released = source.indexOf("releaseProdDeployLease();", rethrown); + + expect(retained).toBeGreaterThanOrEqual(0); + expect(rethrown).toBeGreaterThan(retained); + expect(released).toBeGreaterThan(rethrown); + }); + + test("rejects incomplete or non-empty release readbacks", () => { + const incomplete = JSON.stringify([{ results: [], success: true }]); + const multipleOwners = JSON.stringify([ + { results: [], success: true }, + { results: [{ owner: OWNER_B }, { owner: OWNER_B }], success: true }, + ]); + + expect(() => assertProdDeployLeaseReleased(incomplete)).toThrow(); + expect(() => assertProdDeployLeaseReleased(multipleOwners)).toThrow(); + }); + + test("fails closed when the reserved lease table has a spoofed schema", async () => { + const database = new SqliteD1Database(); + database.execute(` + CREATE TABLE "${PROD_DEPLOY_LEASE_TABLE}" ( + "id" integer PRIMARY KEY, + "owner" text NOT NULL, + "expires_at" integer NOT NULL + ) + `); + + const acquired = await executeBatch(database, acquireProdDeployLeaseStatements(OWNER_A)); + expect(() => assertProdDeployLeaseOwned(acquired, OWNER_A)).toThrow("schema is invalid"); + }); + + test("rejects lease-table triggers before they can change or release ownership", async () => { + const database = new SqliteD1Database(); + assertProdDeployLeaseOwned( + await executeBatch(database, acquireProdDeployLeaseStatements(OWNER_A)), + OWNER_A, + ); + database.execute(` + CREATE TRIGGER mutate_deploy_lease + AFTER DELETE ON "${PROD_DEPLOY_LEASE_TABLE}" + BEGIN + SELECT 1; + END + `); + + const verification = await executeBatch(database, verifyProdDeployLeaseStatements()); + expect(() => assertProdDeployLeaseOwned(verification, OWNER_A)).toThrow( + "must not have triggers", + ); + expect(await readOwner(database)).toBe(OWNER_A); + + const release = await executeBatch(database, releaseProdDeployLeaseStatements(OWNER_A)); + expect(() => assertProdDeployLeaseReleased(release)).toThrow(); + expect(await readOwner(database)).toBe(OWNER_A); + }); +}); diff --git a/apps/api/tests/prod-schema-guard.test.ts b/apps/api/tests/prod-schema-guard.test.ts index 8e37e7b3..6daaa9d0 100644 --- a/apps/api/tests/prod-schema-guard.test.ts +++ b/apps/api/tests/prod-schema-guard.test.ts @@ -1,106 +1,397 @@ import { describe, expect, test } from "bun:test"; +import { parseD1JsonResults } from "../bin/d1-json"; import { - extractTableNames, - findMissingProdTables, - getLatestSnapshotFilename, - parseExpectedTableNames, + assertProdSchemaMatches, + createProdSchemaCatalogFromIntrospectionRows, + createProdSchemaCatalogFromDrizzleSnapshot, + createProdSchemaIntrospectionStatements, + DRIZZLE_MIGRATION_PROD_SCHEMA_CATALOG, + findProdSchemaDifferences, + MANAGED_PROD_SCHEMA_TRIGGERS, + parseGeneratedProdSchemaCatalog, + PROTOCOL_V3_MIGRATION_INTENT_TABLE, + PROTOCOL_V3_MIGRATION_INTENT_TABLE_SQL, } from "../bin/prod-schema-guard"; +import { + applyDrizzleMigration, + assertDrizzleMigrationFiles, + drizzleMigrations, + latestDrizzleSnapshotFilename, +} from "./helpers/drizzle-migrations"; +import { SqliteD1Database } from "./helpers/sqlite-d1"; + +const VALID_SCHEMA_SQL = ` + CREATE TABLE parent ( + id text PRIMARY KEY NOT NULL, + state text DEFAULT 'ready' NOT NULL, + CONSTRAINT parent_state_check CHECK (state IN ('ready', 'retired')) + ); + CREATE TABLE child ( + tenant text NOT NULL, + id text NOT NULL, + parent_id text, + CONSTRAINT child_pk PRIMARY KEY (tenant, id), + CONSTRAINT child_parent_fk FOREIGN KEY (parent_id) + REFERENCES parent (id) ON UPDATE NO ACTION ON DELETE CASCADE, + CONSTRAINT child_id_check CHECK (length(id) > 0) + ); + CREATE UNIQUE INDEX child_parent_idx + ON child (parent_id) WHERE parent_id IS NOT NULL; + CREATE INDEX child_parent_expression_idx + ON child (coalesce(parent_id, id), tenant) WHERE parent_id IS NULL; + CREATE TABLE sequence_owner ( + id integer PRIMARY KEY AUTOINCREMENT + ); +`; + +const DRIZZLE_DIRECTORY = new URL("../../../pkgs/db/drizzle/", import.meta.url); +const PROTOCOL_V3_CUTOVER_TRIGGER_NAMES = [ + "__protocol_v3_cutover_api_command_insert", + "__protocol_v3_cutover_api_command_update", + "__protocol_v3_cutover_app_deployment_run_insert", + "__protocol_v3_cutover_app_deployment_run_update", + "__protocol_v3_cutover_command_insert", + "__protocol_v3_cutover_driver_insert", + "__protocol_v3_cutover_driver_update", + "__protocol_v3_cutover_environment_artifact_backup_staging_insert", + "__protocol_v3_cutover_sandbox_backup_insert", + "__protocol_v3_cutover_sandbox_backup_staging_insert", + "__protocol_v3_cutover_sandbox_backup_update", + "__protocol_v3_cutover_sandbox_insert", + "__protocol_v3_cutover_sandbox_session_insert", + "__protocol_v3_cutover_sandbox_session_update", + "__protocol_v3_cutover_sandbox_update", + "__protocol_v3_cutover_session_insert", + "__protocol_v3_cutover_session_run_insert", + "__protocol_v3_cutover_session_run_update", + "__protocol_v3_cutover_session_update", +] as const; + +async function readCatalog(database: SqliteD1Database) { + const tableNames = await readTableNames(database); + const rows = []; + for (const statement of createProdSchemaIntrospectionStatements(tableNames)) { + rows.push((await database.prepare(statement).all>()).results); + } + return createProdSchemaCatalogFromIntrospectionRows(rows, tableNames); +} + +async function readTableNames(database: SqliteD1Database): Promise { + const { results } = await database + .prepare( + "SELECT name FROM sqlite_master WHERE type = 'table' AND name NOT LIKE 'sqlite_%' ORDER BY name", + ) + .all<{ name: string }>(); + return results.map(({ name }) => name); +} + +async function catalogFor(sql: string) { + const database = new SqliteD1Database(); + database.execute(sql); + return readCatalog(database); +} + +describe("prod schema catalog", () => { + test("requires the journal to exactly match every ordered migration SQL file", () => { + const entries = [ + { idx: 0, tag: "0000_first" }, + { idx: 1, tag: "0001_second" }, + ]; + expect(() => + assertDrizzleMigrationFiles(entries, ["0000_first.sql", "0001_second.sql"], 2), + ).not.toThrow(); + for (const [candidateEntries, filenames, count] of [ + [entries, ["0000_first.sql", "0001_second.sql", "0002_untracked.sql"], 2], + [entries, ["0000_first.sql"], 2], + [[entries[0], { idx: 2, tag: "0002_second" }], ["0000_first.sql", "0002_second.sql"], 2], + [[entries[0], { idx: 1, tag: "0001/second" }], ["0000_first.sql", "0001/second.sql"], 2], + [entries, ["0000_first.sql", "0001_second.sql"], 1], + ] as const) { + expect(() => assertDrizzleMigrationFiles(candidateEntries, filenames, count)).toThrow(); + } + }); + + test("round-trips the multi-statement D1 JSON shape", async () => { + const database = new SqliteD1Database(); + database.execute(VALID_SCHEMA_SQL); + const tableNames = await readTableNames(database); + const results = []; + for (const statement of createProdSchemaIntrospectionStatements(tableNames)) { + results.push(await database.prepare(statement).all>()); + } -describe("getLatestSnapshotFilename", () => { - test("resolves the latest Drizzle snapshot from the journal", () => { expect( - getLatestSnapshotFilename( - JSON.stringify({ - entries: [ - { idx: 0, tag: "0000_baseline" }, - { idx: 1 }, - { idx: 2 }, - { idx: 3 }, - { idx: 4 }, - ], - }), + createProdSchemaCatalogFromIntrospectionRows( + parseD1JsonResults(`wrangler log\n${JSON.stringify(results)}`), + tableNames, ), - ).toBe("0004_snapshot.json"); + ).toEqual(await readCatalog(database)); + const missingSuccess: unknown[] = results.slice(); + missingSuccess[0] = { results: results[0]?.results }; + expect(() => parseD1JsonResults(JSON.stringify(missingSuccess))).toThrow(); }); - test("fails closed when the journal has no valid latest entry", () => { - expect(() => getLatestSnapshotFilename('{"entries":[]}')).toThrow(); + test("compares columns, composite PK, defaults, nullability, index predicate, FK, and checks", async () => { + const expected = await catalogFor(VALID_SCHEMA_SQL); + expect(() => assertProdSchemaMatches(expected, expected)).not.toThrow(); + + expect(expected.tables.find(({ name }) => name === "child")?.indexes).toContainEqual({ + columns: ["coalesce ( parent_id , id )", "tenant"], + name: "child_parent_expression_idx", + predicate: "parent_id is null", + unique: false, + }); + + const mutations = [ + VALID_SCHEMA_SQL.replace("state text", "state blob"), + VALID_SCHEMA_SQL.replace("DEFAULT 'ready'", "DEFAULT 'retired'"), + VALID_SCHEMA_SQL.replace("DEFAULT 'ready' NOT NULL", "DEFAULT 'ready'"), + VALID_SCHEMA_SQL.replace("parent_id text,", "parent_id text, extra text,"), + VALID_SCHEMA_SQL.replace("state text DEFAULT 'ready' NOT NULL", "state text"), + VALID_SCHEMA_SQL.replace("PRIMARY KEY (tenant, id)", "PRIMARY KEY (id, tenant)"), + VALID_SCHEMA_SQL.replace("child_parent_idx", "child_parent_other_idx"), + VALID_SCHEMA_SQL.replace( + "CREATE UNIQUE INDEX child_parent_idx", + "CREATE INDEX child_parent_idx", + ), + VALID_SCHEMA_SQL.replace("coalesce(parent_id, id)", "coalesce(parent_id, tenant)"), + VALID_SCHEMA_SQL.replace("WHERE parent_id IS NOT NULL", "WHERE parent_id IS NULL"), + VALID_SCHEMA_SQL.replace("ON UPDATE NO ACTION", "ON UPDATE CASCADE"), + VALID_SCHEMA_SQL.replace("ON DELETE CASCADE", "ON DELETE RESTRICT"), + VALID_SCHEMA_SQL.replace("length(id) > 0", "length(id) > 1"), + VALID_SCHEMA_SQL.replace("PRIMARY KEY AUTOINCREMENT", "PRIMARY KEY"), + ]; + for (const mutated of mutations) { + expect(findProdSchemaDifferences(expected, await catalogFor(mutated))).not.toEqual([]); + } + await expect( + catalogFor(VALID_SCHEMA_SQL.replace("parent_id text,", "parent_id text UNIQUE,")), + ).rejects.toThrow("unsupported inline UNIQUE constraints"); }); - test("fails closed when journal indexes are out of order, duplicated, or non-contiguous", () => { - for (const entries of [ - [{ idx: 1 }, { idx: 0 }], - [{ idx: 0 }, { idx: 0 }], - [{ idx: 0 }, { idx: 2 }], + test("compares table definitions for SQLite semantics missing from pragma metadata", async () => { + const plain = await catalogFor(`CREATE TABLE semantic ( + id text PRIMARY KEY NOT NULL, + source text NOT NULL, + derived text + )`); + const formatted = await catalogFor(`create table "semantic"( + "id" TEXT primary key not null, + "source" text not null, + "derived" text + )`); + expect(findProdSchemaDifferences(plain, formatted)).toEqual([]); + + for (const sql of [ + `CREATE TABLE semantic ( + id text COLLATE NOCASE PRIMARY KEY NOT NULL, + source text NOT NULL, + derived text + )`, + `CREATE TABLE semantic ( + id text PRIMARY KEY NOT NULL, + source text NOT NULL, + derived text + ) STRICT`, + `CREATE TABLE semantic ( + id text PRIMARY KEY NOT NULL, + source text NOT NULL, + derived text + ) WITHOUT ROWID`, ]) { - expect(() => getLatestSnapshotFilename(JSON.stringify({ entries }))).toThrow(); + expect(findProdSchemaDifferences(plain, await catalogFor(sql))).toContain( + "semantic.definition differs", + ); } - }); -}); -describe("parseExpectedTableNames", () => { - test("extracts sorted table names from a Drizzle snapshot", () => { - const snapshot = JSON.stringify({ - tables: { - session_run: { name: "session_run" }, - agent: { name: "agent" }, - }, - }); + const generatedVirtual = await catalogFor(`CREATE TABLE semantic ( + id text PRIMARY KEY NOT NULL, + source text NOT NULL, + derived text GENERATED ALWAYS AS (lower(source)) VIRTUAL + )`); + expect(findProdSchemaDifferences(plain, generatedVirtual)).toEqual([ + "semantic.columns differs", + "semantic.definition differs", + ]); - expect(parseExpectedTableNames(snapshot)).toEqual(["agent", "session_run"]); - }); + const generatedStored = await catalogFor(`CREATE TABLE semantic ( + id text PRIMARY KEY NOT NULL, + source text NOT NULL, + derived text GENERATED ALWAYS AS (lower(source)) STORED + )`); + expect(findProdSchemaDifferences(generatedVirtual, generatedStored)).toEqual([ + "semantic.columns differs", + "semantic.definition differs", + ]); - test("fails closed when the snapshot has no tables object", () => { - expect(() => parseExpectedTableNames("{}")).toThrow(); + const changedExpression = await catalogFor(`CREATE TABLE semantic ( + id text PRIMARY KEY NOT NULL, + source text NOT NULL, + derived text GENERATED ALWAYS AS (upper(source)) VIRTUAL + )`); + expect(findProdSchemaDifferences(generatedVirtual, changedExpression)).toEqual([ + "semantic.definition differs", + ]); }); - test("fails closed when the snapshot tables object is empty", () => { - expect(() => parseExpectedTableNames('{"tables":{}}')).toThrow(); - }); + test("ignores only known live internal tables and rejects application extras", async () => { + const expected = await catalogFor(VALID_SCHEMA_SQL); + const withInternalTables = await catalogFor(`${VALID_SCHEMA_SQL} + CREATE TABLE d1_migrations (id integer PRIMARY KEY AUTOINCREMENT); + CREATE TABLE _cf_KV (key text); + CREATE TABLE __production_deploy_lease (id integer); + CREATE TABLE __protocol_v3_cutover (id integer); + ${PROTOCOL_V3_MIGRATION_INTENT_TABLE_SQL.replace("CREATE TABLE", "CREATE TABLE IF NOT EXISTS")}; + `); + expect(findProdSchemaDifferences(expected, withInternalTables)).toEqual([]); - test("resolves the checked-in migration chain to a non-empty latest snapshot", async () => { - const metaDir = new URL("../../../pkgs/db/drizzle/meta/", import.meta.url); - const journal = await Bun.file(new URL("_journal.json", metaDir)).text(); - const snapshotFilename = getLatestSnapshotFilename(journal); - const snapshot = await Bun.file(new URL(snapshotFilename, metaDir)).text(); - const tableNames = parseExpectedTableNames(snapshot); + const malformedMigrationIntent = await catalogFor(`${VALID_SCHEMA_SQL} + CREATE TABLE __protocol_v3_cutover (id integer PRIMARY KEY); + ${PROTOCOL_V3_MIGRATION_INTENT_TABLE_SQL.replace("ON DELETE CASCADE", "ON DELETE RESTRICT")}; + `); + expect(findProdSchemaDifferences(expected, malformedMigrationIntent)).toContain( + `${PROTOCOL_V3_MIGRATION_INTENT_TABLE}.definition differs`, + ); - expect(tableNames).toContain("bound_agent_call_idempotency_key"); - expect(tableNames).toContain("usage_event_rollup_receipt"); - }); -}); + const triggeredMigrationIntent = await catalogFor(`${VALID_SCHEMA_SQL} + CREATE TABLE __protocol_v3_cutover (id integer PRIMARY KEY); + ${PROTOCOL_V3_MIGRATION_INTENT_TABLE_SQL}; + CREATE TRIGGER migration_intent_trigger + AFTER INSERT ON ${PROTOCOL_V3_MIGRATION_INTENT_TABLE} + BEGIN + SELECT 1; + END; + `); + expect(findProdSchemaDifferences(expected, triggeredMigrationIntent)).toContain( + "unexpected trigger migration_intent_trigger", + ); -describe("findMissingProdTables", () => { - test("returns expected tables absent from the live database", () => { - expect(findMissingProdTables(["agent", "session_run", "app"], ["agent", "app"])).toEqual([ - "session_run", - ]); + const withRetainedLegacyTable = await catalogFor( + `${VALID_SCHEMA_SQL} CREATE TABLE wechat_context_token (id integer);`, + ); + expect(findProdSchemaDifferences(expected, withRetainedLegacyTable)).toEqual([]); + + for (const name of [ + "WECHAT_CONTEXT_TOKEN", + "stale_application_table", + "__protocol_v3_legacy_rewrite_authorization", + ]) { + const withUnexpectedTable = await catalogFor( + `${VALID_SCHEMA_SQL} CREATE TABLE ${name} (id integer);`, + ); + expect(findProdSchemaDifferences(expected, withUnexpectedTable)).toEqual([ + `unexpected table ${name}`, + ]); + } }); - test("returns empty when every expected table is present (extra live tables are ignored)", () => { - expect(findMissingProdTables(["agent"], ["agent", "d1_migrations", "_cf_KV"])).toEqual([]); + test("requires the exact migration-owned trigger and rejects trigger extras", async () => { + const database = new SqliteD1Database(); + for (const migration of drizzleMigrations.filter(({ index }) => index <= 8)) { + applyDrizzleMigration(database, migration.tag); + } + const expected = await readCatalog(database); + const managed = MANAGED_PROD_SCHEMA_TRIGGERS.find( + ({ name }) => name === "session_event_tool_identity_consistency", + ); + if (!managed) throw new Error("Managed trigger fixture is missing."); + + database.execute(`DROP TRIGGER ${managed.name}`); + expect(findProdSchemaDifferences(expected, await readCatalog(database))).toContain( + `missing trigger ${managed.name}`, + ); + + database.execute(` + CREATE TRIGGER ${managed.name} + BEFORE INSERT ON session_event + WHEN 0 + BEGIN + SELECT 1; + END + `); + expect(findProdSchemaDifferences(expected, await readCatalog(database))).toContain( + `trigger ${managed.name} differs`, + ); + + database.execute(`DROP TRIGGER ${managed.name}`); + database.execute(managed.sql); + database.execute(` + CREATE TRIGGER extra_session_event_trigger + BEFORE UPDATE ON session_event + WHEN 0 + BEGIN + SELECT 1; + END + `); + expect(findProdSchemaDifferences(expected, await readCatalog(database))).toContain( + "unexpected trigger extra_session_event_trigger", + ); + + database.execute(` + CREATE TRIGGER __protocol_v3_cutover_api_command_insert + BEFORE UPDATE ON session_event + BEGIN + SELECT 1; + END + `); + expect(findProdSchemaDifferences(expected, await readCatalog(database))).toContain( + "trigger __protocol_v3_cutover_api_command_insert differs", + ); }); -}); -describe("extractTableNames", () => { - test("parses the wrangler d1 execute --json result shape", () => { - const stdout = JSON.stringify([ - { results: [{ name: "agent" }, { name: "session_run" }], success: true, meta: {} }, - ]); + test("pins the transient cutover trigger set through authority migrations", async () => { + const database = new SqliteD1Database(); + const stages = []; + for (const migration of drizzleMigrations) { + applyDrizzleMigration(database, migration.tag); + if ( + migration.tag === "0019_runtime-subject-operation-authority" || + migration.tag === "0020_sandbox-backup-object-authority" + ) { + stages.push({ + tag: migration.tag, + triggers: (await readCatalog(database)).triggers.filter(({ name }) => + name.startsWith("__protocol_v3_cutover_"), + ), + }); + } + } - expect(extractTableNames(stdout)).toEqual(["agent", "session_run"]); + const canonical = DRIZZLE_MIGRATION_PROD_SCHEMA_CATALOG.triggers.filter(({ name }) => + name.startsWith("__protocol_v3_cutover_"), + ); + expect(canonical.map(({ name }) => name)).toEqual(PROTOCOL_V3_CUTOVER_TRIGGER_NAMES); + expect(stages).toEqual([ + { tag: "0019_runtime-subject-operation-authority", triggers: canonical }, + { tag: "0020_sandbox-backup-object-authority", triggers: canonical }, + ]); }); - test("tolerates leading log lines before the JSON array", () => { - const stdout = `🌀 Executing on remote database DB\n${JSON.stringify([ - { results: [{ name: "agent" }] }, - ])}`; + test("matches the complete migration chain against the latest snapshot", async () => { + const snapshot = await Bun.file( + new URL(`meta/${latestDrizzleSnapshotFilename}`, DRIZZLE_DIRECTORY), + ).json(); + const expected = createProdSchemaCatalogFromDrizzleSnapshot(snapshot); + expect(findProdSchemaDifferences(expected, DRIZZLE_MIGRATION_PROD_SCHEMA_CATALOG)).toEqual([]); + const deployExpected = parseGeneratedProdSchemaCatalog(JSON.stringify(snapshot)); + expect(deployExpected.tables.every(({ definition }) => definition !== null)).toBeTrue(); + expect( + findProdSchemaDifferences(deployExpected, DRIZZLE_MIGRATION_PROD_SCHEMA_CATALOG), + ).toEqual([]); - expect(extractTableNames(stdout)).toEqual(["agent"]); + const firstTable = Object.values( + (snapshot as { tables: Record> }).tables, + )[0]; + if (firstTable === undefined) throw new Error("Drizzle snapshot table fixture is missing."); + firstTable["uniqueConstraints"] = { unsupported: {} }; + expect(() => createProdSchemaCatalogFromDrizzleSnapshot(snapshot)).toThrow( + "unsupported inline UNIQUE constraints", + ); }); - test("throws (fails closed) when no JSON array is present", () => { - expect(() => extractTableNames("error: could not connect")).toThrow(); + test("fails closed on malformed D1 output", () => { + expect(() => parseD1JsonResults("not json")).toThrow(); + expect(() => parseD1JsonResults("[]")).toThrow(); }); }); diff --git a/apps/api/tests/prod-wfp-preflight.test.ts b/apps/api/tests/prod-wfp-preflight.test.ts new file mode 100644 index 00000000..60c478fc --- /dev/null +++ b/apps/api/tests/prod-wfp-preflight.test.ts @@ -0,0 +1,511 @@ +import { describe, expect, test } from "bun:test"; + +import { + apiWorkerDeployArgs, + apiWorkerDryRunArgs, + assertProdWfpReadOnlyInfrastructure, + assertProdWfpWorkflowDeployGate, + PROD_API_WORKER_NAME, + PROD_APP_DEPLOYMENT_DOMAIN, + PROD_APP_DEPLOYMENT_WORKFLOW, + PROD_APP_DEPLOYMENT_WORKFLOW_BINDING, + PROD_APP_DEPLOYMENT_WORKFLOW_CLASS, + PROD_APP_DISPATCH_NAMESPACE, + PROD_APP_WILDCARD_DNS, + PROD_APP_WILDCARD_ROUTE, + PROD_WFP_WRITE_CANARY_SCRIPT, + shouldProbeProdWfpBeforeMutation, + verifyProdWfpGatewayProbe, + verifyProdWfpWriteCanary, + WRANGLER_DISABLE_AUTO_PROVISION, +} from "../bin/prod-wfp-preflight"; +import type { ProdWfpReadOnlyInventory, WfpWriteCanaryClient } from "../bin/prod-wfp-preflight"; +import { + APP_DEPLOYMENT_PROBE_HOST_LABEL, + APP_DEPLOYMENT_PROBE_SCRIPT_NAME, +} from "../src/modules/apps/application/app-deployment-gateway"; + +const ACCOUNT_ID = "a".repeat(32); +const ZONE_ID = "b".repeat(32); +const NOW_MS = Date.parse("2026-08-30T00:00:00.000Z"); +const APP_ID = "01J0000000000000000000000Q"; +const CONFIG = { + accountId: ACCOUNT_ID, + apiWorkerName: PROD_API_WORKER_NAME, + dispatchBinding: "APP_DEPLOYMENT_DISPATCHER", + dispatchNamespace: PROD_APP_DISPATCH_NAMESPACE, + requireProbeOnlyNamespace: false, + wildcardDns: PROD_APP_WILDCARD_DNS, + wildcardRoute: PROD_APP_WILDCARD_ROUTE, + workflowBinding: PROD_APP_DEPLOYMENT_WORKFLOW_BINDING, + workflowClass: PROD_APP_DEPLOYMENT_WORKFLOW_CLASS, + workflowName: PROD_APP_DEPLOYMENT_WORKFLOW, + workflowRepairAllowed: false, + zoneId: ZONE_ID, + zoneName: "mosoo.ai", +} as const; +function validInventory(): ProdWfpReadOnlyInventory { + return { + apiWorkerBindings: [ + { + className: null, + name: "APP_DEPLOYMENT_DISPATCHER", + namespace: PROD_APP_DISPATCH_NAMESPACE, + scriptName: null, + type: "dispatch_namespace", + workflowName: null, + }, + { + className: PROD_APP_DEPLOYMENT_WORKFLOW_CLASS, + name: PROD_APP_DEPLOYMENT_WORKFLOW_BINDING, + namespace: null, + scriptName: PROD_API_WORKER_NAME, + type: "workflow", + workflowName: PROD_APP_DEPLOYMENT_WORKFLOW, + }, + ], + certificates: [ + { + certificates: [ + { + expiresOn: "2026-12-31T00:00:00.000Z", + hosts: ["mosoo.ai", PROD_APP_WILDCARD_DNS], + status: "active", + }, + ], + hosts: ["mosoo.ai", PROD_APP_WILDCARD_DNS], + status: "active", + }, + ], + dnsRecords: [ + { name: PROD_APP_WILDCARD_DNS, proxied: true, type: "A" }, + { name: "api.mosoo.ai", proxied: true, type: "A" }, + { name: "mosoo.ai", proxied: null, type: "TXT" }, + ], + legacyResources: [], + namespace: { + id: "namespace-id", + name: PROD_APP_DISPATCH_NAMESPACE, + scriptCount: 2, + trustedWorkers: false, + }, + probeScript: { + id: APP_DEPLOYMENT_PROBE_SCRIPT_NAME, + namespace: PROD_APP_DISPATCH_NAMESPACE, + }, + routes: [ + { pattern: PROD_APP_WILDCARD_ROUTE, script: PROD_API_WORKER_NAME }, + { pattern: "api.mosoo.ai/*", script: "unrelated-worker" }, + ], + workflow: { + className: PROD_APP_DEPLOYMENT_WORKFLOW_CLASS, + id: "workflow-id", + name: PROD_APP_DEPLOYMENT_WORKFLOW, + scriptName: PROD_API_WORKER_NAME, + }, + zone: { accountId: ACCOUNT_ID, id: ZONE_ID, name: "mosoo.ai", status: "active" }, + }; +} + +describe("production WfP preflight", () => { + test("disables Wrangler auto-provision for API dry-run and deployment", () => { + expect(apiWorkerDryRunArgs()).toEqual([ + "deploy", + "--env", + "prod", + "--minify", + "--strict", + "--dry-run", + WRANGLER_DISABLE_AUTO_PROVISION, + ]); + expect(apiWorkerDeployArgs("release-tag")).toEqual([ + "deploy", + "--env", + "prod", + "--minify", + "--strict", + WRANGLER_DISABLE_AUTO_PROVISION, + "--containers-rollout", + "immediate", + "--tag", + "release-tag", + ]); + }); + + test("keeps every Workflow and the stage/prod dispatch topology exact", async () => { + const wrangler = Bun.TOML.parse( + await Bun.file(new URL("../wrangler.toml", import.meta.url)).text(), + ) as { + workflows: Array<{ binding: string; class_name: string; name: string }>; + env: Record< + string, + { + dispatch_namespaces: Array<{ binding: string; namespace: string }>; + routes: Array<{ pattern: string; zone_name: string }>; + vars: Record; + workflows: Array<{ binding: string; class_name: string; name: string }>; + } + >; + }; + + expect(wrangler.workflows).toEqual([ + { + binding: PROD_APP_DEPLOYMENT_WORKFLOW_BINDING, + class_name: PROD_APP_DEPLOYMENT_WORKFLOW_CLASS, + name: "mosoo-app-deployment-dev", + }, + ]); + + for (const [environment, domain, namespace, workflow] of [ + ["stage", "apps-stage.mosoo.ai", "mosoo-app-deployments-stage", "mosoo-app-deployment-stage"], + [ + "prod", + PROD_APP_DEPLOYMENT_DOMAIN, + PROD_APP_DISPATCH_NAMESPACE, + PROD_APP_DEPLOYMENT_WORKFLOW, + ], + ] as const) { + const config = wrangler.env[environment]; + expect(config?.vars["MOSOO_APP_DEPLOYMENT_DOMAIN"]).toBe(domain); + expect(config?.vars["MOSOO_APP_DISPATCH_NAMESPACE"]).toBe(namespace); + expect(config?.dispatch_namespaces).toEqual([ + { binding: "APP_DEPLOYMENT_DISPATCHER", namespace }, + ]); + expect(config?.workflows).toEqual([ + { + binding: PROD_APP_DEPLOYMENT_WORKFLOW_BINDING, + class_name: PROD_APP_DEPLOYMENT_WORKFLOW_CLASS, + name: workflow, + }, + ]); + expect(config?.routes).toContainEqual({ pattern: `*.${domain}/*`, zone_name: "mosoo.ai" }); + } + + expect(PROD_APP_WILDCARD_ROUTE).toBe("*.apps.mosoo.ai/*"); + }); + + test("accepts only the exact active account, zone, namespace, DNS, and certificate", () => { + expect(assertProdWfpReadOnlyInfrastructure(validInventory(), CONFIG, NOW_MS)).toBe("exact"); + + const trusted = validInventory(); + trusted.namespace.trustedWorkers = true; + expect(() => assertProdWfpReadOnlyInfrastructure(trusted, CONFIG, NOW_MS)).toThrow( + "missing, mismatched, or trusted", + ); + + const inconsistentCount = validInventory(); + inconsistentCount.namespace.scriptCount = 0; + expect(() => assertProdWfpReadOnlyInfrastructure(inconsistentCount, CONFIG, NOW_MS)).toThrow( + "missing, mismatched, or trusted", + ); + + const wrongAccount = validInventory(); + wrongAccount.zone.accountId = "c".repeat(32); + expect(() => assertProdWfpReadOnlyInfrastructure(wrongAccount, CONFIG, NOW_MS)).toThrow( + "zone identity", + ); + + const dnsOnly = validInventory(); + dnsOnly.dnsRecords = [{ name: PROD_APP_WILDCARD_DNS, proxied: false, type: "CNAME" }]; + expect(() => assertProdWfpReadOnlyInfrastructure(dnsOnly, CONFIG, NOW_MS)).toThrow( + "proxied traffic records", + ); + }); + + test("requires exact remote route, binding, probe script, and probe-only bootstrap namespace", () => { + const missingRoute = validInventory(); + missingRoute.routes = []; + expect(() => assertProdWfpReadOnlyInfrastructure(missingRoute, CONFIG, NOW_MS)).toThrow( + "route *.apps.mosoo.ai/* exactly once", + ); + + const wrongBinding = validInventory(); + wrongBinding.apiWorkerBindings[0].namespace = "wrong-namespace"; + expect(() => assertProdWfpReadOnlyInfrastructure(wrongBinding, CONFIG, NOW_MS)).toThrow( + "must bind APP_DEPLOYMENT_DISPATCHER", + ); + + for (const probeScript of [ + { id: null, namespace: null }, + { id: APP_DEPLOYMENT_PROBE_SCRIPT_NAME, namespace: "wrong-namespace" }, + { id: "wrong-probe", namespace: PROD_APP_DISPATCH_NAMESPACE }, + ]) { + const inventory = validInventory(); + inventory.probeScript = probeScript; + expect(() => assertProdWfpReadOnlyInfrastructure(inventory, CONFIG, NOW_MS)).toThrow( + `pre-provisioned ${APP_DEPLOYMENT_PROBE_SCRIPT_NAME}`, + ); + } + + const missingWorkflow = validInventory(); + missingWorkflow.workflow.id = null; + expect(() => assertProdWfpReadOnlyInfrastructure(missingWorkflow, CONFIG, NOW_MS)).toThrow( + "Bootstrap the exact AppDeploymentWorkflow", + ); + + const workflowBootstrap = validInventory(); + workflowBootstrap.workflow = { + className: null, + id: null, + name: null, + scriptName: null, + }; + workflowBootstrap.apiWorkerBindings = workflowBootstrap.apiWorkerBindings.filter( + (binding) => binding.type !== "workflow", + ); + expect(assertProdWfpReadOnlyInfrastructure(workflowBootstrap, CONFIG, NOW_MS)).toBe( + "bootstrap", + ); + expect(() => assertProdWfpWorkflowDeployGate("bootstrap", false)).toThrow( + "requires the closed production gate", + ); + expect(() => assertProdWfpWorkflowDeployGate("bootstrap", true)).not.toThrow(); + expect(shouldProbeProdWfpBeforeMutation("bootstrap")).toBe(false); + expect(shouldProbeProdWfpBeforeMutation("exact")).toBe(true); + + const interruptedBootstrap = validInventory(); + interruptedBootstrap.apiWorkerBindings = interruptedBootstrap.apiWorkerBindings.filter( + (binding) => binding.type !== "workflow", + ); + expect(() => assertProdWfpReadOnlyInfrastructure(interruptedBootstrap, CONFIG, NOW_MS)).toThrow( + "must bind APP_DEPLOYMENT_WORKFLOW", + ); + expect( + assertProdWfpReadOnlyInfrastructure( + interruptedBootstrap, + { ...CONFIG, workflowRepairAllowed: true }, + NOW_MS, + ), + ).toBe("repair"); + expect(() => assertProdWfpWorkflowDeployGate("repair", false)).toThrow( + "requires the closed production gate", + ); + expect(shouldProbeProdWfpBeforeMutation("repair")).toBe(false); + + const wrongWorkflowBinding = validInventory(); + wrongWorkflowBinding.apiWorkerBindings[1].workflowName = "wrong-workflow"; + expect(() => assertProdWfpReadOnlyInfrastructure(wrongWorkflowBinding, CONFIG, NOW_MS)).toThrow( + "must bind APP_DEPLOYMENT_WORKFLOW", + ); + + const removedWorkflowWithOldBinding = validInventory(); + removedWorkflowWithOldBinding.workflow = workflowBootstrap.workflow; + expect(() => + assertProdWfpReadOnlyInfrastructure(removedWorkflowWithOldBinding, CONFIG, NOW_MS), + ).toThrow("Bootstrap the exact AppDeploymentWorkflow"); + + const bootstrap = validInventory(); + expect(() => + assertProdWfpReadOnlyInfrastructure( + bootstrap, + { ...CONFIG, requireProbeOnlyNamespace: true }, + NOW_MS, + ), + ).toThrow("contain only the pre-provisioned"); + bootstrap.namespace.scriptCount = 1; + expect(() => + assertProdWfpReadOnlyInfrastructure( + bootstrap, + { ...CONFIG, requireProbeOnlyNamespace: true }, + NOW_MS, + ), + ).not.toThrow(); + }); + + test("rejects every Worker route that can shadow the deployment wildcard", () => { + for (const route of [ + { pattern: `${PROD_APP_WILDCARD_DNS}/admin*`, script: "other-worker" }, + { + pattern: `https://${APP_DEPLOYMENT_PROBE_HOST_LABEL}.${PROD_APP_DEPLOYMENT_DOMAIN}/*`, + script: "other-worker", + }, + { + pattern: `app-${APP_ID.toLowerCase()}.${PROD_APP_DEPLOYMENT_DOMAIN}/*`, + script: null, + }, + { pattern: "*.mosoo.ai/private*", script: null }, + { pattern: null, script: null }, + ]) { + const inventory = validInventory(); + inventory.routes = [...inventory.routes, route]; + expect(() => assertProdWfpReadOnlyInfrastructure(inventory, CONFIG, NOW_MS)).toThrow( + "must not shadow *.apps.mosoo.ai/*", + ); + } + }); + + test("rejects every specific DNS record below the deployment wildcard", () => { + for (const record of [ + { name: `probe.${PROD_APP_DEPLOYMENT_DOMAIN}`, proxied: true, type: "A" }, + { + name: `app-${APP_ID.toLowerCase()}.${PROD_APP_DEPLOYMENT_DOMAIN}`, + proxied: true, + type: "CNAME", + }, + { name: `disabled.${PROD_APP_DEPLOYMENT_DOMAIN}`, proxied: null, type: "TXT" }, + { name: `delegated.${PROD_APP_DEPLOYMENT_DOMAIN}`, proxied: null, type: "NS" }, + { + name: `_validation.probe.${PROD_APP_DEPLOYMENT_DOMAIN}`, + proxied: null, + type: "TXT", + }, + ]) { + const inventory = validInventory(); + inventory.dnsRecords = [...inventory.dnsRecords, record]; + expect(() => assertProdWfpReadOnlyInfrastructure(inventory, CONFIG, NOW_MS)).toThrow( + "DNS records must not shadow *.apps.mosoo.ai", + ); + } + }); + + test("reports legacy Pages/classic Worker resources without deleting them", () => { + const inventory = validInventory(); + inventory.legacyResources = [ + "Pages project app-legacy (delete project app-legacy)", + "classic Worker route route-id: app-legacy.apps.mosoo.ai/*", + ]; + + expect(() => assertProdWfpReadOnlyInfrastructure(inventory, CONFIG, NOW_MS)).toThrow( + "Pages project app-legacy", + ); + }); + + test("rejects shallow, single-host, inactive, and expiring certificates", () => { + for (const mutate of [ + (inventory: ProdWfpReadOnlyInventory) => { + inventory.certificates[0].hosts = ["*.mosoo.ai"]; + inventory.certificates[0].certificates[0].hosts = ["*.mosoo.ai"]; + }, + (inventory: ProdWfpReadOnlyInventory) => { + const exactHost = `app-${APP_ID.toLowerCase()}.${PROD_APP_DEPLOYMENT_DOMAIN}`; + inventory.certificates[0].hosts = [exactHost]; + inventory.certificates[0].certificates[0].hosts = [exactHost]; + }, + (inventory: ProdWfpReadOnlyInventory) => { + inventory.certificates[0].status = "pending_deployment"; + }, + (inventory: ProdWfpReadOnlyInventory) => { + inventory.certificates[0].certificates[0].expiresOn = "2026-09-20T00:00:00.000Z"; + }, + ]) { + const inventory = validInventory(); + mutate(inventory); + expect(() => assertProdWfpReadOnlyInfrastructure(inventory, CONFIG, NOW_MS)).toThrow( + "valid for at least 30 more days", + ); + } + }); + + test("proves Workers Scripts Write and always verifies exact cleanup", async () => { + const calls: string[] = []; + let exists = false; + const client: WfpWriteCanaryClient = { + async deleteScript(name) { + calls.push(`delete:${name}`); + exists = false; + }, + async readScript(name) { + calls.push(`read:${name}`); + return exists ? { id: name, namespace: PROD_APP_DISPATCH_NAMESPACE } : null; + }, + async uploadScript(name) { + calls.push(`upload:${name}`); + exists = true; + return { id: name }; + }, + }; + + await expect( + verifyProdWfpWriteCanary(client, PROD_APP_DISPATCH_NAMESPACE, PROD_WFP_WRITE_CANARY_SCRIPT), + ).resolves.toBeUndefined(); + expect(calls).toEqual([ + `delete:${PROD_WFP_WRITE_CANARY_SCRIPT}`, + `read:${PROD_WFP_WRITE_CANARY_SCRIPT}`, + `upload:${PROD_WFP_WRITE_CANARY_SCRIPT}`, + `read:${PROD_WFP_WRITE_CANARY_SCRIPT}`, + `delete:${PROD_WFP_WRITE_CANARY_SCRIPT}`, + `read:${PROD_WFP_WRITE_CANARY_SCRIPT}`, + ]); + }); + + test("fails closed when upload or deletion readback is not exact", async () => { + const remainingClient: WfpWriteCanaryClient = { + async deleteScript() {}, + async readScript(name) { + return { id: name, namespace: PROD_APP_DISPATCH_NAMESPACE }; + }, + async uploadScript() { + return { id: "wrong-script" }; + }, + }; + + await expect( + verifyProdWfpWriteCanary( + remainingClient, + PROD_APP_DISPATCH_NAMESPACE, + PROD_WFP_WRITE_CANARY_SCRIPT, + ), + ).rejects.toThrow("curl -X DELETE"); + }); + + test("probes request/body, stream, cancellation recovery, and WebSocket", async () => { + const calls: string[] = []; + const encoder = new TextEncoder(); + const fetcher: typeof fetch = async (input, init) => { + const url = new URL( + typeof input === "string" ? input : input instanceof URL ? input.href : input.url, + ); + calls.push(url.pathname); + const nonce = url.searchParams.get("nonce") ?? ""; + + if (url.pathname.endsWith("/http")) { + return Response.json( + { + body: typeof init?.body === "string" ? init.body : null, + hostname: url.hostname, + method: init?.method, + nonce, + pathname: url.pathname, + search: url.search, + }, + { headers: { "Cache-Control": "no-store" } }, + ); + } + + const chunks = url.pathname.endsWith("/stream") + ? [`start:${nonce}\n`, `end:${nonce}\n`] + : [`start:${nonce}\n`]; + return new Response( + new ReadableStream({ + start(controller) { + for (const chunk of chunks) controller.enqueue(encoder.encode(chunk)); + if (url.pathname.endsWith("/stream")) controller.close(); + }, + }), + ); + }; + let websocket: { nonce: string; url: string } | null = null; + + await verifyProdWfpGatewayProbe( + PROD_APP_DEPLOYMENT_DOMAIN, + { + fetch: fetcher, + probeWebSocket: async (url, nonce) => { + websocket = { nonce, url }; + }, + }, + () => "probe-nonce", + ); + + expect(calls).toEqual([ + "/.well-known/mosoo-wfp-probe/http", + "/.well-known/mosoo-wfp-probe/stream", + "/.well-known/mosoo-wfp-probe/cancel", + "/.well-known/mosoo-wfp-probe/http", + ]); + expect(websocket).toEqual({ + nonce: "probe-nonce", + url: `wss://${APP_DEPLOYMENT_PROBE_HOST_LABEL}.apps.mosoo.ai/.well-known/mosoo-wfp-probe/websocket?nonce=probe-nonce`, + }); + }); +}); diff --git a/apps/api/tests/production-deploy-safety.test.ts b/apps/api/tests/production-deploy-safety.test.ts index 6aaae4a6..41569b2c 100644 --- a/apps/api/tests/production-deploy-safety.test.ts +++ b/apps/api/tests/production-deploy-safety.test.ts @@ -1,9 +1,2740 @@ -import { expect, test } from "bun:test"; +import { describe, expect, test } from "bun:test"; -test("switches the production Worker and Driver image without a gradual protocol split", async () => { - const source = await Bun.file(new URL("../bin/deploy-prod.ts", import.meta.url)).text(); +import { MANAGED_PROD_SCHEMA_TRIGGERS } from "../bin/prod-schema-guard"; +import { + ACCEPT_PROTOCOL_V3_QUEUE_RESUME_SQL, + assertCutoverMigrationJournalAudited, + assertProtocolV3SmokeAgent, + assertProtocolV3Release, + assertProtocolV3WorkerVersion, + assertProtocolV3LegacyTerminalIntegrity, + assertProtocolV3LegacyTerminalSourceInventory, + assertProtocolV3LossyMigrationInventory, + assertProtocolV3RuntimeAuthorityPreflight, + AUDITED_MIGRATION_NAMES, + beginProtocolV3MigrationSql, + CLOSE_PROTOCOL_V3_SMOKE_WINDOW_SQL, + collectProtocolV3ContainerApplications, + collectProtocolV3ContainerInstances, + completeProtocolV3QueueResume, + DROP_PROTOCOL_V3_CUTOVER_TRIGGERS_SQL, + ENABLE_PROTOCOL_V3_COMMAND_FREEZE_SQL, + ENTER_PROTOCOL_V3_DRAIN_SQL, + ENTER_PROTOCOL_V3_QUEUES_RESUMING_SQL, + findPendingProdMigrations, + installProtocolV3CutoverSql, + installProtocolV3PostMigrationCutoverSql, + INSTALL_PROTOCOL_V3_POST_MIGRATION_CUTOVER_SQL, + isProtocolV3ContainerRolloutConverged, + isProtocolV3CutoverDrained, + isProtocolV3RuntimeDrained, + isProtocolV3SmokeReady, + openProtocolV3SmokeWindowSql, + parseProtocolV3CommandFreeze, + parseCleanGitTreeOid, + parseProtocolV3ContainerManifestDigest, + parseProtocolV3CutoverDrain, + parseProtocolV3CutoverObjects, + parseProtocolV3CutoverProbe, + parseProtocolV3CutoverState, + parseProtocolV3LegacyTerminalIntegrity, + parseProtocolV3LegacyTerminalSourceInventory, + parseProtocolV3LossyMigrationInventory, + parseProtocolV3RuntimeAuthorityPreflight, + parseProtocolV3SmokeStatus, + parseProtocolV3WorkerDeployment, + parseStoredProtocolV3SmokeRequestKey, + parseStoredProtocolV3SmokeSession, + parseStoredProtocolV3CutoverBookmark, + parseTimeTravelBookmark, + PROTOCOL_V3_COMMAND_FREEZE_SQL, + PROTOCOL_V3_CUTOVER_OBJECT_COUNT, + PROTOCOL_V3_CUTOVER_OBJECTS_SQL, + PROTOCOL_V3_CUTOVER_PROBE_SQL, + PROTOCOL_V3_CUTOVER_QUEUE_NAMES, + PROTOCOL_V3_LEGACY_TERMINAL_INTEGRITY_SQL, + PROTOCOL_V3_LEGACY_TERMINAL_SOURCE_INVENTORY_SQL, + PROTOCOL_V3_LOSSY_MIGRATION_INVENTORY_SQL, + PROTOCOL_V3_MIGRATION_INTENT_TABLE, + PROTOCOL_V3_POST_MIGRATION_CUTOVER_OBJECT_COUNT, + PROTOCOL_V3_POST_MIGRATION_CUTOVER_DRAIN_SQL, + PROTOCOL_V3_POST_MIGRATION_UNSAFE_SANDBOXES_SQL, + PROTOCOL_V3_RUNTIME_AUTHORITY_PREFLIGHT_SQL, + PROTOCOL_V3_SMOKE_REQUEST_KEY_SQL, + PROTOCOL_V3_SMOKE_SESSION_SQL, + protocolV3ContainerImageTag, + protocolV3RuntimeAuthorityPreflightSql, + protocolV3SmokeAgentSql, + protocolV3SmokeStatusSql, + recoverProtocolV3CutoverFailure, + REMOVE_PROTOCOL_V3_CUTOVER_SQL, + storeProtocolV3CutoverBookmarkSql, + storeProtocolV3RolloutSql, + storeProtocolV3SmokeRequestKeySql, + storeProtocolV3SmokeSessionSql, + updateAndVerifyProtocolV3QueueDelivery, +} from "../bin/protocol-v3-cutover"; +import { applyDrizzleMigration, applyDrizzleMigrationsThrough } from "./helpers/drizzle-migrations"; +import { SqliteD1Database } from "./helpers/sqlite-d1"; - expect(source).toContain( - 'run(["deploy", "--env", ENV, "--minify", "--containers-rollout", "immediate"]);', +const RELEASE_TREE_OID = "0123456789abcdef0123456789abcdef01234567"; +const CONTAINER_IMAGE_DIGEST = "a".repeat(64); +const INSTALL_PROTOCOL_V3_CUTOVER_SQL = installProtocolV3CutoverSql(RELEASE_TREE_OID); +const EMPTY_ROLLOUT = { + containerApplicationVersion: null, + containerImageDigest: null, + migrationStarted: false, + releaseTreeOid: RELEASE_TREE_OID, + workerVersionId: null, +} as const; +const DRAINED_CUTOVER_ROW = { + active_app_deployment_runs: 0, + active_runs: 0, + live_drivers: 0, + nonterminal_commands: 0, + nonterminal_api_commands: 0, + unsafe_environment_artifact_backup_staging: 0, + unsafe_sandbox_backups: 0, + unsafe_sandbox_backup_staging: 0, + unsafe_sandbox_sessions: 0, + unsafe_sandboxes: 0, + unsafe_sessions: 0, + unsettled_effects: 0, +} as const; +const INSERT_ENVIRONMENT_ARTIFACT_STAGING_SQL = ` + INSERT INTO environment_package_artifact_backup_staging ( + actual_backup_id, app_id, attempt_count, claim_owner, command_id, created_at, + delivery_generation, dir, input_digest, paths_json, updated_at + ) VALUES ( + NULL, 'app-1', 1, 'worker-1', 'artifact-command', 1, + 1, '/tmp/artifact', '${"0".repeat(64)}', + '{"executable":[],"node":[],"python":[]}', 1 + ) +`; + +function d1Json(row: Record): string { + return JSON.stringify([{ meta: {}, results: [row], success: true }]); +} + +function createCutoverGateDatabase(): SqliteD1Database { + const database = new SqliteD1Database(); + database.execute(` + CREATE TABLE d1_migrations (name text PRIMARY KEY); + CREATE TABLE session ( + id text PRIMARY KEY, + creator_account_id text NOT NULL, + end_user_id text, + status text NOT NULL, + status_operation_id text + ); + CREATE TABLE sandbox ( + id text PRIMARY KEY, + subject_kind text NOT NULL, + subject_id text NOT NULL, + status text NOT NULL, + status_operation_id text, + claim_owner text, + claim_expires_at integer + ); + CREATE TABLE sandbox_session ( + session_id text PRIMARY KEY, + sandbox_id text NOT NULL, + status text NOT NULL + ); + CREATE TABLE sandbox_backup ( + id text PRIMARY KEY, + status text NOT NULL, + error_message text + ); + CREATE TABLE session_run ( + id text PRIMARY KEY, + status text NOT NULL, + created_by_account_id text NOT NULL, + session_id text NOT NULL + ); + CREATE TABLE driver_instance ( + id text PRIMARY KEY, + status text NOT NULL, + sandbox_id text NOT NULL, + sandbox_session_id text NOT NULL + ); + CREATE TABLE driver_command ( + id text PRIMARY KEY, + driver_instance_id text NOT NULL, + kind text NOT NULL, + status text NOT NULL + ); + CREATE TABLE api_command ( + id text PRIMARY KEY, + status text NOT NULL, + claim_owner text, + claim_expires_at integer, + kind text NOT NULL, + created_at integer NOT NULL, + attempt_count integer NOT NULL DEFAULT 0, + payload_json text NOT NULL DEFAULT '{}' + ); + CREATE TABLE app_deployment_run ( + id text PRIMARY KEY, + status text NOT NULL + ); + CREATE TABLE external_tool_effect (id text PRIMARY KEY, status text NOT NULL); + `); + return database; +} + +function addPostRuntimeAuthoritySchema(database: SqliteD1Database): void { + database.execute(` + ALTER TABLE sandbox ADD operation_kind text; + ALTER TABLE session ADD archived_at integer; + ALTER TABLE session ADD cleanup_operation_kind text; + ALTER TABLE session ADD runtime_provisioning_operation_id text; + ALTER TABLE session ADD runtime_provisioning_run_id text; + ALTER TABLE session ADD runtime_provisioning_sandbox_id text; + ALTER TABLE session ADD runtime_provisioning_sandbox_session_id text; + ALTER TABLE session ADD runtime_provisioning_sandbox_incarnation integer; + ALTER TABLE session ADD runtime_provisioning_heartbeat_at integer; + ALTER TABLE api_command ADD delivery_generation integer NOT NULL DEFAULT 1; + CREATE TABLE sandbox_backup_staging ( + id text PRIMARY KEY, + sandbox_id text NOT NULL, + workspace_session_id text + ); + CREATE TABLE environment_package_artifact_backup_staging ( + actual_backup_id text, + app_id text NOT NULL, + attempt_count integer NOT NULL, + claim_owner text NOT NULL, + command_id text PRIMARY KEY, + created_at integer NOT NULL, + delivery_generation integer NOT NULL, + dir text NOT NULL, + input_digest text NOT NULL, + paths_json text NOT NULL, + updated_at integer NOT NULL + ); + `); +} + +async function readCutoverObjects(database: SqliteD1Database) { + const row = await database + .prepare(PROTOCOL_V3_CUTOVER_OBJECTS_SQL) + .first>(); + if (row === null) throw new Error("Cutover object inventory returned no row."); + return parseProtocolV3CutoverObjects(d1Json(row)); +} + +async function readCutoverState(database: SqliteD1Database) { + const row = await database + .prepare(PROTOCOL_V3_COMMAND_FREEZE_SQL) + .first>(); + if (row === null) throw new Error("Cutover state returned no row."); + return parseProtocolV3CutoverState(d1Json(row)); +} + +async function readLegacyTerminalIntegrity(database: SqliteD1Database) { + const row = await database + .prepare(PROTOCOL_V3_LEGACY_TERMINAL_INTEGRITY_SQL) + .first>(); + if (row === null) throw new Error("Legacy terminal integrity query returned no row."); + return parseProtocolV3LegacyTerminalIntegrity(d1Json(row)); +} + +async function readLegacyTerminalSourceInventory(database: SqliteD1Database) { + const row = await database + .prepare(PROTOCOL_V3_LEGACY_TERMINAL_SOURCE_INVENTORY_SQL) + .first>(); + if (row === null) throw new Error("Legacy terminal source inventory returned no row."); + return parseProtocolV3LegacyTerminalSourceInventory(d1Json(row)); +} + +function createLegacyTerminalDatabase(): SqliteD1Database { + const database = new SqliteD1Database(); + database.execute(` + CREATE TABLE session ( + id text PRIMARY KEY, + last_run_id text, + message_seq_cursor integer NOT NULL, + runtime_event_seq_cursor integer NOT NULL, + status text NOT NULL, + status_operation_id text + ); + CREATE TABLE session_run ( + completed_at integer, + error_code text, + error_details_json text, + error_message text, + id text PRIMARY KEY, + session_id text NOT NULL, + status text NOT NULL, + status_event text NOT NULL + ); + CREATE TABLE session_event ( + id text PRIMARY KEY, + event_type text NOT NULL, + run_id text, + seq integer NOT NULL, + session_id text NOT NULL, + source_event_id text NOT NULL + ); + CREATE TABLE session_message ( + id text PRIMARY KEY, + plan_json text, + role text NOT NULL, + seq integer NOT NULL, + segments_json text, + session_id text NOT NULL, + session_run_id text + ); + CREATE TABLE session_permission_request ( + id text PRIMARY KEY, + run_id text NOT NULL, + session_id text NOT NULL + ); + CREATE TABLE driver_command (id text PRIMARY KEY, status text NOT NULL); + CREATE TABLE external_tool_effect (id text PRIMARY KEY, status text NOT NULL); + `); + return database; +} + +function createValidLegacyCompletionDatabase(): SqliteD1Database { + const database = createLegacyTerminalDatabase(); + database.execute(` + INSERT INTO session VALUES ('session-1', 'completed-run', 1, 1, 'IDLE', NULL); + INSERT INTO session_run VALUES + (100, NULL, NULL, NULL, 'completed-run', 'session-1', 'completed', 'run.complete'); + INSERT INTO session_event VALUES + ('completed-event', 'run.completed', 'completed-run', 1, 'session-1', 'session-run-terminal:completed-run:run.completed'); + INSERT INTO session_message VALUES + ('assistant-1', '[]', 'assistant', 1, '[]', 'session-1', 'completed-run'); + `); + return database; +} + +function containerPage( + instances: readonly Record[], + pageToken: string | null, + nextPageToken: string | null, +): string { + return JSON.stringify({ + instances, + result_info: { next_page_token: nextPageToken, page_token: pageToken, per_page: 100 }, + }); +} + +function containerApplication(name: string, version: number): Record { + return { + configuration: { + image: `registry.cloudflare.com/account/${name}@sha256:${CONTAINER_IMAGE_DIGEST}`, + }, + health: { + instances: { active: 1, failed: 0, healthy: 1, scheduling: 0, starting: 0 }, + }, + id: crypto.randomUUID(), + name, + version, + }; +} + +function containerApplicationPage( + applications: readonly Record[], + pageToken: string | null, + nextPageToken: string | null, +): Record { + return { + result: applications, + result_info: { next_page_token: nextPageToken, page_token: pageToken, per_page: 100 }, + success: true, + }; +} + +interface PaginationFailureFixture { + readonly collect: (readPage: (pageToken: string | null) => T) => Promise; + readonly invalidMessage: string; + readonly invalidPage: T; + readonly label: string; + readonly page: (pageToken: string | null, nextPageToken: string | null) => T; +} + +function registerPaginationFailureTests(fixture: PaginationFailureFixture): void { + test(`fails closed on repeated ${fixture.label} page tokens`, async () => { + await expect(fixture.collect((pageToken) => fixture.page(pageToken, "page-2"))).rejects.toThrow( + "repeated page token", + ); + }); + + test(`propagates a later ${fixture.label} page failure`, async () => { + await expect( + fixture.collect((pageToken) => { + if (pageToken === null) return fixture.page(null, "page-2"); + throw new Error(`second ${fixture.label} page failed`); + }), + ).rejects.toThrow(`second ${fixture.label} page failed`); + }); + + test(`rejects an unexpected ${fixture.label} pagination shape`, async () => { + await expect(fixture.collect(() => fixture.invalidPage)).rejects.toThrow( + fixture.invalidMessage, + ); + }); + + test(`bounds ${fixture.label} pagination`, async () => { + let pagesRead = 0; + await expect( + fixture.collect((pageToken) => { + pagesRead += 1; + return fixture.page(pageToken, `page-${pagesRead}`); + }), + ).rejects.toThrow("exceeded its safety limit"); + expect(pagesRead).toBe(100); + }); +} + +describe("protocol v3 production cutover", () => { + test("closes new runtime admission while existing work drains", async () => { + const database = createCutoverGateDatabase(); + const smokeAccountId = "01J00000000000000000000001"; + const smokeSessionId = "01J0000000000000000000000A"; + const otherSessionId = "01J0000000000000000000000B"; + const smokeRequestKey = "protocol-v3-cutover-2de67417-f144-47e1-8b2e-1e71621a0d92"; + database.execute(` + INSERT INTO session VALUES + ('${smokeSessionId}', '${smokeAccountId}', '${smokeRequestKey}', 'IDLE', NULL), + ('${otherSessionId}', '${smokeAccountId}', 'ordinary-user', 'IDLE', NULL); + INSERT INTO sandbox VALUES + ('existing-sandbox', 'session', '${smokeSessionId}', 'active', NULL, 'worker-1', 100), + ('other-sandbox', 'session', '${otherSessionId}', 'cold', NULL, NULL, NULL); + INSERT INTO session_run VALUES + ('existing-run', 'queued', '${smokeAccountId}', '${smokeSessionId}'); + INSERT INTO app_deployment_run VALUES + ('existing-app-run', 'building'), + ('freeze-app-run', 'preparing'); + INSERT INTO driver_instance VALUES + ('existing-driver', 'ready', 'existing-sandbox', '${smokeSessionId}'), + ('other-existing-driver', 'stopped', 'other-sandbox', '${otherSessionId}'); + INSERT INTO api_command ( + id, status, claim_owner, claim_expires_at, kind, created_at + ) VALUES ( + 'existing-api-command', 'running', 'worker-1', 100, + 'environment_package_artifact_build', 0 + ); + `); + database.execute(INSTALL_PROTOCOL_V3_CUTOVER_SQL); + + for (const kind of [ + "session_run_dispatch", + "app_deployment_run_dispatch", + "environment_package_artifact_build", + ]) { + for (let request = 0; request < 3; request += 1) { + await expect( + database + .prepare( + `INSERT INTO api_command ( + id, status, claim_owner, claim_expires_at, kind, created_at + ) VALUES (?, 'queued', NULL, NULL, ?, 1)`, + ) + .bind(`blocked-${kind}-${request}`, kind) + .run(), + ).rejects.toThrow("blocks new nonterminal API commands"); + } + } + for (const kind of [ + "app_deployment_script_reconciliation", + "cost_ledger_reconciliation", + "sandbox_backup_reconciliation", + "scheduled_maintenance", + ]) { + await expect( + database + .prepare( + `INSERT INTO api_command ( + id, status, claim_owner, claim_expires_at, kind, created_at + ) VALUES (?, 'queued', NULL, NULL, ?, 1)`, + ) + .bind(`drain-${kind}`, kind) + .run(), + ).resolves.toBeDefined(); + } + database.execute(` + INSERT INTO api_command ( + id, status, claim_owner, claim_expires_at, kind, created_at + ) VALUES ( + 'terminal-business-command', 'failed', NULL, NULL, + 'environment_package_artifact_build', 0 + ) + `); + await expect( + database + .prepare("UPDATE api_command SET status = 'queued' WHERE id = 'terminal-business-command'") + .run(), + ).rejects.toThrow("blocks API command admission"); + await expect( + database + .prepare( + `UPDATE api_command + SET kind = 'session_run_dispatch' + WHERE id = 'drain-scheduled_maintenance'`, + ) + .run(), + ).rejects.toThrow("blocks API command admission"); + + await expect( + database.prepare("UPDATE session_run SET status = 'running' WHERE id = 'existing-run'").run(), + ).resolves.toBeDefined(); + await expect( + database + .prepare("UPDATE session_run SET status = 'completed' WHERE id = 'existing-run'") + .run(), + ).resolves.toBeDefined(); + await expect( + database.prepare("UPDATE session_run SET status = 'queued' WHERE id = 'existing-run'").run(), + ).rejects.toThrow("blocks Session Run reactivation"); + await expect( + database + .prepare( + `INSERT INTO session_run VALUES ('new-run', 'queued', '${smokeAccountId}', '${smokeSessionId}')`, + ) + .run(), + ).rejects.toThrow("blocks new active Session Runs"); + await expect( + database + .prepare( + "UPDATE app_deployment_run SET status = 'submitting' WHERE id = 'existing-app-run'", + ) + .run(), + ).resolves.toBeDefined(); + await expect( + database + .prepare("UPDATE app_deployment_run SET status = 'success' WHERE id = 'existing-app-run'") + .run(), + ).resolves.toBeDefined(); + await expect( + database + .prepare( + "UPDATE app_deployment_run SET status = 'activating' WHERE id = 'existing-app-run'", + ) + .run(), + ).rejects.toThrow("blocks App deployment Run reactivation"); + await expect( + database.prepare("INSERT INTO app_deployment_run VALUES ('new-app-run', 'queued')").run(), + ).rejects.toThrow("blocks new active App deployment Runs"); + + await expect( + database + .prepare("UPDATE driver_instance SET status = 'stopping' WHERE id = 'existing-driver'") + .run(), + ).resolves.toBeDefined(); + await expect( + database + .prepare("UPDATE driver_instance SET status = 'stopped' WHERE id = 'existing-driver'") + .run(), + ).resolves.toBeDefined(); + await expect( + database + .prepare("UPDATE driver_instance SET status = 'ready' WHERE id = 'existing-driver'") + .run(), + ).rejects.toThrow("blocks Driver reactivation"); + await expect( + database + .prepare( + `INSERT INTO driver_instance VALUES ('new-driver', 'provisioning', 'existing-sandbox', '${smokeSessionId}')`, + ) + .run(), + ).rejects.toThrow("blocks new live Driver instances"); + + for (const kind of ["input.start", "mcp.execute"]) { + await expect( + database + .prepare("INSERT INTO driver_command VALUES (?, 'existing-driver', ?, 'queued')") + .bind(kind, kind) + .run(), + ).rejects.toThrow("blocks new Driver commands"); + } + for (const kind of ["permission.resolve", "session.stop", "turn.cancel"]) { + await expect( + database + .prepare("INSERT INTO driver_command VALUES (?, 'existing-driver', ?, 'queued')") + .bind(kind, kind) + .run(), + ).resolves.toBeDefined(); + } + + await expect( + database + .prepare( + "UPDATE sandbox SET status = 'destroying', claim_owner = NULL, claim_expires_at = NULL WHERE id = 'existing-sandbox'", + ) + .run(), + ).resolves.toBeDefined(); + await expect( + database.prepare("UPDATE sandbox SET status = 'cold' WHERE id = 'existing-sandbox'").run(), + ).resolves.toBeDefined(); + await expect( + database.prepare("UPDATE sandbox SET status = 'active' WHERE id = 'existing-sandbox'").run(), + ).rejects.toThrow("blocks sandbox activation"); + await expect( + database + .prepare("INSERT INTO sandbox_backup VALUES ('staging-backup', 'creating', NULL)") + .run(), + ).rejects.toThrow("blocks new sandbox backup work"); + + database.execute(ENABLE_PROTOCOL_V3_COMMAND_FREEZE_SQL); + const stillDraining = await database + .prepare(PROTOCOL_V3_COMMAND_FREEZE_SQL) + .first>(); + expect(parseProtocolV3CommandFreeze(d1Json(stillDraining))).toBeFalse(); + await expect( + database + .prepare( + "UPDATE api_command SET status = 'succeeded', claim_owner = NULL, claim_expires_at = NULL WHERE id = 'existing-api-command'", + ) + .run(), + ).resolves.toBeDefined(); + database.execute(` + UPDATE api_command + SET status = 'succeeded' + WHERE id LIKE 'drain-%' + `); + + database.execute(ENABLE_PROTOCOL_V3_COMMAND_FREEZE_SQL); + const appRunStillActive = await database + .prepare(PROTOCOL_V3_COMMAND_FREEZE_SQL) + .first>(); + expect(parseProtocolV3CommandFreeze(d1Json(appRunStillActive))).toBeFalse(); + database.execute("UPDATE app_deployment_run SET status = 'failed' WHERE id = 'freeze-app-run'"); + + database.execute(ENABLE_PROTOCOL_V3_COMMAND_FREEZE_SQL); + const frozen = await database + .prepare(PROTOCOL_V3_COMMAND_FREEZE_SQL) + .first>(); + expect(parseProtocolV3CommandFreeze(d1Json(frozen))).toBeTrue(); + await expect( + database + .prepare( + "INSERT INTO driver_command VALUES ('frozen-control', 'existing-driver', 'session.stop', 'queued')", + ) + .run(), + ).rejects.toThrow("blocks new Driver commands"); + + await expect( + database + .prepare("UPDATE api_command SET status = 'queued' WHERE id = 'existing-api-command'") + .run(), + ).rejects.toThrow("blocks API command admission"); + await expect( + database + .prepare( + "UPDATE api_command SET status = 'queued' WHERE id = 'drain-scheduled_maintenance'", + ) + .run(), + ).rejects.toThrow("blocks API command admission"); + for (const status of ["queued", "running"]) { + await expect( + database + .prepare( + `INSERT INTO api_command ( + id, status, claim_owner, claim_expires_at, kind, created_at + ) VALUES (?, ?, NULL, NULL, 'scheduled_maintenance', 1)`, + ) + .bind(`frozen-${status}`, status) + .run(), + ).rejects.toThrow("blocks new nonterminal API commands"); + } + + database.execute(openProtocolV3SmokeWindowSql(smokeAccountId)); + await expect( + database + .prepare( + `INSERT INTO session_run VALUES ('smoke-run-before-request', 'queued', '${smokeAccountId}', '${smokeSessionId}')`, + ) + .run(), + ).rejects.toThrow("blocks new active Session Runs"); + database.execute(storeProtocolV3SmokeRequestKeySql(smokeRequestKey)); + await expect( + database + .prepare( + `INSERT INTO session_run VALUES ('smoke-run', 'queued', '${smokeAccountId}', '${smokeSessionId}')`, + ) + .run(), + ).resolves.toBeDefined(); + await expect( + database + .prepare( + `INSERT INTO session_run VALUES ('same-account-run', 'queued', '${smokeAccountId}', '${otherSessionId}')`, + ) + .run(), + ).rejects.toThrow("blocks new active Session Runs"); + await expect( + database + .prepare( + `INSERT INTO driver_instance VALUES ('smoke-driver', 'provisioning', 'existing-sandbox', '${smokeSessionId}')`, + ) + .run(), + ).resolves.toBeDefined(); + await expect( + database + .prepare( + `INSERT INTO driver_instance VALUES ('other-driver', 'provisioning', 'other-sandbox', '${otherSessionId}')`, + ) + .run(), + ).rejects.toThrow("blocks new live Driver instances"); + await expect( + database.prepare("UPDATE sandbox SET status = 'active' WHERE id = 'existing-sandbox'").run(), + ).resolves.toBeDefined(); + await expect( + database.prepare("UPDATE sandbox SET status = 'active' WHERE id = 'other-sandbox'").run(), + ).rejects.toThrow("blocks sandbox activation"); + await expect( + database + .prepare( + `INSERT INTO sandbox_session VALUES ('${smokeSessionId}', 'existing-sandbox', 'active')`, + ) + .run(), + ).resolves.toBeDefined(); + await expect( + database + .prepare( + `INSERT INTO sandbox_session VALUES ('${otherSessionId}', 'other-sandbox', 'active')`, + ) + .run(), + ).rejects.toThrow("blocks new active sandbox Sessions"); + await expect( + database + .prepare( + `UPDATE session SET status_operation_id = 'smoke-operation' WHERE id = '${smokeSessionId}'`, + ) + .run(), + ).resolves.toBeDefined(); + await expect( + database + .prepare( + `UPDATE session SET status_operation_id = 'ordinary-operation' WHERE id = '${otherSessionId}'`, + ) + .run(), + ).rejects.toThrow("blocks Session operation acquisition"); + await expect( + database + .prepare( + "INSERT INTO driver_command VALUES ('smoke-input', 'smoke-driver', 'input.start', 'queued')", + ) + .run(), + ).rejects.toThrow("blocks new Driver commands"); + await expect( + database + .prepare( + "INSERT INTO driver_command VALUES ('smoke-stop', 'smoke-driver', 'session.stop', 'queued')", + ) + .run(), + ).resolves.toBeDefined(); + await expect( + database + .prepare( + "INSERT INTO driver_command VALUES ('other-stop', 'other-existing-driver', 'session.stop', 'queued')", + ) + .run(), + ).rejects.toThrow("blocks new Driver commands"); + + database.execute(CLOSE_PROTOCOL_V3_SMOKE_WINDOW_SQL); + await expect( + database + .prepare( + "INSERT INTO driver_command VALUES ('closed-again', 'existing-driver', 'session.stop', 'queued')", + ) + .run(), + ).rejects.toThrow("blocks new Driver commands"); + + database.execute(REMOVE_PROTOCOL_V3_CUTOVER_SQL); + await expect( + database + .prepare( + `INSERT INTO session_run VALUES ('after-cutover', 'queued', '${smokeAccountId}', '${smokeSessionId}')`, + ) + .run(), + ).resolves.toBeDefined(); + }); + + test("parses the fail-closed migration and drain probes", () => { + expect( + parseProtocolV3CutoverProbe( + `[wrangler warning]\n${d1Json({ + gate_present: 1, + })}`, + ), + ).toEqual({ gatePresent: true }); + expect(() => parseProtocolV3CutoverProbe(d1Json({ gate_present: 2 }))).toThrow("zero or one"); + + const localMigrations = [ + "0013_durable-mcp-effect-v3.sql", + "0014_session-event-stream-identity.sql", + "0019_runtime-subject-operation-authority.sql", + ]; + expect(() => assertCutoverMigrationJournalAudited(AUDITED_MIGRATION_NAMES)).not.toThrow(); + expect(() => + assertCutoverMigrationJournalAudited([...AUDITED_MIGRATION_NAMES, "0021_unreviewed.sql"]), + ).toThrow("audited only through 0020_sandbox-backup-object-authority.sql"); + expect(() => + assertCutoverMigrationJournalAudited([ + ...AUDITED_MIGRATION_NAMES.slice(0, -1), + "0018_z_unreviewed.sql", + AUDITED_MIGRATION_NAMES.at(-1) ?? "", + ]), + ).toThrow("audited only through 0020_sandbox-backup-object-authority.sql"); + expect(() => + assertCutoverMigrationJournalAudited(AUDITED_MIGRATION_NAMES.with(18, "0018_replaced.sql")), + ).toThrow("audited only through 0020_sandbox-backup-object-authority.sql"); + const remoteLedger = JSON.stringify([ + { + results: localMigrations.slice(0, 2).map((name) => ({ name })), + success: true, + }, + ]); + const pending = findPendingProdMigrations(remoteLedger, localMigrations); + expect(pending).toEqual(["0019_runtime-subject-operation-authority.sql"]); + expect( + findPendingProdMigrations( + JSON.stringify([ + { + results: [{ name: localMigrations[1] }, { name: localMigrations[0] }], + success: true, + }, + ]), + localMigrations, + ), + ).toEqual(["0019_runtime-subject-operation-authority.sql"]); + expect(() => + findPendingProdMigrations( + JSON.stringify([ + { + results: [{ name: localMigrations[0] }, { name: localMigrations[2] }], + success: true, + }, + ]), + localMigrations, + ), + ).toThrow("not an exact prefix"); + const blocked = parseProtocolV3CutoverDrain( + d1Json({ + ...DRAINED_CUTOVER_ROW, + nonterminal_commands: 1, + }), + ); + expect(isProtocolV3RuntimeDrained(blocked)).toBeTrue(); + expect(isProtocolV3CutoverDrained(blocked)).toBeFalse(); + const claimedEffect = parseProtocolV3CutoverDrain( + d1Json({ + ...DRAINED_CUTOVER_ROW, + unsettled_effects: 1, + }), + ); + expect(isProtocolV3RuntimeDrained(claimedEffect)).toBeFalse(); + expect(isProtocolV3CutoverDrained(claimedEffect)).toBeFalse(); + for (const blockedAuthority of [ + { active_app_deployment_runs: 1 }, + { nonterminal_api_commands: 1 }, + { unsafe_environment_artifact_backup_staging: 1 }, + ]) { + const state = parseProtocolV3CutoverDrain( + d1Json({ ...DRAINED_CUTOVER_ROW, ...blockedAuthority }), + ); + expect(isProtocolV3RuntimeDrained(state)).toBeFalse(); + expect(isProtocolV3CutoverDrained(state)).toBeFalse(); + } + for (const activeRuntime of [ + { active_runs: 1, live_drivers: 0 }, + { active_runs: 0, live_drivers: 1 }, + ]) { + const state = parseProtocolV3CutoverDrain( + d1Json({ + ...DRAINED_CUTOVER_ROW, + ...activeRuntime, + }), + ); + expect(isProtocolV3RuntimeDrained(state)).toBeFalse(); + expect(isProtocolV3CutoverDrained(state)).toBeFalse(); + } + expect( + isProtocolV3CutoverDrained( + parseProtocolV3CutoverDrain( + d1Json({ + ...DRAINED_CUTOVER_ROW, + }), + ), + ), + ).toBeTrue(); + }); + + test("bounds the read-only migration 0013 loss inventory and fails on any candidate", async () => { + const database = new SqliteD1Database(); + applyDrizzleMigrationsThrough(database, "0012_agent-task-snapshot-state"); + const row = await database + .prepare(PROTOCOL_V3_LOSSY_MIGRATION_INVENTORY_SQL) + .first>(); + const empty = parseProtocolV3LossyMigrationInventory(d1Json(row ?? {})); + expect(empty.totalCandidates).toBe(0); + expect(empty.candidateIds).toEqual([]); + expect(() => assertProtocolV3LossyMigrationInventory(empty)).not.toThrow(); + + const candidateIds = Array.from({ length: 50 }, (_, index) => [ + "mcp_argument_omission", + `01${"0".repeat(22)}${String(index).padStart(2, "0")}`, + ]); + const blockedRow = { + attempt_completion_time_fabrications: 0, + candidate_ids_json: JSON.stringify(candidateIds), + command_error_omissions: 0, + command_payload_conflicts: 0, + control_reason_omissions: 0, + input_start_result_omissions: 0, + input_text_omissions: 0, + mcp_argument_omissions: 51, + mcp_command_terminal_conflicts: 0, + mcp_result_conflicts: 0, + mcp_result_omissions: 0, + orphan_effects: 0, + provider_receipt_losses: 0, + permission_payload_rewrites: 0, + session_run_error_omissions: 0, + total_candidates: 51, + }; + const blocked = parseProtocolV3LossyMigrationInventory(d1Json(blockedRow)); + expect(blocked.candidateIds).toHaveLength(50); + expect(() => assertProtocolV3LossyMigrationInventory(blocked)).toThrow( + "No lossy migration candidates are authorized", + ); + expect(() => + parseProtocolV3LossyMigrationInventory( + d1Json({ + ...blockedRow, + candidate_ids_json: JSON.stringify([ + ...candidateIds, + ["mcp_argument_omission", "01000000000000000000000050"], + ]), + }), + ), + ).toThrow("candidate ID count is inconsistent"); + }); + + test("runs the migration 0013 loss preflight before migrations and keeps a direct guard", async () => { + const deploySource = await Bun.file(new URL("../bin/deploy-prod.ts", import.meta.url)).text(); + const cutoverSource = deploySource.slice( + deploySource.indexOf("async function runProtocolV3Cutover"), + ); + expect(cutoverSource.indexOf("verifyProdLossyMigrationInventory();")).toBeGreaterThanOrEqual(0); + expect(cutoverSource.indexOf("verifyProdLossyMigrationInventory();")).toBeLessThan( + cutoverSource.indexOf("applyD1Migrations();"), + ); + + const migrationSource = await Bun.file( + new URL("../../../pkgs/db/drizzle/0013_durable-mcp-effect-v3.sql", import.meta.url), + ).text(); + const lossGuard = migrationSource.indexOf("__durable_mcp_v3_loss_guard"); + expect(lossGuard).toBeGreaterThanOrEqual(0); + expect(lossGuard).toBeLessThan(migrationSource.indexOf("ALTER TABLE `driver_command`")); + expect(lossGuard).toBeLessThan(migrationSource.indexOf("UPDATE `driver_command`")); + }); + + test("rebuilds the exact post-migration gate before old Workers can reacquire work", async () => { + const database = createCutoverGateDatabase(); + const accountId = "01J00000000000000000000001"; + const smokeSessionId = "01J0000000000000000000000A"; + const otherSessionId = "01J0000000000000000000000B"; + const requestKey = "protocol-v3-cutover-2de67417-f144-47e1-8b2e-1e71621a0d92"; + database.execute(` + INSERT INTO session VALUES + ('${smokeSessionId}', '${accountId}', '${requestKey}', 'IDLE', NULL), + ('${otherSessionId}', '${accountId}', 'ordinary-user', 'IDLE', NULL); + INSERT INTO sandbox VALUES + ('smoke-sandbox', 'session', '${smokeSessionId}', 'cold', NULL, NULL, NULL), + ('other-sandbox', 'session', '${otherSessionId}', 'cold', NULL, NULL, NULL); + INSERT INTO api_command ( + id, status, claim_owner, claim_expires_at, kind, created_at, + attempt_count, payload_json + ) VALUES ( + 'artifact-command', 'running', 'worker-1', 9000000000000000, + 'environment_package_artifact_build', 0, 1, + '{"appId":"app-1","inputDigest":"${"0".repeat(64)}"}' + ); + `); + database.execute(INSTALL_PROTOCOL_V3_CUTOVER_SQL); + addPostRuntimeAuthoritySchema(database); + database.execute(INSTALL_PROTOCOL_V3_POST_MIGRATION_CUTOVER_SQL); + expect(await readCutoverObjects(database)).toEqual({ + exactObjectCount: PROTOCOL_V3_POST_MIGRATION_CUTOVER_OBJECT_COUNT, + objectCount: PROTOCOL_V3_POST_MIGRATION_CUTOVER_OBJECT_COUNT, + }); + database.execute( + `INSERT INTO api_command ( + id, status, claim_owner, claim_expires_at, kind, created_at, + attempt_count, payload_json, delivery_generation + ) VALUES ( + 'terminal-command', 'succeeded', NULL, NULL, 'scheduled_maintenance', 0, + 0, '{}', 1 + )`, + ); + + const gate = await database + .prepare('SELECT started_at AS startedAt FROM "__protocol_v3_cutover" WHERE id = 1') + .first<{ startedAt: number }>(); + if (gate === null) throw new Error("Expected the protocol v3 cutover gate."); + database.execute( + `UPDATE api_command SET created_at = ${gate.startedAt + 1} + WHERE id = 'artifact-command'`, + ); + await expect(database.prepare(INSERT_ENVIRONMENT_ARTIFACT_STAGING_SQL).run()).rejects.toThrow( + "blocks new environment artifact backup staging", + ); + database.execute("UPDATE api_command SET created_at = 0 WHERE id = 'artifact-command'"); + await expect( + database + .prepare(INSERT_ENVIRONMENT_ARTIFACT_STAGING_SQL.replace("'worker-1'", "'wrong-owner'")) + .run(), + ).rejects.toThrow("blocks new environment artifact backup staging"); + await expect( + database.prepare(INSERT_ENVIRONMENT_ARTIFACT_STAGING_SQL).run(), + ).resolves.toBeDefined(); + database.execute(` + DELETE FROM environment_package_artifact_backup_staging + WHERE command_id = 'artifact-command'; + UPDATE api_command + SET status = 'succeeded', claim_owner = NULL, claim_expires_at = NULL + WHERE id = 'artifact-command'; + `); + database.execute(ENABLE_PROTOCOL_V3_COMMAND_FREEZE_SQL); + await expect( + database + .prepare("UPDATE api_command SET delivery_generation = 2 WHERE id = 'terminal-command'") + .run(), + ).rejects.toThrow("blocks API command admission"); + await expect(database.prepare(INSERT_ENVIRONMENT_ARTIFACT_STAGING_SQL).run()).rejects.toThrow( + "blocks new environment artifact backup staging", + ); + + await expect( + database + .prepare( + `UPDATE session SET runtime_provisioning_operation_id = 'ordinary-operation' WHERE id = '${otherSessionId}'`, + ) + .run(), + ).rejects.toThrow("blocks Session operation acquisition"); + await expect( + database + .prepare( + `UPDATE session SET archived_at = 1, cleanup_operation_kind = 'archive' WHERE id = '${otherSessionId}'`, + ) + .run(), + ).resolves.toBeDefined(); + await expect( + database + .prepare( + `UPDATE session SET cleanup_operation_kind = 'delete' WHERE id = '${otherSessionId}'`, + ) + .run(), + ).rejects.toThrow("blocks Session operation acquisition"); + await expect( + database + .prepare("UPDATE sandbox SET operation_kind = 'activate' WHERE id = 'other-sandbox'") + .run(), + ).rejects.toThrow("blocks sandbox activation"); + await expect( + database + .prepare( + `INSERT INTO sandbox_backup_staging VALUES ('ordinary-staging', 'other-sandbox', '${otherSessionId}')`, + ) + .run(), + ).rejects.toThrow("blocks new sandbox backup staging"); + + database.execute(openProtocolV3SmokeWindowSql(accountId)); + database.execute(storeProtocolV3SmokeRequestKeySql(requestKey)); + await expect( + database + .prepare( + `UPDATE session SET runtime_provisioning_operation_id = 'smoke-operation' WHERE id = '${smokeSessionId}'`, + ) + .run(), + ).resolves.toBeDefined(); + await expect( + database + .prepare("UPDATE sandbox SET operation_kind = 'activate' WHERE id = 'smoke-sandbox'") + .run(), + ).resolves.toBeDefined(); + await expect( + database + .prepare( + `INSERT INTO sandbox_backup_staging VALUES ('smoke-staging', 'smoke-sandbox', '${smokeSessionId}')`, + ) + .run(), + ).resolves.toBeDefined(); + + database.execute(REMOVE_PROTOCOL_V3_CUTOVER_SQL); + database.execute(INSTALL_PROTOCOL_V3_POST_MIGRATION_CUTOVER_SQL); + expect( + await database + .prepare('SELECT count(*) AS count FROM "__protocol_v3_cutover"') + .first<{ count: number }>(), + ).toEqual({ count: 0 }); + expect(await readCutoverObjects(database)).toEqual({ + exactObjectCount: PROTOCOL_V3_POST_MIGRATION_CUTOVER_OBJECT_COUNT, + objectCount: PROTOCOL_V3_POST_MIGRATION_CUTOVER_OBJECT_COUNT, + }); + database.execute(installProtocolV3PostMigrationCutoverSql(RELEASE_TREE_OID)); + expect(await readCutoverObjects(database)).toEqual({ + exactObjectCount: PROTOCOL_V3_POST_MIGRATION_CUTOVER_OBJECT_COUNT, + objectCount: PROTOCOL_V3_POST_MIGRATION_CUTOVER_OBJECT_COUNT, + }); + }); + + test("migration 0020 preserves the exact preinstalled cutover gate", async () => { + const database = new SqliteD1Database(); + applyDrizzleMigrationsThrough(database, "0019_runtime-subject-operation-authority"); + database.execute(INSTALL_PROTOCOL_V3_CUTOVER_SQL); + + applyDrizzleMigration(database, "0020_sandbox-backup-object-authority"); + + expect(await readCutoverObjects(database)).toEqual({ + exactObjectCount: PROTOCOL_V3_POST_MIGRATION_CUTOVER_OBJECT_COUNT, + objectCount: PROTOCOL_V3_POST_MIGRATION_CUTOVER_OBJECT_COUNT, + }); + }); + + test("reports legacy identity and backup rows that migration 0019 will reject atomically", async () => { + const database = new SqliteD1Database(); + database.execute(` + CREATE TABLE app (id text PRIMARY KEY); + CREATE TABLE agent ( + id text PRIMARY KEY, + app_id text NOT NULL, + kind text NOT NULL, + owner_account_id text NOT NULL + ); + CREATE TABLE session ( + id text PRIMARY KEY, + agent_id text NOT NULL, + app_id text NOT NULL, + kind text NOT NULL, + archived_at integer, + cleanup_operation_kind text, + runtime_provisioning_heartbeat_at integer, + runtime_provisioning_operation_id text, + runtime_provisioning_run_id text, + runtime_provisioning_sandbox_id text, + status text NOT NULL, + status_operation_id text + ); + CREATE TABLE session_run ( + id text PRIMARY KEY, + session_id text NOT NULL, + agent_id text NOT NULL, + status text NOT NULL + ); + CREATE TABLE sandbox_session ( + session_id text NOT NULL, + sandbox_id text NOT NULL, + cwd text NOT NULL, + status text NOT NULL + ); + CREATE TABLE sandbox ( + id text PRIMARY KEY, + kind text NOT NULL, + subject_kind text NOT NULL, + subject_id text NOT NULL, + agent_id text, + app_id text, + owner_account_id text, + last_backup_id text, + last_restore_backup_id text + ); + CREATE TABLE driver_instance (id text PRIMARY KEY, generation); + CREATE TABLE app_deployment (id text PRIMARY KEY, last_successful_url text); + CREATE TABLE preflight_parent (id text PRIMARY KEY); + CREATE TABLE preflight_child ( + id text PRIMARY KEY, + parent_id text REFERENCES preflight_parent(id) + ); + CREATE TABLE sandbox_backup ( + id text PRIMARY KEY, + sandbox_id text NOT NULL, + dir text NOT NULL, + session_run_id text, + status text NOT NULL, + error_message text, + keep integer NOT NULL, + ttl_seconds integer NOT NULL, + created_at integer NOT NULL, + updated_at integer NOT NULL + ); + INSERT INTO app VALUES ('app-1'); + INSERT INTO agent VALUES ('agent-1', 'app-1', 'cattle', 'account-1'); + INSERT INTO session ( + id, agent_id, app_id, kind, status + ) VALUES ('session-1', 'agent-1', 'app-1', 'cattle', 'IDLE'); + INSERT INTO session_run VALUES ('run-1', 'session-1', 'agent-1', 'completed'); + INSERT INTO sandbox_session VALUES ('session-1', 'sandbox-1', '/workspace', 'closed'); + INSERT INTO sandbox ( + id, kind, subject_kind, subject_id, agent_id, app_id, owner_account_id + ) VALUES ('sandbox-1', 'cattle', 'session', 'session-1', NULL, NULL, NULL); + INSERT INTO sandbox_backup VALUES ( + 'backup-1', 'sandbox-1', '/workspace', 'run-1', 'ready', NULL, 0, 3600, 1, 2 + ); + `); + const readPreflight = async () => + parseProtocolV3RuntimeAuthorityPreflight( + d1Json( + await database + .prepare(PROTOCOL_V3_RUNTIME_AUTHORITY_PREFLIGHT_SQL) + .first>(), + ), + ); + const valid = await readPreflight(); + expect(() => assertProtocolV3RuntimeAuthorityPreflight(valid)).not.toThrow(); + + database.execute("INSERT INTO app_deployment VALUES ('deployment-1', 'https://legacy.test')"); + expect((await readPreflight()).legacyAppDeploymentTraffic).toBe(1); + database.execute("DELETE FROM app_deployment"); + + database.execute("INSERT INTO driver_instance VALUES ('driver-1', 1.5)"); + expect((await readPreflight()).invalidDriverGenerations).toBe(1); + database.execute("DELETE FROM driver_instance"); + + database.execute("UPDATE sandbox SET last_backup_id = 'missing' WHERE id = 'sandbox-1'"); + expect((await readPreflight()).invalidSandboxBackupPointers).toBe(1); + database.execute("UPDATE sandbox SET last_backup_id = NULL WHERE id = 'sandbox-1'"); + + database.execute( + "UPDATE session SET runtime_provisioning_operation_id = 'operation-1' WHERE id = 'session-1'", + ); + expect((await readPreflight()).nonstaticSessions).toBe(1); + database.execute( + "UPDATE session SET runtime_provisioning_operation_id = NULL WHERE id = 'session-1'", + ); + + database.execute("UPDATE sandbox_session SET sandbox_id = 'other-sandbox'"); + expect((await readPreflight()).invalidSandboxSessionAuthorities).toBe(1); + database.execute("UPDATE sandbox_session SET sandbox_id = 'sandbox-1'"); + + database.execute(` + PRAGMA foreign_keys = OFF; + INSERT INTO preflight_child VALUES ('child-1', 'missing-parent'); + PRAGMA foreign_keys = ON; + `); + expect((await readPreflight()).foreignKeyViolations).toBe(1); + database.execute(` + PRAGMA foreign_keys = OFF; + DELETE FROM preflight_child; + PRAGMA foreign_keys = ON; + `); + + for (const [breakAuthority, restoreAuthority] of [ + [ + "UPDATE session_run SET status = 'failed' WHERE id = 'run-1'", + "UPDATE session_run SET status = 'completed' WHERE id = 'run-1'", + ], + [ + "UPDATE session_run SET agent_id = 'other-agent' WHERE id = 'run-1'", + "UPDATE session_run SET agent_id = 'agent-1' WHERE id = 'run-1'", + ], + [ + "UPDATE sandbox_session SET sandbox_id = 'other-sandbox'", + "UPDATE sandbox_session SET sandbox_id = 'sandbox-1'", + ], + [ + "UPDATE sandbox_session SET cwd = '/other'", + "UPDATE sandbox_session SET cwd = '/workspace'", + ], + [ + "UPDATE sandbox_backup SET dir = '' WHERE id = 'backup-1'", + "UPDATE sandbox_backup SET dir = '/workspace' WHERE id = 'backup-1'", + ], + ] as const) { + database.execute(breakAuthority); + expect((await readPreflight()).invalidSandboxBackups).toBe(1); + database.execute(restoreAuthority); + } + + database.execute("UPDATE sandbox SET agent_id = 'agent-1' WHERE id = 'sandbox-1'"); + expect((await readPreflight()).invalidSandboxIdentities).toBe(1); + database.execute( + "UPDATE sandbox SET agent_id = NULL, app_id = NULL, owner_account_id = NULL WHERE id = 'sandbox-1'", + ); + database.execute(` + INSERT INTO sandbox_backup VALUES ( + 'backup-2', 'sandbox-1', '/workspace', 'run-1', 'pruned', NULL, 1, 3600, 2, 3 + ); + `); + const invalid = await readPreflight(); + expect(invalid.duplicateSandboxBackups).toBe(1); + expect(() => assertProtocolV3RuntimeAuthorityPreflight(invalid)).toThrow( + "Migration 0019 remains the atomic authority", + ); + }); + + test("preflights the real 0012 schema without reading 0015 Session columns", async () => { + const database = new SqliteD1Database(); + applyDrizzleMigrationsThrough(database, "0012_agent-task-snapshot-state"); + const readPreflight = async () => + parseProtocolV3RuntimeAuthorityPreflight( + d1Json( + await database + .prepare(protocolV3RuntimeAuthorityPreflightSql(true)) + .first>(), + ), + ); + + const clean = await readPreflight(); + expect(() => assertProtocolV3RuntimeAuthorityPreflight(clean)).not.toThrow(); + database.execute(` + INSERT INTO session ( + agent_id, created_at, creator_account_id, id, kind, model, app_id, + provider, renamed, runtime_id, status, updated_at + ) VALUES ( + '01J0000000000000000000001A', 1, '01J0000000000000000000001B', + '01J0000000000000000000001C', 'cattle', 'model', + '01J0000000000000000000001D', 'provider', 0, 'codex', 'RUNNING', 1 + ) + `); + expect((await readPreflight()).nonstaticSessions).toBe(1); + }); + + test("checks 0015 Session authority columns once migration 0015 is applied", async () => { + const database = new SqliteD1Database(); + applyDrizzleMigrationsThrough(database, "0015_session-cleanup-operation"); + database.execute(` + INSERT INTO session ( + agent_id, created_at, creator_account_id, id, kind, model, app_id, + provider, renamed, runtime_id, runtime_provisioning_heartbeat_at, + runtime_provisioning_operation_id, runtime_provisioning_sandbox_id, + status, updated_at + ) VALUES ( + '01J0000000000000000000001A', 1, '01J0000000000000000000001B', + '01J0000000000000000000001C', 'cattle', 'model', + '01J0000000000000000000001D', 'provider', 0, 'codex', 1, + '01J0000000000000000000001E', '01J0000000000000000000001F', + 'IDLE', 1 + ) + `); + const preflight = parseProtocolV3RuntimeAuthorityPreflight( + d1Json( + await database + .prepare(protocolV3RuntimeAuthorityPreflightSql(false)) + .first>(), + ), + ); + + expect(preflight.nonstaticSessions).toBe(1); + expect(() => assertProtocolV3RuntimeAuthorityPreflight(preflight)).toThrow( + "nonstaticSessions=1", + ); + }); + + test("counts every post-migration authority lease before declaring the boundary drained", async () => { + const database = createCutoverGateDatabase(); + addPostRuntimeAuthoritySchema(database); + database.execute(` + INSERT INTO session VALUES ( + 'session-1', 'account-1', 'user-1', 'IDLE', NULL, + NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL + ); + INSERT INTO sandbox VALUES ( + 'sandbox-1', 'session', 'session-1', 'cold', NULL, NULL, NULL, NULL + ); + `); + const readDrain = async () => + parseProtocolV3CutoverDrain( + d1Json( + await database + .prepare(PROTOCOL_V3_POST_MIGRATION_CUTOVER_DRAIN_SQL) + .first>(), + ), + ); + expect(isProtocolV3CutoverDrained(await readDrain())).toBeTrue(); + + for (const [column, value] of [ + ["cleanup_operation_kind", "'archive'"], + ["runtime_provisioning_operation_id", "'operation-1'"], + ["runtime_provisioning_run_id", "'run-1'"], + ["runtime_provisioning_sandbox_id", "'sandbox-1'"], + ["runtime_provisioning_sandbox_session_id", "'sandbox-session-1'"], + ["runtime_provisioning_sandbox_incarnation", "1"], + ["runtime_provisioning_heartbeat_at", "1"], + ] as const) { + database.execute(`UPDATE session SET "${column}" = ${value} WHERE id = 'session-1'`); + expect((await readDrain()).unsafeSessions).toBe(1); + database.execute(`UPDATE session SET "${column}" = NULL WHERE id = 'session-1'`); + } + + database.execute( + "UPDATE session SET archived_at = 1, cleanup_operation_kind = 'archive' WHERE id = 'session-1'", + ); + expect(isProtocolV3CutoverDrained(await readDrain())).toBeTrue(); + database.execute("UPDATE session SET cleanup_operation_kind = 'delete' WHERE id = 'session-1'"); + expect((await readDrain()).unsafeSessions).toBe(1); + database.execute( + "UPDATE session SET archived_at = NULL, cleanup_operation_kind = NULL WHERE id = 'session-1'", + ); + + database.execute("UPDATE sandbox SET operation_kind = 'activate' WHERE id = 'sandbox-1'"); + expect((await readDrain()).unsafeSandboxes).toBe(1); + expect( + await database.prepare(PROTOCOL_V3_POST_MIGRATION_UNSAFE_SANDBOXES_SQL).first("id"), + ).toBe("sandbox-1"); + database.execute("UPDATE sandbox SET operation_kind = NULL WHERE id = 'sandbox-1'"); + database.execute( + "INSERT INTO sandbox_backup_staging VALUES ('staging-1', 'sandbox-1', 'session-1')", + ); + expect((await readDrain()).unsafeSandboxBackupStaging).toBe(1); + database.execute(INSERT_ENVIRONMENT_ARTIFACT_STAGING_SQL); + expect((await readDrain()).unsafeEnvironmentArtifactBackupStaging).toBe(1); + database.execute("INSERT INTO app_deployment_run VALUES ('active-app-run', 'activating')"); + expect((await readDrain()).activeAppDeploymentRuns).toBe(1); + expect(isProtocolV3CutoverDrained(await readDrain())).toBeFalse(); + }); + + test("allows only deterministic legacy terminal normalization before migration 0014", async () => { + const database = createLegacyTerminalDatabase(); + database.execute(` + INSERT INTO session VALUES ('session-1', 'failed-run', 1, 3, 'IDLE', NULL); + INSERT INTO session_run VALUES + (100, NULL, NULL, NULL, 'completed-run', 'session-1', 'completed', 'run.complete'), + (100, 'runtime.failed', NULL, 'Run failed', 'failed-run', 'session-1', 'failed', 'run.fail'); + INSERT INTO session_event VALUES + ('completed-event', 'run.completed', 'completed-run', 1, 'session-1', 'session-run-terminal:completed-run:run.completed'), + ('failed-event', 'run.failed', 'failed-run', 2, 'session-1', 'session-run-terminal:failed-run:run.failed'), + ('message-event', 'message.added', 'completed-run', 3, 'session-1', 'message-source'); + INSERT INTO session_message VALUES + ('assistant-1', '[]', 'assistant', 1, '[]', 'session-1', 'completed-run'); + `); + + const integrity = await readLegacyTerminalIntegrity(database); + expect(integrity).toEqual({ + ambiguousAssistantRuns: 0, + duplicateTerminalRuns: 0, + invalidFailedRuns: 0, + invalidNonfailedRunErrors: 0, + invalidTerminalLinks: 0, + legacyMaterializedMessages: 1, + legacyStreamRows: 1, + legacyTerminalEvents: 2, + mismatchedTerminalEvents: 0, + missingTerminalEvents: 0, + noncanonicalTerminalSources: 0, + nonterminalCommands: 0, + partialAssistantProjections: 0, + partialTerminalProjections: 0, + repairableFailedRuns: 1, + rewriteCandidateManifestJson: "[]", + unsettledEffects: 0, + }); + expect(() => assertProtocolV3LegacyTerminalIntegrity(integrity)).not.toThrow(); + const sourceInventory = await readLegacyTerminalSourceInventory(database); + expect(sourceInventory).toEqual({ + cancelled: { canonical: 0, canonicalTargetCollisions: 0, noncanonical: 0, total: 0 }, + completed: { canonical: 1, canonicalTargetCollisions: 0, noncanonical: 0, total: 1 }, + failed: { canonical: 1, canonicalTargetCollisions: 0, noncanonical: 0, total: 1 }, + invalidTerminalLinks: 0, + mismatchedTerminalEvents: 0, + multipleTerminalRuns: 0, + }); + expect(() => assertProtocolV3LegacyTerminalSourceInventory(sourceInventory)).not.toThrow(); + }); + + test("inventories deterministic source rewrites but rejects collisions and multiple terminals", async () => { + const database = createValidLegacyCompletionDatabase(); + database.execute(` + UPDATE session_event + SET source_event_id = 'provider-completion-event' + WHERE id = 'completed-event'; + INSERT INTO session_event VALUES + ('canonical-source-owner', 'message.added', 'completed-run', 2, 'session-1', 'session-run-terminal:completed-run:run.completed'), + ('second-terminal', 'run.failed', 'completed-run', 3, 'session-1', 'session-run-terminal:completed-run:run.failed'); + `); + + const inventory = await readLegacyTerminalSourceInventory(database); + expect(inventory).toEqual({ + cancelled: { canonical: 0, canonicalTargetCollisions: 0, noncanonical: 0, total: 0 }, + completed: { canonical: 0, canonicalTargetCollisions: 1, noncanonical: 1, total: 1 }, + failed: { canonical: 1, canonicalTargetCollisions: 0, noncanonical: 0, total: 1 }, + invalidTerminalLinks: 0, + mismatchedTerminalEvents: 1, + multipleTerminalRuns: 1, + }); + expect(() => assertProtocolV3LegacyTerminalSourceInventory(inventory)).toThrow( + "not ready for the protocol v3 production cutover", + ); + }); + + test("allows collision-free provider sources as deterministic migration rewrite candidates", async () => { + const database = createValidLegacyCompletionDatabase(); + database.execute( + "UPDATE session_event SET source_event_id = 'provider-completion-event' WHERE id = 'completed-event'", + ); + + const inventory = await readLegacyTerminalSourceInventory(database); + expect(inventory.completed).toEqual({ + canonical: 0, + canonicalTargetCollisions: 0, + noncanonical: 1, + total: 1, + }); + expect(() => assertProtocolV3LegacyTerminalSourceInventory(inventory)).not.toThrow(); + const integrity = await readLegacyTerminalIntegrity(database); + expect(JSON.parse(integrity.rewriteCandidateManifestJson)).toEqual([ + [ + "completed-event", + "session-1", + "completed-run", + "run.completed", + "provider-completion-event", + 1, + ], + ]); + }); + + for (const fixture of [ + { + blocker: "mismatchedTerminalEvents", + label: "a source rewrite whose event kind conflicts with its Run", + mutate: "UPDATE session_run SET status = 'failed' WHERE id = 'completed-run'", + }, + { + blocker: "invalidTerminalLinks", + label: "a source rewrite without an exact Run link", + mutate: "UPDATE session_event SET run_id = 'unknown-run' WHERE id = 'completed-event'", + }, + ] as const) { + test(`rejects ${fixture.label} in the read-only inventory`, async () => { + const database = createValidLegacyCompletionDatabase(); + database.execute(fixture.mutate); + + const inventory = await readLegacyTerminalSourceInventory(database); + expect(inventory[fixture.blocker]).toBe(1); + expect(() => assertProtocolV3LegacyTerminalSourceInventory(inventory)).toThrow( + "not ready for the protocol v3 production cutover", + ); + }); + } + + test("blocks every ambiguous legacy terminal and assistant projection", async () => { + const database = createLegacyTerminalDatabase(); + database.execute(` + INSERT INTO session VALUES ('session-1', 'partial-run', 3, 20, 'RUNNING', 'operation-1'); + INSERT INTO session_run VALUES + (100, NULL, NULL, NULL, 'duplicate-run', 'session-1', 'completed', 'run.complete'), + (100, NULL, NULL, NULL, 'mismatch-run', 'session-1', 'completed', 'run.complete'), + (100, 'runtime.failed', '{}', 'Run failed', 'missing-run', 'session-1', 'failed', 'run.fail'), + (NULL, 'runtime.failed', '{}', 'Run failed', 'partial-run', 'session-1', 'failed', 'run.complete'), + (100, NULL, NULL, NULL, 'ambiguous-run', 'session-1', 'completed', 'run.complete'), + (100, NULL, NULL, 'Run failed', 'invalid-error-run', 'session-1', 'failed', 'run.fail'), + (100, 'stale.error', '{}', 'Stale error', 'invalid-nonfailed-error-run', 'session-1', 'completed', 'run.complete'); + INSERT INTO session_event VALUES + ('duplicate-event-1', 'run.completed', 'duplicate-run', 1, 'session-1', 'session-run-terminal:duplicate-run:run.completed'), + ('duplicate-event-2', 'run.completed', 'duplicate-run', 2, 'session-1', 'legacy-duplicate-source'), + ('mismatch-event', 'run.failed', 'mismatch-run', 3, 'session-1', 'session-run-terminal:mismatch-run:run.failed'), + ('partial-event', 'run.failed', 'partial-run', 4, 'session-1', 'session-run-terminal:partial-run:run.failed'), + ('ambiguous-event', 'run.completed', 'ambiguous-run', 5, 'session-1', 'session-run-terminal:ambiguous-run:run.completed'), + ('invalid-error-event', 'run.failed', 'invalid-error-run', 6, 'session-1', 'session-run-terminal:invalid-error-run:run.failed'), + ('invalid-nonfailed-error-event', 'run.completed', 'invalid-nonfailed-error-run', 7, 'session-1', 'session-run-terminal:invalid-nonfailed-error-run:run.completed'), + ('orphan-event', 'run.completed', 'unknown-run', 8, 'session-1', 'session-run-terminal:unknown-run:run.completed'); + INSERT INTO session_message VALUES + ('partial-assistant', '[]', 'assistant', 1, '[]', 'session-1', 'partial-run'), + ('ambiguous-assistant-1', '[]', 'assistant', 2, '[]', 'session-1', 'ambiguous-run'), + ('ambiguous-assistant-2', '[]', 'assistant', 3, '[]', 'session-1', 'ambiguous-run'); + INSERT INTO session_permission_request VALUES ('partial-permission', 'partial-run', 'session-1'); + INSERT INTO driver_command VALUES ('accepted-command', 'accepted'); + INSERT INTO external_tool_effect VALUES ('claimed-effect', 'claimed'); + `); + + const integrity = await readLegacyTerminalIntegrity(database); + expect(integrity).toMatchObject({ + ambiguousAssistantRuns: 1, + duplicateTerminalRuns: 1, + invalidFailedRuns: 1, + invalidNonfailedRunErrors: 1, + invalidTerminalLinks: 1, + mismatchedTerminalEvents: 1, + missingTerminalEvents: 1, + noncanonicalTerminalSources: 1, + nonterminalCommands: 1, + partialAssistantProjections: 1, + partialTerminalProjections: 1, + unsettledEffects: 1, + }); + expect(() => assertProtocolV3LegacyTerminalIntegrity(integrity)).toThrow( + "Legacy terminal integrity is ambiguous", + ); + }); + + for (const fixture of [ + { + blocker: "partialTerminalProjections", + label: "a terminal event beyond the Session cursor", + mutate: "UPDATE session SET runtime_event_seq_cursor = 0 WHERE id = 'session-1'", + }, + { + blocker: "partialAssistantProjections", + label: "a materialized assistant beyond the Session cursor", + mutate: "UPDATE session SET message_seq_cursor = 0 WHERE id = 'session-1'", + }, + { + blocker: "partialTerminalProjections", + label: "a terminal Run with a permission request", + mutate: + "INSERT INTO session_permission_request VALUES ('permission-1', 'completed-run', 'session-1')", + }, + { + blocker: "partialTerminalProjections", + label: "a current terminal Run whose Session remains live", + mutate: "UPDATE session SET status = 'RUNNING' WHERE id = 'session-1'", + }, + { + blocker: "partialTerminalProjections", + label: "a current terminal Run whose Session keeps an operation fence", + mutate: "UPDATE session SET status_operation_id = 'operation-1' WHERE id = 'session-1'", + }, + { + blocker: "partialTerminalProjections", + label: "a terminal Run without a completion timestamp", + mutate: "UPDATE session_run SET completed_at = NULL WHERE id = 'completed-run'", + }, + { + blocker: "partialTerminalProjections", + label: "a terminal Run with the wrong lifecycle event", + mutate: "UPDATE session_run SET status_event = 'run.fail' WHERE id = 'completed-run'", + }, + ] as const) { + test(`blocks ${fixture.label} before migration 0014`, async () => { + const database = createValidLegacyCompletionDatabase(); + database.execute(fixture.mutate); + + const integrity = await readLegacyTerminalIntegrity(database); + expect(integrity[fixture.blocker]).toBe(1); + expect(() => assertProtocolV3LegacyTerminalIntegrity(integrity)).toThrow( + "Legacy terminal integrity is ambiguous", + ); + }); + } + + test("keeps the recovery bookmark in the one-shot D1 gate", async () => { + const database = createCutoverGateDatabase(); + database.execute(INSTALL_PROTOCOL_V3_CUTOVER_SQL); + database.execute(ENTER_PROTOCOL_V3_DRAIN_SQL); + + const bookmark = "00000085-0000024c-00004c6d-8e61117bf38d7adb71b934ebbf891683"; + expect(parseTimeTravelBookmark(JSON.stringify({ bookmark }))).toBe(bookmark); + database.execute(storeProtocolV3CutoverBookmarkSql(bookmark)); + + const stored = await database + .prepare( + 'SELECT pre_migration_bookmark AS bookmark FROM "__protocol_v3_cutover" WHERE id = 1', + ) + .first<{ bookmark: string }>(); + expect(parseStoredProtocolV3CutoverBookmark(d1Json(stored))).toBe(bookmark); + + const sessionId = "01J0000000000000000000000A"; + const requestKey = "protocol-v3-cutover-2de67417-f144-47e1-8b2e-1e71621a0d92"; + database.execute(storeProtocolV3SmokeRequestKeySql(requestKey)); + const storedRequestKey = await database + .prepare(PROTOCOL_V3_SMOKE_REQUEST_KEY_SQL) + .first>(); + expect(parseStoredProtocolV3SmokeRequestKey(d1Json(storedRequestKey))).toBe(requestKey); + database.execute(storeProtocolV3SmokeSessionSql(sessionId)); + const storedSession = await database + .prepare(PROTOCOL_V3_SMOKE_SESSION_SQL) + .first>(); + expect(parseStoredProtocolV3SmokeSession(d1Json(storedSession))).toBe(sessionId); + }); + + test("persists migration intent before the remote apply can outlive this process", async () => { + const database = createCutoverGateDatabase(); + database.execute(INSTALL_PROTOCOL_V3_CUTOVER_SQL); + + database.execute(beginProtocolV3MigrationSql(RELEASE_TREE_OID)); + expect((await readCutoverState(database)).migrationStarted).toBe(false); + + database.execute(ENABLE_PROTOCOL_V3_COMMAND_FREEZE_SQL); + database.execute(beginProtocolV3MigrationSql(RELEASE_TREE_OID)); + expect(await readCutoverState(database)).toMatchObject({ + enabled: true, + migrationStarted: true, + }); + + database.execute(INSTALL_PROTOCOL_V3_CUTOVER_SQL); + expect((await readCutoverState(database)).migrationStarted).toBe(true); + }); + + test("requires exact trigger-free migration intent authority", async () => { + for (const mutate of [ + (database: SqliteD1Database) => + database.execute(` + DROP TABLE "${PROTOCOL_V3_MIGRATION_INTENT_TABLE}"; + CREATE TABLE "${PROTOCOL_V3_MIGRATION_INTENT_TABLE}" ( + id integer PRIMARY KEY, + started_at integer NOT NULL + ); + `), + (database: SqliteD1Database) => + database.execute(` + CREATE TRIGGER migration_intent_spoof + AFTER INSERT ON "${PROTOCOL_V3_MIGRATION_INTENT_TABLE}" + BEGIN + SELECT 1; + END; + `), + ]) { + const database = createCutoverGateDatabase(); + database.execute(INSTALL_PROTOCOL_V3_CUTOVER_SQL); + mutate(database); + await expect(readCutoverState(database)).rejects.toThrow( + "migration intent authority is invalid", + ); + } + }); + + test("treats an isolated migration intent table as incomplete gate cleanup", async () => { + const database = new SqliteD1Database(); + database.execute(`CREATE TABLE "${PROTOCOL_V3_MIGRATION_INTENT_TABLE}" (id integer)`); + const probe = await database + .prepare(PROTOCOL_V3_CUTOVER_PROBE_SQL) + .first>(); + expect(parseProtocolV3CutoverProbe(d1Json(probe))).toEqual({ gatePresent: true }); + }); + + test("keeps a durable marker until queue resume survives a restart", async () => { + const database = createCutoverGateDatabase(); + database.execute(INSTALL_PROTOCOL_V3_CUTOVER_SQL); + database.execute(ENABLE_PROTOCOL_V3_COMMAND_FREEZE_SQL); + database.execute(ENTER_PROTOCOL_V3_QUEUES_RESUMING_SQL); + + const readState = async () => + parseProtocolV3CutoverState( + d1Json( + await database.prepare(PROTOCOL_V3_COMMAND_FREEZE_SQL).first>(), + ), + ); + expect(await readState()).toEqual({ + commandFreeze: true, + ...EMPTY_ROLLOUT, + enabled: true, + phase: "queues_resuming", + }); + + await expect( + database + .prepare( + "INSERT INTO session_run VALUES ('v3-run', 'queued', '01J00000000000000000000001', 'session-1')", + ) + .run(), + ).rejects.toThrow("protocol v3 cutover blocks new active Session Runs"); + + database.execute(INSTALL_PROTOCOL_V3_CUTOVER_SQL); + expect(await readState()).toEqual({ + commandFreeze: true, + ...EMPTY_ROLLOUT, + enabled: true, + phase: "queues_resuming", + }); + + database.execute(ACCEPT_PROTOCOL_V3_QUEUE_RESUME_SQL); + expect(await readState()).toEqual({ + commandFreeze: true, + ...EMPTY_ROLLOUT, + enabled: false, + phase: "queues_resuming", + }); + await expect( + database + .prepare("INSERT INTO session_run VALUES ('v3-run', 'queued', 'account-1', 'session-1')") + .run(), + ).resolves.toBeDefined(); + + expect(await readCutoverObjects(database)).toEqual({ + exactObjectCount: PROTOCOL_V3_CUTOVER_OBJECT_COUNT, + objectCount: PROTOCOL_V3_CUTOVER_OBJECT_COUNT, + }); + + database.execute(DROP_PROTOCOL_V3_CUTOVER_TRIGGERS_SQL); + expect(await readCutoverObjects(database)).toEqual({ + exactObjectCount: 0, + objectCount: 1, + }); + await expect( + database + .prepare( + "INSERT INTO session_run VALUES ('cleanup-interrupted', 'queued', '01J00000000000000000000001', 'session-1')", + ) + .run(), + ).resolves.toBeDefined(); + + database.execute(INSTALL_PROTOCOL_V3_CUTOVER_SQL); + database.execute(REMOVE_PROTOCOL_V3_CUTOVER_SQL); + expect(await readCutoverObjects(database)).toEqual({ + exactObjectCount: 0, + objectCount: 0, + }); + }); + + test("binds crash recovery to one clean Git tree and one rollout", async () => { + const database = createCutoverGateDatabase(); + database.execute(INSTALL_PROTOCOL_V3_CUTOVER_SQL); + database.execute(INSTALL_PROTOCOL_V3_CUTOVER_SQL); + const initial = await readCutoverState(database); + expect(() => assertProtocolV3Release(initial, RELEASE_TREE_OID)).not.toThrow(); + + const otherRelease = "89abcdef0123456789abcdef0123456789abcdef"; + database.execute(installProtocolV3CutoverSql(otherRelease)); + expect(() => assertProtocolV3Release(initial, otherRelease)).toThrow("belongs to release tree"); + + const workerVersionId = "2de67417-f144-47e1-8b2e-1e71621a0d92"; + database.execute( + storeProtocolV3RolloutSql(RELEASE_TREE_OID, workerVersionId, 17, CONTAINER_IMAGE_DIGEST), + ); + database.execute( + storeProtocolV3RolloutSql(RELEASE_TREE_OID, workerVersionId, 17, CONTAINER_IMAGE_DIGEST), + ); + expect(await readCutoverState(database)).toEqual({ + commandFreeze: false, + containerApplicationVersion: 17, + containerImageDigest: CONTAINER_IMAGE_DIGEST, + enabled: true, + migrationStarted: false, + phase: "draining", + releaseTreeOid: RELEASE_TREE_OID, + workerVersionId, + }); + + database.execute( + storeProtocolV3RolloutSql( + RELEASE_TREE_OID, + "aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa", + 18, + "b".repeat(64), + ), + ); + expect((await readCutoverState(database)).workerVersionId).toBe(workerVersionId); + }); + + test("rejects dirty tracked, untracked, and submodule release trees", () => { + expect(parseCleanGitTreeOid(`${RELEASE_TREE_OID}\n`, "")).toBe(RELEASE_TREE_OID); + for (const status of [" M apps/api/src/index.ts", "?? untracked.ts", " m apps/driver"]) { + expect(() => parseCleanGitTreeOid(RELEASE_TREE_OID, status)).toThrow("clean Git worktree"); + } + for (const index of ["S apps/api/src/index.ts", "h apps/api/src/index.ts"]) { + expect(() => parseCleanGitTreeOid(RELEASE_TREE_OID, "", index)).toThrow("clean Git worktree"); + } + expect(() => parseCleanGitTreeOid(RELEASE_TREE_OID, "", "", "apps/web/.env.prod")).toThrow( + "Vite build inputs", + ); + expect(() => + parseCleanGitTreeOid(RELEASE_TREE_OID, "", "", "", ["PATH", "VITE_MOSOO_ENVIRONMENT"]), + ).toThrow("Vite build inputs"); + }); + + test("verifies the exact tagged Worker version before accepting rollout", () => { + const versionId = "2de67417-f144-47e1-8b2e-1e71621a0d92"; + expect( + parseProtocolV3WorkerDeployment( + JSON.stringify({ versions: [{ percentage: 100, version_id: versionId }] }), + ), + ).toEqual({ versionId }); + expect(() => + assertProtocolV3WorkerVersion( + JSON.stringify({ + annotations: { "workers/tag": `protocol-v3-${RELEASE_TREE_OID}` }, + id: versionId, + }), + versionId, + RELEASE_TREE_OID, + ), + ).not.toThrow(); + expect(() => + assertProtocolV3WorkerVersion( + JSON.stringify({ annotations: { "workers/tag": "protocol-v3-wrong" }, id: versionId }), + versionId, + RELEASE_TREE_OID, + ), + ).toThrow("not the exact protocol v3 release"); + + const repository = "registry.cloudflare.com/account/mosoo-api-prod-sandbox-prod"; + expect(protocolV3ContainerImageTag(repository, versionId)).toBe(`${repository}:2de67417`); + expect( + parseProtocolV3ContainerManifestDigest( + JSON.stringify({ Descriptor: { digest: `sha256:${CONTAINER_IMAGE_DIGEST}` } }), + ), + ).toBe(CONTAINER_IMAGE_DIGEST); + }); + + test("keeps admission closed when queue resume crashes partway and converges on retry", async () => { + const database = createCutoverGateDatabase(); + database.execute(INSTALL_PROTOCOL_V3_CUTOVER_SQL); + database.execute(ENABLE_PROTOCOL_V3_COMMAND_FREEZE_SQL); + database.execute(ENTER_PROTOCOL_V3_QUEUES_RESUMING_SQL); + const queues = [true, true, true]; + const actions: string[] = []; + + await expect( + completeProtocolV3QueueResume(await readCutoverState(database), { + commitAcceptance: () => { + actions.push("commit"); + database.execute(ACCEPT_PROTOCOL_V3_QUEUE_RESUME_SQL); + }, + removeMarker: () => { + actions.push("remove"); + database.execute(REMOVE_PROTOCOL_V3_CUTOVER_SQL); + }, + resumeAndVerifyQueues: async () => { + actions.push("resume-1"); + queues[0] = false; + queues[1] = false; + throw new Error("third queue readback failed"); + }, + }), + ).rejects.toThrow("third queue readback failed"); + expect(actions).toEqual(["resume-1"]); + expect(await readCutoverState(database)).toEqual({ + commandFreeze: true, + ...EMPTY_ROLLOUT, + enabled: true, + phase: "queues_resuming", + }); + + await completeProtocolV3QueueResume(await readCutoverState(database), { + commitAcceptance: () => { + actions.push("commit"); + database.execute(ACCEPT_PROTOCOL_V3_QUEUE_RESUME_SQL); + }, + removeMarker: () => { + actions.push("remove"); + database.execute(REMOVE_PROTOCOL_V3_CUTOVER_SQL); + }, + resumeAndVerifyQueues: async () => { + actions.push("resume-2"); + queues.fill(false); + }, + }); + expect(actions).toEqual(["resume-1", "resume-2", "commit", "remove"]); + expect(queues).toEqual([false, false, false]); + expect(await readCutoverObjects(database)).toEqual({ exactObjectCount: 0, objectCount: 0 }); + }); + + test("mutates and reads back every cutover Queue lane, then re-pauses all after failure", async () => { + const paused = new Map(PROTOCOL_V3_CUTOVER_QUEUE_NAMES.map((name) => [name, true])); + const calls: string[] = []; + let staleReadback: string | null = null; + const control = { + list: async () => PROTOCOL_V3_CUTOVER_QUEUE_NAMES.map((name) => ({ id: `id:${name}`, name })), + mutate: (queueName: string, action: "pause" | "resume") => { + calls.push(`${action}:${queueName}`); + paused.set(queueName, action === "pause"); + }, + read: async (queueId: string) => { + const name = queueId.slice("id:".length); + calls.push(`read:${name}`); + const deliveryPaused = paused.get(name); + if (deliveryPaused === undefined) throw new Error(`Unknown Queue ${name}.`); + return { + deliveryPaused: name === staleReadback ? !deliveryPaused : deliveryPaused, + name, + }; + }, + }; + + await updateAndVerifyProtocolV3QueueDelivery(control, "resume"); + expect([...paused.entries()]).toEqual( + PROTOCOL_V3_CUTOVER_QUEUE_NAMES.map((name) => [name, false]), + ); + await updateAndVerifyProtocolV3QueueDelivery(control, "pause"); + expect([...paused.entries()]).toEqual( + PROTOCOL_V3_CUTOVER_QUEUE_NAMES.map((name) => [name, true]), + ); + + staleReadback = "api-command-dlq"; + await expect(updateAndVerifyProtocolV3QueueDelivery(control, "resume")).rejects.toThrow( + "Production queue resume and readback failed", + ); + staleReadback = null; + await updateAndVerifyProtocolV3QueueDelivery(control, "pause"); + expect([...paused.entries()]).toEqual( + PROTOCOL_V3_CUTOVER_QUEUE_NAMES.map((name) => [name, true]), + ); + for (const action of ["pause", "resume"] as const) { + for (const name of PROTOCOL_V3_CUTOVER_QUEUE_NAMES) { + expect(calls).toContain(`${action}:${name}`); + expect(calls).toContain(`read:${name}`); + } + } + }); + + test("drains and re-pauses all three API command lanes before migration", async () => { + const deploySource = await Bun.file(new URL("../bin/deploy-prod.ts", import.meta.url)).text(); + const drainStart = deploySource.indexOf("if (initialPendingMigrations.length > 0)"); + const migrationStart = deploySource.indexOf("if (durableMcpMigrationPending)", drainStart); + const drainSource = deploySource.slice(drainStart, migrationStart); + + expect(drainSource).toContain("await resumeAndVerifyProdQueues(queueApiConfig);"); + expect(drainSource).toContain("await pauseAndVerifyProdQueues(queueApiConfig);"); + expect(drainSource).not.toContain('["api-command"]'); + expect(deploySource).toContain("let migrationStarted = true;"); + expect( + deploySource.indexOf("cutoverState = beginProtocolV3Migration(", migrationStart), + ).toBeLessThan(deploySource.indexOf("applyD1Migrations();", migrationStart)); + expect( + deploySource.indexOf("authorizeProdLegacyTerminalRewrite(", migrationStart), + ).toBeGreaterThan( + deploySource.indexOf("cutoverState = beginProtocolV3Migration(", migrationStart), + ); + expect( + deploySource.indexOf("authorizeProdLegacyTerminalRewrite(", migrationStart), + ).toBeLessThan(deploySource.indexOf("applyD1Migrations();", migrationStart)); + }); + + test("recovers a lost acceptance acknowledgement and an externally re-paused queue", async () => { + const database = createCutoverGateDatabase(); + database.execute(INSTALL_PROTOCOL_V3_CUTOVER_SQL); + database.execute(ENABLE_PROTOCOL_V3_COMMAND_FREEZE_SQL); + database.execute(ENTER_PROTOCOL_V3_QUEUES_RESUMING_SQL); + const queues = [true, true, true]; + const actions: string[] = []; + + await expect( + completeProtocolV3QueueResume(await readCutoverState(database), { + commitAcceptance: () => { + actions.push("commit-lost-ack"); + database.execute(ACCEPT_PROTOCOL_V3_QUEUE_RESUME_SQL); + throw new Error("acceptance readback unavailable"); + }, + removeMarker: () => { + actions.push("remove"); + database.execute(REMOVE_PROTOCOL_V3_CUTOVER_SQL); + }, + resumeAndVerifyQueues: async () => { + actions.push("resume-1"); + queues.fill(false); + }, + }), + ).rejects.toThrow("acceptance readback unavailable"); + expect(await readCutoverState(database)).toEqual({ + commandFreeze: true, + ...EMPTY_ROLLOUT, + enabled: false, + phase: "queues_resuming", + }); + + queues[1] = true; + await completeProtocolV3QueueResume(await readCutoverState(database), { + commitAcceptance: () => actions.push("unexpected-recommit"), + removeMarker: () => { + actions.push("remove"); + database.execute(REMOVE_PROTOCOL_V3_CUTOVER_SQL); + }, + resumeAndVerifyQueues: async () => { + actions.push("resume-2"); + queues.fill(false); + }, + }); + expect(actions).toEqual(["resume-1", "commit-lost-ack", "resume-2", "remove"]); + expect(queues).toEqual([false, false, false]); + expect(await readCutoverObjects(database)).toEqual({ exactObjectCount: 0, objectCount: 0 }); + }); + + for (const spoof of [ + { + label: "a WHEN 0 trigger", + name: "__protocol_v3_cutover_session_run_insert", + }, + { + label: "a differently-cased reserved trigger", + name: "__PROTOCOL_V3_CUTOVER_SESSION_RUN_INSERT", + }, + ]) { + test(`fails exact gate verification for ${spoof.label}`, async () => { + const database = createCutoverGateDatabase(); + database.execute(` + CREATE TRIGGER "${spoof.name}" + BEFORE INSERT ON "session_run" + WHEN 0 + BEGIN + SELECT 1; + END; + `); + + database.execute(INSTALL_PROTOCOL_V3_CUTOVER_SQL); + expect(await readCutoverObjects(database)).toEqual({ + exactObjectCount: PROTOCOL_V3_CUTOVER_OBJECT_COUNT - 1, + objectCount: PROTOCOL_V3_CUTOVER_OBJECT_COUNT, + }); + await expect( + database + .prepare( + "INSERT INTO session_run VALUES ('spoof-run', 'queued', 'account-1', 'session-1')", + ) + .run(), + ).resolves.toBeDefined(); + + const probe = await database + .prepare(PROTOCOL_V3_CUTOVER_PROBE_SQL) + .first>(); + expect(parseProtocolV3CutoverProbe(d1Json(probe)).gatePresent).toBe(true); + }); + } + + test("rejects an extra trigger attached to a protected admission table", async () => { + const database = createCutoverGateDatabase(); + database.execute(INSTALL_PROTOCOL_V3_CUTOVER_SQL); + database.execute(` + CREATE TRIGGER disable_protocol_v3_gate + BEFORE INSERT ON "session_run" + BEGIN + UPDATE "__protocol_v3_cutover" + SET "command_freeze" = 1, "enabled" = 0, "phase" = 'queues_resuming' + WHERE "id" = 1; + END + `); + + expect(await readCutoverObjects(database)).toEqual({ + exactObjectCount: 0, + objectCount: PROTOCOL_V3_CUTOVER_OBJECT_COUNT + 1, + }); + await expect( + database + .prepare("INSERT INTO session_run VALUES ('spoof-run', 'queued', 'account-1', 'session-1')") + .run(), + ).resolves.toBeDefined(); + const probe = await database + .prepare(PROTOCOL_V3_CUTOVER_PROBE_SQL) + .first>(); + expect(parseProtocolV3CutoverProbe(d1Json(probe)).gatePresent).toBe(true); + }); + + test("excludes only the exact permanent sandbox identity trigger", async () => { + const database = createCutoverGateDatabase(); + database.execute(` + ALTER TABLE sandbox ADD kind text; + ALTER TABLE sandbox ADD agent_id text; + ALTER TABLE sandbox ADD app_id text; + ALTER TABLE sandbox ADD owner_account_id text; + `); + database.execute(INSTALL_PROTOCOL_V3_CUTOVER_SQL); + const managed = MANAGED_PROD_SCHEMA_TRIGGERS.find( + (trigger) => trigger.name === "sandbox_identity_immutable", + ); + if (managed === undefined) throw new Error("Sandbox identity trigger fixture is missing."); + database.execute(managed.sql); + expect(await readCutoverObjects(database)).toEqual({ + exactObjectCount: PROTOCOL_V3_CUTOVER_OBJECT_COUNT, + objectCount: PROTOCOL_V3_CUTOVER_OBJECT_COUNT, + }); + + database.execute(` + DROP TRIGGER sandbox_identity_immutable; + CREATE TRIGGER sandbox_identity_immutable + BEFORE INSERT ON sandbox WHEN 0 + BEGIN + SELECT 1; + END; + `); + expect(await readCutoverObjects(database)).toEqual({ + exactObjectCount: 0, + objectCount: PROTOCOL_V3_CUTOVER_OBJECT_COUNT + 1, + }); + }); + + test("excludes only the exact permanent environment staging triggers", async () => { + const database = createCutoverGateDatabase(); + addPostRuntimeAuthoritySchema(database); + database.execute(INSTALL_PROTOCOL_V3_POST_MIGRATION_CUTOVER_SQL); + for (const name of [ + "environment_package_artifact_backup_staging_authority", + "environment_package_artifact_backup_staging_immutable", + ]) { + const managed = MANAGED_PROD_SCHEMA_TRIGGERS.find((trigger) => trigger.name === name); + if (managed === undefined) throw new Error(`Managed trigger ${name} fixture is missing.`); + database.execute(managed.sql); + } + expect(await readCutoverObjects(database)).toEqual({ + exactObjectCount: PROTOCOL_V3_POST_MIGRATION_CUTOVER_OBJECT_COUNT, + objectCount: PROTOCOL_V3_POST_MIGRATION_CUTOVER_OBJECT_COUNT, + }); + + database.execute(` + DROP TRIGGER environment_package_artifact_backup_staging_authority; + CREATE TRIGGER environment_package_artifact_backup_staging_authority + BEFORE INSERT ON environment_package_artifact_backup_staging WHEN 0 + BEGIN + SELECT 1; + END; + `); + expect(await readCutoverObjects(database)).toEqual({ + exactObjectCount: 0, + objectCount: PROTOCOL_V3_POST_MIGRATION_CUTOVER_OBJECT_COUNT + 1, + }); + }); + + test("does not exempt a same-name spoof of the rewrite revocation trigger", async () => { + const database = createCutoverGateDatabase(); + database.execute(INSTALL_PROTOCOL_V3_CUTOVER_SQL); + database.execute(` + CREATE TRIGGER "__protocol_v3_legacy_rewrite_gate_update" + BEFORE INSERT ON "session_run" + BEGIN + UPDATE "__protocol_v3_cutover" + SET "command_freeze" = 1, "enabled" = 0, "phase" = 'queues_resuming' + WHERE "id" = 1; + END + `); + + expect(await readCutoverObjects(database)).toEqual({ + exactObjectCount: 0, + objectCount: PROTOCOL_V3_CUTOVER_OBJECT_COUNT + 1, + }); + await expect( + database + .prepare( + "INSERT INTO session_run VALUES ('auth-spoof-run', 'queued', 'account-1', 'session-1')", + ) + .run(), + ).resolves.toBeDefined(); + }); + + test("detects a same-name object from another sqlite namespace through cleanup", async () => { + const database = createCutoverGateDatabase(); + database.execute(INSTALL_PROTOCOL_V3_CUTOVER_SQL); + database.execute(` + DROP TRIGGER "__protocol_v3_cutover_session_run_insert"; + CREATE VIEW "__protocol_v3_cutover_session_run_insert" AS SELECT 1 AS value; + `); + + database.execute(INSTALL_PROTOCOL_V3_CUTOVER_SQL); + expect(await readCutoverObjects(database)).toEqual({ + exactObjectCount: 0, + objectCount: PROTOCOL_V3_CUTOVER_OBJECT_COUNT + 1, + }); + + database.execute(REMOVE_PROTOCOL_V3_CUTOVER_SQL); + expect(await readCutoverObjects(database)).toEqual({ + exactObjectCount: 0, + objectCount: 1, + }); + }); + + test("retries accepted queue verification and idempotent marker cleanup without re-pausing", async () => { + const database = createCutoverGateDatabase(); + database.execute(INSTALL_PROTOCOL_V3_CUTOVER_SQL); + database.execute(ENABLE_PROTOCOL_V3_COMMAND_FREEZE_SQL); + database.execute(ENTER_PROTOCOL_V3_QUEUES_RESUMING_SQL); + database.execute(ACCEPT_PROTOCOL_V3_QUEUE_RESUME_SQL); + database.execute(REMOVE_PROTOCOL_V3_CUTOVER_SQL); + const calls: string[] = []; + const originalError = new Error("marker deletion committed before its readback failed"); + const state = { + bookmark: null, + initialPendingMigrations: [], + migrationStarted: false, + originalError, + queuesVerified: true, + }; + await expect( + recoverProtocolV3CutoverFailure(state, { + commitQueueAcceptance: () => calls.push("unexpected-commit"), + pauseAndVerifyQueues: async () => calls.push("unexpected-pause"), + printBookmark: () => calls.push("unexpected-bookmark"), + probe: () => { + calls.push("probe"); + return parseProtocolV3CutoverProbe( + d1Json({ + gate_present: 0, + }), + ); + }, + readBookmark: () => null, + readPendingMigrations: () => [], + removeMarker: () => calls.push("unexpected-remove"), + resumeAndVerifyQueues: async () => calls.push("resume"), + write: () => {}, + }), + ).resolves.toBeUndefined(); + expect(calls).toEqual(["resume", "probe"]); + expect(await readCutoverObjects(database)).toEqual({ exactObjectCount: 0, objectCount: 0 }); + }); + + test("keeps production closed when an unacknowledged migration commits after recovery starts", async () => { + const database = createCutoverGateDatabase(); + const migration = "0019_runtime-subject-operation-authority.sql"; + database.execute(INSTALL_PROTOCOL_V3_CUTOVER_SQL); + database.execute(ENABLE_PROTOCOL_V3_COMMAND_FREEZE_SQL); + database.execute(beginProtocolV3MigrationSql(RELEASE_TREE_OID)); + const calls: string[] = []; + const messages: string[] = []; + const originalError = new Error("migration apply acknowledgement timed out"); + + await expect( + recoverProtocolV3CutoverFailure( + { + bookmark: "emergency-bookmark", + initialPendingMigrations: [migration], + migrationStarted: (await readCutoverState(database)).migrationStarted, + originalError, + queuesVerified: false, + }, + { + commitQueueAcceptance: () => calls.push("unexpected-commit"), + pauseAndVerifyQueues: async () => { + calls.push("pause"); + database.execute(`INSERT INTO d1_migrations VALUES ('${migration}')`); + }, + printBookmark: () => calls.push("bookmark"), + probe: () => ({ gatePresent: true }), + readBookmark: () => null, + readPendingMigrations: () => { + calls.push("unexpected-read-pending"); + return [migration]; + }, + removeMarker: () => calls.push("unexpected-remove"), + resumeAndVerifyQueues: async () => calls.push("unexpected-resume"), + write: (message) => messages.push(message), + }, + ), + ).rejects.toThrow(originalError.message); + + expect(calls).toEqual(["pause", "bookmark"]); + expect(messages.join("\n")).toContain("migration request may have committed"); + expect(await readCutoverState(database)).toMatchObject({ + enabled: true, + migrationStarted: true, + }); + expect( + await database.prepare("SELECT name FROM d1_migrations").first<{ name: string }>(), + ).toEqual({ name: migration }); + }); + + test("keeps a gate-missing pre-acceptance failure closed after probing", async () => { + const infrastructureCalls: string[] = []; + const messages: string[] = []; + const originalError = new Error("cutover failed before queue acceptance"); + + await expect( + recoverProtocolV3CutoverFailure( + { + bookmark: "emergency-bookmark", + initialPendingMigrations: ["0019_runtime-subject-operation-authority.sql"], + migrationStarted: false, + originalError, + queuesVerified: false, + }, + { + commitQueueAcceptance: () => { + infrastructureCalls.push("commit"); + }, + pauseAndVerifyQueues: async () => { + infrastructureCalls.push("pause"); + }, + printBookmark: () => {}, + probe: () => { + infrastructureCalls.push("probe"); + return { + gatePresent: false, + }; + }, + readBookmark: () => { + infrastructureCalls.push("read-bookmark"); + return null; + }, + readPendingMigrations: () => { + infrastructureCalls.push("read-pending"); + return []; + }, + removeMarker: () => { + infrastructureCalls.push("remove"); + }, + resumeAndVerifyQueues: async () => { + infrastructureCalls.push("resume-and-verify"); + }, + write: (message) => messages.push(message), + }, + ), + ).rejects.toThrow(originalError.message); + + expect(infrastructureCalls).toEqual(["read-pending", "pause"]); + expect(messages.join("\n")).toContain("rolling forward this exact v3 release"); + expect(messages.join("\n")).not.toContain("manually resume"); + }); + + test("keeps queues closed when first Workflow publication fails its exact readback", async () => { + const calls: string[] = []; + const originalError = new Error("published Workflow binding is not exact"); + + await expect( + recoverProtocolV3CutoverFailure( + { + bookmark: null, + initialPendingMigrations: [], + migrationStarted: false, + originalError, + queuesVerified: false, + }, + { + commitQueueAcceptance: () => calls.push("unexpected-commit"), + pauseAndVerifyQueues: async () => calls.push("pause-and-verify"), + printBookmark: () => calls.push("unexpected-bookmark"), + probe: () => { + calls.push("unexpected-probe"); + return { gatePresent: true }; + }, + readBookmark: () => null, + readPendingMigrations: () => { + calls.push("unexpected-read-pending"); + return []; + }, + removeMarker: () => calls.push("unexpected-remove"), + resumeAndVerifyQueues: async () => calls.push("unexpected-resume"), + write: () => {}, + }, + ), + ).rejects.toThrow(originalError.message); + expect(calls).toEqual(["pause-and-verify"]); + }); + + test("restores the old service when legacy preflight fails before protocol migration", async () => { + const infrastructureCalls: string[] = []; + const originalError = new Error("Legacy terminal integrity is ambiguous"); + + await expect( + recoverProtocolV3CutoverFailure( + { + bookmark: null, + initialPendingMigrations: [ + "0013_durable-mcp-effect-v3.sql", + "0014_session-event-stream-identity.sql", + ], + migrationStarted: false, + originalError, + queuesVerified: false, + }, + { + commitQueueAcceptance: () => { + infrastructureCalls.push("commit"); + }, + pauseAndVerifyQueues: async () => { + infrastructureCalls.push("pause"); + }, + printBookmark: () => { + infrastructureCalls.push("print-bookmark"); + }, + probe: () => { + infrastructureCalls.push("probe"); + return { + gatePresent: true, + }; + }, + readBookmark: () => { + infrastructureCalls.push("read-bookmark"); + return null; + }, + readPendingMigrations: () => { + infrastructureCalls.push("read-pending"); + return ["0013_durable-mcp-effect-v3.sql", "0014_session-event-stream-identity.sql"]; + }, + removeMarker: () => { + infrastructureCalls.push("remove"); + }, + resumeAndVerifyQueues: async () => { + infrastructureCalls.push("resume-and-verify"); + }, + write: () => {}, + }, + ), + ).rejects.toThrow(originalError.message); + + expect(infrastructureCalls).toEqual(["read-pending", "resume-and-verify", "remove"]); + }); + + test("keeps old admission closed until every queue is verifiably resumed", async () => { + const calls: string[] = []; + const queues = [true, true, true]; + const originalError = new Error("legacy preflight failed"); + + await expect( + recoverProtocolV3CutoverFailure( + { + bookmark: null, + initialPendingMigrations: ["0019_runtime-subject-operation-authority.sql"], + migrationStarted: false, + originalError, + queuesVerified: false, + }, + { + commitQueueAcceptance: () => calls.push("commit"), + pauseAndVerifyQueues: async () => { + calls.push("pause-and-verify"); + queues.fill(true); + expect(queues).toEqual([true, true, true]); + }, + printBookmark: () => {}, + probe: () => ({ + gatePresent: true, + }), + readBookmark: () => null, + readPendingMigrations: () => { + calls.push("read-pending"); + return ["0019_runtime-subject-operation-authority.sql"]; + }, + removeMarker: () => calls.push("remove"), + resumeAndVerifyQueues: async () => { + calls.push("verify"); + queues[0] = false; + throw new Error("queue 2 remains paused"); + }, + write: () => {}, + }, + ), + ).rejects.toThrow(originalError.message); + expect(calls).toEqual(["read-pending", "verify", "pause-and-verify"]); + expect(queues).toEqual([true, true, true]); + }); + + test("re-pauses every queue when marker cleanup and its readback both fail", async () => { + const calls: string[] = []; + const originalError = new Error("pre-migration deploy failed"); + await expect( + recoverProtocolV3CutoverFailure( + { + bookmark: null, + initialPendingMigrations: ["0019_runtime-subject-operation-authority.sql"], + migrationStarted: false, + originalError, + queuesVerified: false, + }, + { + commitQueueAcceptance: () => calls.push("unexpected-commit"), + pauseAndVerifyQueues: async () => calls.push("pause-and-verify"), + printBookmark: () => calls.push("unexpected-bookmark"), + probe: () => { + calls.push("probe"); + throw new Error("D1 marker readback failed"); + }, + readBookmark: () => null, + readPendingMigrations: () => { + calls.push("read-pending"); + return ["0019_runtime-subject-operation-authority.sql"]; + }, + removeMarker: () => { + calls.push("remove"); + throw new Error("marker delete acknowledgement lost"); + }, + resumeAndVerifyQueues: async () => calls.push("resume-and-verify"), + write: () => {}, + }, + ), + ).rejects.toThrow(originalError.message); + expect(calls).toEqual([ + "read-pending", + "resume-and-verify", + "remove", + "pause-and-verify", + "probe", + ]); + }); + + test("re-pauses every queue when gate-absent old-service resume is only partial", async () => { + const calls: string[] = []; + const queues = [true, true, true]; + let resumeAttempts = 0; + const originalError = new Error("pre-migration deploy failed"); + + await expect( + recoverProtocolV3CutoverFailure( + { + bookmark: null, + initialPendingMigrations: ["0019_runtime-subject-operation-authority.sql"], + migrationStarted: false, + originalError, + queuesVerified: false, + }, + { + commitQueueAcceptance: () => calls.push("unexpected-commit"), + pauseAndVerifyQueues: async () => { + calls.push("pause-and-verify"); + queues.fill(true); + }, + printBookmark: () => calls.push("unexpected-bookmark"), + probe: () => { + calls.push("probe"); + return { gatePresent: false }; + }, + readBookmark: () => null, + readPendingMigrations: () => { + calls.push("read-pending"); + return ["0019_runtime-subject-operation-authority.sql"]; + }, + removeMarker: () => { + calls.push("remove"); + throw new Error("marker delete acknowledgement lost"); + }, + resumeAndVerifyQueues: async () => { + calls.push("resume-and-verify"); + resumeAttempts += 1; + if (resumeAttempts === 1) { + queues.fill(false); + return; + } + queues[0] = false; + throw new Error("queue resume was partial"); + }, + write: () => {}, + }, + ), + ).rejects.toThrow(originalError.message); + + expect(calls).toEqual([ + "read-pending", + "resume-and-verify", + "remove", + "pause-and-verify", + "probe", + "resume-and-verify", + "pause-and-verify", + ]); + expect(queues).toEqual([true, true, true]); + }); + + test("requires the live smoke Driver to complete protocol v3 hello and ready", () => { + const ready = parseProtocolV3SmokeStatus( + d1Json({ + boot_token_used_at: 1_787_942_399_000, + connection_id: "connection-1", + driver_pid: 42, + driver_started_at: 1_787_942_400_000, + driver_status: "ready", + driver_version: "3.0.0", + protocol_version: 3, + status_event: "driver.ready", + }), + ); + expect(isProtocolV3SmokeReady(ready)).toBeTrue(); + expect(isProtocolV3SmokeReady({ ...ready, driverPid: null })).toBeFalse(); + expect(isProtocolV3SmokeReady({ ...ready, connectionId: null })).toBeFalse(); + expect(isProtocolV3SmokeReady({ ...ready, driverStatus: "connecting" })).toBeFalse(); + expect(isProtocolV3SmokeReady({ ...ready, protocolVersion: 2 })).toBeFalse(); + expect(protocolV3SmokeStatusSql("01J0000000000000000000000A")).toContain( + '"latest"."driver_started_at"', + ); + }); + + test("requires a published cattle Agent for the production smoke", async () => { + const database = new SqliteD1Database(); + const agentId = "01J0000000000000000000000A"; + database.execute(` + CREATE TABLE agent (id text PRIMARY KEY, kind text NOT NULL, status text NOT NULL); + INSERT INTO agent VALUES ('${agentId}', 'pet', 'published'); + `); + const readAgent = async () => { + const row = await database + .prepare(protocolV3SmokeAgentSql(agentId)) + .first>(); + if (row === null) throw new Error("Smoke Agent query returned no row."); + return d1Json(row); + }; + + let raw = await readAgent(); + expect(() => assertProtocolV3SmokeAgent(raw)).toThrow("published cattle Agent"); + database.execute(`UPDATE agent SET kind = 'cattle', status = 'draft'`); + raw = await readAgent(); + expect(() => assertProtocolV3SmokeAgent(raw)).toThrow("published cattle Agent"); + database.execute(`UPDATE agent SET status = 'published'`); + raw = await readAgent(); + expect(() => assertProtocolV3SmokeAgent(raw)).not.toThrow(); + }); + + test("rejects an old Container version found only on a later page", async () => { + const requestedPages: Array = []; + const instances = await collectProtocolV3ContainerInstances((pageToken) => { + requestedPages.push(pageToken); + return pageToken === null + ? containerPage([{ state: "running", version: 3 }], null, "page-2") + : containerPage([{ state: "running", version: 2 }], "page-2", null); + }); + + expect(requestedPages).toEqual([null, "page-2"]); + expect(instances).toHaveLength(2); + expect( + isProtocolV3ContainerRolloutConverged({ state: "ready", version: 3 }, instances), + ).toBeFalse(); + expect( + isProtocolV3ContainerRolloutConverged({ state: "unknown", version: 3 }, [ + { state: "running", version: 3 }, + ]), + ).toBeFalse(); + }); + + test("accepts an exact scale-to-zero Container application with no old instances", async () => { + const [application] = await collectProtocolV3ContainerApplications(() => + containerApplicationPage( + [ + { + ...containerApplication("mosoo-api-prod-sandbox-prod", 3), + health: { + instances: { active: 0, failed: 0, healthy: 0, scheduling: 0, starting: 0 }, + }, + }, + ], + null, + null, + ), + ); + const instances = await collectProtocolV3ContainerInstances(() => + containerPage([], null, null), + ); + if (application === undefined) throw new Error("Container application fixture is missing."); + + expect(application.state).toBe("ready"); + expect(isProtocolV3ContainerRolloutConverged(application, instances)).toBeTrue(); + }); + + test.each(["degraded", "provisioning", "unknown"])( + "does not accept an empty %s Container application as converged", + (state) => { + expect(isProtocolV3ContainerRolloutConverged({ state, version: 3 }, [])).toBeFalse(); + }, + ); + + test.each(["active", "ready"])( + "accepts an empty exact %s Container application as converged", + (state) => { + expect(isProtocolV3ContainerRolloutConverged({ state, version: 3 }, [])).toBeTrue(); + }, ); + + test("finds the production Container application on a later page", async () => { + const requestedPages: Array = []; + const applications = await collectProtocolV3ContainerApplications((pageToken) => { + requestedPages.push(pageToken); + return pageToken === null + ? containerApplicationPage([containerApplication("other-app", 3)], null, "page-2") + : containerApplicationPage( + [containerApplication("mosoo-api-prod-sandbox-prod", 3)], + "page-2", + null, + ); + }); + + expect(requestedPages).toEqual([null, "page-2"]); + expect(applications.map(({ name }) => name)).toEqual([ + "other-app", + "mosoo-api-prod-sandbox-prod", + ]); + }); + + registerPaginationFailureTests({ + collect: collectProtocolV3ContainerApplications, + invalidMessage: "valid opaque token", + invalidPage: { result: [], result_info: { page_token: null }, success: true }, + label: "Container application", + page: (pageToken, nextPageToken) => containerApplicationPage([], pageToken, nextPageToken), + }); + + registerPaginationFailureTests({ + collect: collectProtocolV3ContainerInstances, + invalidMessage: "paginated object", + invalidPage: "[]", + label: "Container instance", + page: (pageToken, nextPageToken) => containerPage([], pageToken, nextPageToken), + }); }); diff --git a/apps/api/tests/public-thread-api-fixtures.ts b/apps/api/tests/public-thread-api-fixtures.ts index 62cebbb3..56e27594 100644 --- a/apps/api/tests/public-thread-api-fixtures.ts +++ b/apps/api/tests/public-thread-api-fixtures.ts @@ -3,7 +3,12 @@ import { expect } from "bun:test"; import { PUBLIC_API_PREFIX } from "@mosoo/contracts/public-api"; import type { SessionRuntimeEventVisibility } from "@mosoo/contracts/session"; import { sessionEventsTable } from "@mosoo/db"; -import { createRuntimeEvent } from "@mosoo/runtime-events"; +import { + createRuntimeEvent, + createRuntimeEventSemanticHash, + createSessionRunTerminalSourceId, + stringifyRuntimeEventSemanticValue, +} from "@mosoo/runtime-events"; import type { RuntimeEventKind, RuntimeEventVisibility } from "@mosoo/runtime-events"; import { Hono } from "hono"; @@ -119,6 +124,18 @@ const RUNTIME_EVENT_IDS_BY_SEQ = [ "01J00000000000000000000015", "01J00000000000000000000016", "01J00000000000000000000017", + "01J00000000000000000000018", + "01J00000000000000000000019", + "01J0000000000000000000001A", + "01J0000000000000000000001B", + "01J0000000000000000000001C", + "01J0000000000000000000001D", + "01J0000000000000000000001E", + "01J0000000000000000000001F", + "01J0000000000000000000001G", + "01J0000000000000000000001H", + "01J0000000000000000000001J", + "01J0000000000000000000001K", ]; function runtimeEventIdForSeq(seq: number): string { @@ -134,6 +151,7 @@ function runtimeEventIdForSeq(seq: number): string { export async function insertRuntimeEvent( database: SqliteD1Database, input: { + eventId?: string; kind: RuntimeEventKind; occurredAt: number; payload: unknown; @@ -147,19 +165,34 @@ export async function insertRuntimeEvent( const visibility = input.visibility ?? "participant"; const databaseVisibility: SessionRuntimeEventVisibility = visibility === "public" || visibility === "participant" ? "all_consumers" : "owner_debug"; - const eventId = runtimeEventIdForSeq(input.seq); + const eventId = input.eventId ?? runtimeEventIdForSeq(input.seq); + const sourceEventId = + runId !== null && + (input.kind === "run.cancelled" || + input.kind === "run.completed" || + input.kind === "run.failed") + ? createSessionRunTerminalSourceId(runId, input.kind) + : eventId; + const terminal = + input.kind === "run.cancelled" || input.kind === "run.completed" || input.kind === "run.failed"; + if (terminal && !isRecord(input.payload)) { + throw new Error("Terminal runtime event fixture payload must be an object."); + } const event = createRuntimeEvent({ actor: "driver", id: eventId, kind: input.kind, occurredAt: new Date(input.occurredAt).toISOString(), origin: "driver", - payload: input.payload, + payload: terminal ? { ...input.payload, lifecycle: "IDLE" } : input.payload, ...(runId === null ? {} : { runId }), sessionId: input.sessionId, + sourceEventId, visibility, }); const projection = createSessionRuntimeEventProjection(event); + const semanticHash = await createRuntimeEventSemanticHash(event); + const terminalEventJson = terminal ? stringifyRuntimeEventSemanticValue(event) : null; await database .app() @@ -172,17 +205,28 @@ export async function insertRuntimeEvent( eventType: input.kind, family: projection.family, id: eventId, + mcpCommandId: projection.mcpCommandId, occurredAt: input.occurredAt, processStatus: projection.processStatus, processType: projection.processType, runId, + runtimeOperationEventJson: null, + semanticHash, seq: input.seq, sessionId: input.sessionId, source: "driver", - sourceEventId: eventId, + sourceEventId, + streamId: projection.streamId, + terminalEventJson, toolCallId: projection.toolCallId, + toolInputDeltaJson: projection.toolInputDeltaJson, toolInputJson: projection.toolInputJson, toolName: projection.toolName, + toolOutputDeltaText: projection.toolOutputDeltaText, + toolOutputText: projection.toolOutputText, + toolParentMessageId: projection.toolParentMessageId, + toolResultMessageId: projection.toolResultMessageId, + toolStatus: projection.toolStatus, tokens: projection.tokens, traceId: null, visibility: databaseVisibility, @@ -223,7 +267,10 @@ export function createPublicEventSessionNamespace(): { fetch: async () => { const socket = new PublicEventTestSocket(); sockets.add(socket); - return { status: 101, webSocket: socket as unknown as WebSocket } as Response; + return { + status: 101, + webSocket: socket as unknown as WebSocket, + } as Response; }, publishEvents: async () => { for (const socket of sockets) { @@ -236,7 +283,7 @@ export function createPublicEventSessionNamespace(): { binding: { get: () => stub, idFromName: (name: string) => name, - } as unknown as ApiBindings["Session"], + }, close: () => { for (const socket of sockets) { socket.close(); diff --git a/apps/api/tests/public-thread-api.e2e.test.ts b/apps/api/tests/public-thread-api.e2e.test.ts index 000319f2..56718597 100644 --- a/apps/api/tests/public-thread-api.e2e.test.ts +++ b/apps/api/tests/public-thread-api.e2e.test.ts @@ -1,7 +1,12 @@ import { describe, expect, test } from "bun:test"; import { PUBLIC_THREAD_API_THREADS_MAX_LIMIT } from "@mosoo/contracts/public-api"; -import { sessionExecutionSnapshotsTable, sessionRunsTable, sessionsTable } from "@mosoo/db"; +import { + sessionExecutionSnapshotsTable, + sessionMessagesTable, + sessionRunsTable, + sessionsTable, +} from "@mosoo/db"; import { eq } from "drizzle-orm"; import { fileStore } from "../src/modules/files/application/file-store"; @@ -9,14 +14,18 @@ import { PUBLIC_API_RATE_LIMIT_REQUESTS_PER_MINUTE, enforcePublicApiRateLimit, } from "../src/modules/public-api/public-api-rate-limit.service"; +import { createSessionProcessEventsFromSessionEventRows } from "../src/modules/sessions/application/session-process-events.service"; +import type { SessionEventProcessRow } from "../src/modules/sessions/application/session-process-events.service"; import { insertSessionMessage } from "../src/modules/sessions/infrastructure/session-message-store.repository"; import type { ApiBindings } from "../src/platform/cloudflare/worker-types"; import { PublicApiMemoryFileBucket, PUBLIC_API_TEST_IDS, TOKENS, + createPre0014PublicHttpContractDatabase, createPublicHttpContractDatabase, createPublicHttpTestBindings, + migratePre0014PublicHttpContractDatabase, } from "./helpers/public-api-http-test-fixture"; import { OWNER_VIEWER, @@ -63,6 +72,7 @@ const PROGRESS_OUTPUT_TEXTS = [ "进度 2:已调用工具校验表格,不能进入最终回答。", "进度 3:artifact 已创建,不能进入最终回答。", ] as const; +const FINAL_MESSAGE_ID = "01J0000000000000000000001M"; type PublicHttpTestDatabase = Awaited>; @@ -97,7 +107,7 @@ async function createReadyAppDraftFile(input: { name: string; }): Promise { const bindings = createPublicHttpTestBindings(input.database, { - fileBucket: input.bucket as unknown as R2Bucket, + fileBucket: input.bucket, }) as ApiBindings; const fileBytes = new TextEncoder().encode(input.body); const upload = await fileStore.createUpload(bindings, OWNER_VIEWER, { @@ -136,7 +146,7 @@ async function createPendingAppDraftFile(input: { name: string; }): Promise { const bindings = createPublicHttpTestBindings(input.database, { - fileBucket: input.bucket as unknown as R2Bucket, + fileBucket: input.bucket, }) as ApiBindings; const fileBytes = new TextEncoder().encode(input.body); const upload = await fileStore.createUpload(bindings, OWNER_VIEWER, { @@ -537,6 +547,18 @@ describe("Public Thread API e2e", () => { seq: 2, sessionId: threadId, }); + await insertRuntimeEvent(database, { + kind: "tool.call.updated", + occurredAt: 975, + payload: { + status: "cancelled", + title: "record_meal", + toolCallId: "tool-call-2", + }, + runId, + seq: 3, + sessionId: threadId, + }); const toolEventsResponse = await requestPublicApi( app, @@ -552,16 +574,23 @@ describe("Public Thread API e2e", () => { expect(toolEvents.map((event) => event["toolCallId"])).toEqual([ "tool-call-1", "tool-call-1", + "tool-call-2", ]); expect(toolEvents[0]).toMatchObject({ + toolInput: { calories: 420, mealId: "meal-1" }, toolName: "record_meal", type: "tool.use.started", }); - expectNoProperties(toolEvents[0], ["toolInput"]); expect(toolEvents[1]).toMatchObject({ toolInput: { calories: 420, mealId: "meal-1" }, type: "tool.use.completed", }); + expect(toolEvents[2]).toMatchObject({ + content: "record_meal", + toolName: "record_meal", + type: "tool.use.completed", + }); + expectNoProperties(toolEvents[2], ["toolInput"]); await database.prepare("DELETE FROM session_event WHERE session_id = ?").bind(threadId).run(); @@ -594,7 +623,7 @@ describe("Public Thread API e2e", () => { occurredAt: 1_085, payload: { content: FINAL_OUTPUT_TEXT, - messageId: "assistant-final", + messageId: FINAL_MESSAGE_ID, role: "agent", }, runId, @@ -602,20 +631,28 @@ describe("Public Thread API e2e", () => { sessionId: threadId, }); await insertRuntimeEvent(database, { - kind: "run.completed", - occurredAt: 1_125, - payload: { stopReason: "debug" }, + kind: "message.completed", + occurredAt: 1_100, + payload: { messageId: FINAL_MESSAGE_ID, role: "agent" }, runId, seq: 6, sessionId: threadId, + }); + await insertRuntimeEvent(database, { + kind: "run.started", + occurredAt: 1_125, + payload: { startedAt: "1970-01-01T00:00:01.125Z" }, + runId, + seq: 7, + sessionId: threadId, visibility: "owner_debug", }); await insertRuntimeEvent(database, { kind: "run.completed", occurredAt: 1_150, - payload: { stopReason: "end_turn" }, + payload: { finalMessageId: FINAL_MESSAGE_ID, stopReason: "end_turn" }, runId, - seq: 7, + seq: 8, sessionId: threadId, }); @@ -652,13 +689,19 @@ describe("Public Thread API e2e", () => { }); } await insertSessionMessage(database, { - content: FINAL_OUTPUT_TEXT, + content: "", createdByAccountId: PUBLIC_API_TEST_IDS.ownerAccount, + id: FINAL_MESSAGE_ID, role: "assistant", - segments: [{ kind: "text", text: FINAL_OUTPUT_TEXT }], sessionId: threadId, sessionRunId: runId, }); + await database + .app() + .update(sessionMessagesTable) + .set({ projectionFormat: "event_stream_v3" }) + .where(eq(sessionMessagesTable.id, FINAL_MESSAGE_ID)) + .run(); await database .app() @@ -673,6 +716,7 @@ describe("Public Thread API e2e", () => { errorCode: null, errorDetailsJson: null, errorMessage: null, + errorRetryable: null, status: "completed", updatedAt: 1_150, }) @@ -1165,39 +1209,138 @@ describe("Public Thread API e2e", () => { liveEvents.close(); await insertRuntimeEvent(database, { - kind: "message.completed", + kind: "message.added", occurredAt: 3_250, - payload: { messageId: "assistant-stream-live-1", role: "agent" }, + payload: { + content: "Live stream \uE200cite\uE202hidden\uE201delta A", + messageId: "assistant-stream-live-1", + role: "agent", + }, runId, seq: 6, sessionId: threadId, }); await insertRuntimeEvent(database, { - kind: "message.added", + kind: "message.completed", occurredAt: 3_300, + payload: { messageId: "assistant-stream-live-1", role: "agent" }, + runId, + seq: 7, + sessionId: threadId, + }); + await insertRuntimeEvent(database, { + kind: "message.delta", + occurredAt: 3_350, payload: { - content: "Live stream \uE200cite\uE202hidden\uE201delta A", - messageId: "assistant-stream-live-1", + contentDelta: "Same poll ", + messageId: "assistant-stream-same-poll", role: "agent", }, runId, - seq: 7, + seq: 8, sessionId: threadId, }); await insertRuntimeEvent(database, { kind: "message.added", - occurredAt: 3_350, + occurredAt: 3_400, + payload: { + content: "Same poll suffix", + messageId: "assistant-stream-same-poll", + role: "agent", + }, + runId, + seq: 9, + sessionId: threadId, + }); + await insertRuntimeEvent(database, { + kind: "message.completed", + occurredAt: 3_450, + payload: { messageId: "assistant-stream-same-poll", role: "agent" }, + runId, + seq: 10, + sessionId: threadId, + }); + const noDeltaTerminals = [ + ["message.completed", { messageId: "no-delta-completed", role: "agent" }], + ["message.cancelled", { messageId: "no-delta-cancelled", role: "agent" }], + [ + "message.failed", + { + error: { code: "runtime.failed", message: "Runtime failed." }, + messageId: "no-delta-failed", + role: "agent", + }, + ], + ] as const; + + for (const [index, [kind, payload]] of noDeltaTerminals.entries()) { + await insertRuntimeEvent(database, { + kind, + occurredAt: 3_550 + index * 50, + payload, + runId, + seq: 12 + index, + sessionId: threadId, + }); + } + const streamedTerminals = [ + [ + "message.cancelled", + "Cancelled stream \uE200cite\uE202hidden-cancelled\uE201", + { messageId: "assistant-stream-cancelled", role: "agent" }, + ], + [ + "message.failed", + "Failed stream \uE200cite\uE202hidden-failed\uE201", + { + error: { code: "runtime.failed", message: "Runtime failed." }, + messageId: "assistant-stream-failed", + role: "agent", + }, + ], + ] as const; + + for (const [index, [kind, contentDelta, payload]] of streamedTerminals.entries()) { + const seq = 15 + index * 2; + await insertRuntimeEvent(database, { + kind: "message.delta", + occurredAt: 3_700 + index * 100, + payload: { contentDelta, messageId: payload.messageId, role: "agent" }, + runId, + seq, + sessionId: threadId, + }); + await insertRuntimeEvent(database, { + kind, + occurredAt: 3_750 + index * 100, + payload, + runId, + seq: seq + 1, + sessionId: threadId, + }); + } + await insertRuntimeEvent(database, { + kind: "message.added", + occurredAt: 3_900, payload: { content: "Tail marker", messageId: "tail-message", role: "agent", }, runId, - seq: 8, + seq: 19, + sessionId: threadId, + }); + await insertRuntimeEvent(database, { + kind: "message.completed", + occurredAt: 3_950, + payload: { messageId: "tail-message", role: "agent" }, + runId, + seq: 20, sessionId: threadId, }); - const text = `${openDeltaText}${await readUntil("id: 01J00000000000000000000017")}`; + const text = `${openDeltaText}${await readUntil("id: 01J0000000000000000000001K")}`; await reader.cancel(); expect(text).toContain("event: thread.event"); expect(text).toContain("id: 01J00000000000000000000011"); @@ -1205,13 +1348,35 @@ describe("Public Thread API e2e", () => { expect(text).not.toContain("id: 01J00000000000000000000013"); expect(text).toContain("id: 01J00000000000000000000014"); expect(text).not.toContain("id: 01J00000000000000000000015"); - expect(text).not.toContain("id: 01J00000000000000000000016"); + expect(text).toContain("id: 01J00000000000000000000016"); expect(text).toContain("id: 01J00000000000000000000017"); + expect(text).not.toContain("id: 01J00000000000000000000018"); + expect(text).toContain("id: 01J00000000000000000000019"); + expect(text).not.toContain("id: 01J0000000000000000000001A"); + expect(text).toContain("id: 01J0000000000000000000001B"); + expect(text).toContain("id: 01J0000000000000000000001C"); + expect(text).toContain("id: 01J0000000000000000000001D"); + expect(text).not.toContain("id: 01J0000000000000000000001E"); + expect(text).toContain("id: 01J0000000000000000000001F"); + expect(text).not.toContain("id: 01J0000000000000000000001G"); + expect(text).toContain("id: 01J0000000000000000000001H"); + expect(text).not.toContain("id: 01J0000000000000000000001J"); + expect(text).toContain("id: 01J0000000000000000000001K"); expect(text.match(/Live stream /gu)).toHaveLength(1); expect(text.match(/delta A/gu)).toHaveLength(1); + expect(text.match(/Same poll /gu)).toHaveLength(1); + expect(text.match(/suffix/gu)).toHaveLength(1); + expect(text.match(/Cancelled stream /gu)).toHaveLength(1); + expect(text.match(/Failed stream /gu)).toHaveLength(1); + expect(text).not.toContain("hidden-cancelled"); + expect(text).not.toContain("hidden-failed"); + expect(text).not.toContain("Message updated."); + expect(text).not.toContain("Runtime failed."); expect(text).toContain('"type":"agent.message.delta"'); + expect(text).toContain('"status":"error"'); + expect(text).toMatch(/id: 01J0000000000000000000001H\ndata: [^\n]*"status":"error"/u); expect(text).toContain('"toolCallId":"tool-call-stream-1"'); - expect(text).not.toContain('"toolInput"'); + expect(text).toContain('"toolInput":{"calories":420,"mealId":"meal-1"}'); expect(text).toContain('"toolName":"record_meal"'); expect(text).toContain('"runId":null'); expect(text).toContain('"content":"'); @@ -1220,9 +1385,2204 @@ describe("Public Thread API e2e", () => { expect(text).not.toContain("private-diagnostic"); expect(text).not.toContain("traceId"); expect(text).not.toContain("event: thread.error"); + + const replayResponse = await requestPublicApi( + app, + database, + new Request(`https://api.example.com/api/v1/threads/${threadId}/events?limit=100`, { + headers: { Authorization: bearer(TOKENS.owner) }, + }), + ); + const replayEvents = expectArray((await readJson(replayResponse))["events"]).map( + expectRecord, + ); + + for (const id of [ + "01J00000000000000000000016", + "01J00000000000000000000019", + "01J0000000000000000000001B", + "01J0000000000000000000001C", + "01J0000000000000000000001D", + "01J0000000000000000000001F", + "01J0000000000000000000001H", + "01J0000000000000000000001K", + ]) { + expect(replayEvents.some((event) => event["id"] === id)).toBe(true); + } + for (const id of ["01J0000000000000000000001D", "01J0000000000000000000001H"]) { + expect(replayEvents.find((event) => event["id"] === id)).toMatchObject({ + status: "error", + }); + } }); }, 10_000); + test.each([ + { + canonicalText: "Hello world", + liveText: "Hello ", + provider: "OpenAI", + snapshotContent: "Hello ", + threadIndex: 250, + }, + { + canonicalText: "Hello world", + liveText: "Hello ", + provider: "ACP", + snapshotContent: "Hello ", + threadIndex: 251, + }, + { + canonicalText: "Hello world", + liveText: "Hello ", + provider: "Claude", + snapshotContent: [{ text: "Hello ", type: "text" }], + threadIndex: 252, + }, + { + canonicalText: "Final world", + liveText: "Draft ", + provider: "divergent", + snapshotContent: "Final ", + threadIndex: 253, + }, + ] as const)( + "projects $provider snapshot-before-terminal order without replaying live text", + async ({ canonicalText, liveText, provider, snapshotContent, threadIndex }) => { + const database = await createPublicHttpContractDatabase(); + const app = createPublicThreadApiTestApp(); + const liveEvents = createPublicEventSessionNamespace(); + const threadId = generatedPublicThreadId(threadIndex); + const messageId = `snapshot-before-terminal-${provider.toLowerCase()}`; + + await insertPublicThread(database, { + id: threadId, + title: `${provider} snapshot order`, + updatedAt: 1_000, + }); + await insertRuntimeEvent(database, { + kind: "message.started", + occurredAt: 1_100, + payload: { messageId, role: "agent" }, + seq: 1, + sessionId: threadId, + }); + await insertRuntimeEvent(database, { + kind: "message.delta", + occurredAt: 1_200, + payload: { contentDelta: liveText, messageId, role: "agent" }, + seq: 2, + sessionId: threadId, + }); + + const response = await requestPublicApi( + app, + database, + new Request(`https://api.example.com/api/v1/threads/${threadId}/events/stream`, { + headers: { Authorization: bearer(TOKENS.owner) }, + }), + { sessionNamespace: liveEvents.binding }, + ); + const reader = response.body?.getReader(); + if (!reader) { + throw new Error("Expected stream response body."); + } + + const decoder = new TextDecoder(); + let sseText = ""; + const readUntil = async (marker: string): Promise => { + while (!sseText.includes(marker)) { + const chunk = await Promise.race([ + reader.read(), + Bun.sleep(3_000).then(() => { + throw new Error(`Timed out waiting for ${marker}.`); + }), + ]); + + if (chunk.done) { + throw new Error(`SSE closed before ${marker}.`); + } + sseText += decoder.decode(chunk.value, { stream: true }); + } + }; + + await readUntil("id: 01J00000000000000000000011"); + await insertRuntimeEvent(database, { + kind: "message.added", + occurredAt: 1_300, + payload: { content: snapshotContent, messageId, role: "agent" }, + seq: 3, + sessionId: threadId, + }); + await insertRuntimeEvent(database, { + kind: "message.delta", + occurredAt: 1_400, + payload: { contentDelta: "world", messageId, role: "agent" }, + seq: 4, + sessionId: threadId, + }); + await insertRuntimeEvent(database, { + kind: "message.completed", + occurredAt: 1_500, + payload: { messageId, role: "agent" }, + seq: 5, + sessionId: threadId, + }); + liveEvents.emit(); + await readUntil("id: 01J00000000000000000000014"); + await reader.cancel(); + liveEvents.close(); + + const liveContent = sseText + .split("\n") + .filter((line) => line.startsWith("data: ")) + .map((line) => expectRecord(JSON.parse(line.slice("data: ".length)))["content"]) + .filter((content): content is string => typeof content === "string") + .join(""); + const replayResponse = await requestPublicApi( + app, + database, + new Request(`https://api.example.com/api/v1/threads/${threadId}/events`, { + headers: { Authorization: bearer(TOKENS.owner) }, + }), + ); + const replayContent = expectArray((await readJson(replayResponse))["events"]) + .map((event) => expectRecord(event)["content"]) + .filter((content): content is string => typeof content === "string") + .join(""); + + expect(replayContent).toBe(canonicalText); + expect(liveContent).toBe(provider === "divergent" ? liveText : canonicalText); + }, + 10_000, + ); + + test("reconciles a paginated authoritative snapshot above the former payload limit", async () => { + const database = await createPublicHttpContractDatabase(); + const app = createPublicThreadApiTestApp(); + const liveEvents = createPublicEventSessionNamespace(); + const threadId = generatedPublicThreadId(270); + const messageId = "paginated-authoritative-snapshot"; + const eventId = (seq: number) => generatedPublicThreadId(18_000 + seq); + const fragments = Array.from({ length: 1_001 }, (_, index) => + index === 1_000 ? "🙂终" : "x".repeat(400), + ); + + await insertPublicThread(database, { + id: threadId, + title: "Paginated authoritative snapshot", + updatedAt: 1_000, + }); + const response = await requestPublicApi( + app, + database, + new Request(`https://api.example.com/api/v1/threads/${threadId}/events/stream`, { + headers: { Authorization: bearer(TOKENS.owner) }, + }), + { sessionNamespace: liveEvents.binding }, + ); + const reader = response.body?.getReader(); + if (!reader) { + throw new Error("Expected stream response body."); + } + await reader.read(); + + await insertRuntimeEvent(database, { + eventId: eventId(1), + kind: "message.started", + occurredAt: 1_000, + payload: { messageId, role: "agent" }, + seq: 1, + sessionId: threadId, + }); + for (const [index, content] of fragments.entries()) { + const seq = index + 2; + await insertRuntimeEvent(database, { + eventId: eventId(seq), + kind: index === 0 ? "message.added" : "message.delta", + occurredAt: seq * 1_000, + payload: + index === 0 + ? { content, messageId, role: "agent" } + : { contentDelta: content, messageId, role: "agent" }, + seq, + sessionId: threadId, + }); + } + const terminalSeq = fragments.length + 2; + const terminalEventId = eventId(terminalSeq); + await insertRuntimeEvent(database, { + eventId: terminalEventId, + kind: "message.completed", + occurredAt: terminalSeq * 1_000, + payload: { messageId, role: "agent" }, + seq: terminalSeq, + sessionId: threadId, + }); + liveEvents.emit(); + + const decoder = new TextDecoder(); + let sseText = ""; + while (!sseText.includes(`id: ${terminalEventId}`)) { + const chunk = await Promise.race([ + reader.read(), + Bun.sleep(8_000).then(() => { + throw new Error("Timed out waiting for the paginated authoritative snapshot."); + }), + ]); + if (chunk.done) { + throw new Error("SSE closed before the paginated authoritative snapshot."); + } + sseText += decoder.decode(chunk.value, { stream: true }); + } + await reader.cancel(); + liveEvents.close(); + + const liveMessages = sseText + .split("\n") + .filter((line) => line.startsWith("data: ")) + .map((line) => expectRecord(JSON.parse(line.slice("data: ".length)))) + .filter((event) => event["type"] === "agent.message.delta"); + const replayResponse = await requestPublicApi( + app, + database, + new Request(`https://api.example.com/api/v1/threads/${threadId}/events`, { + headers: { Authorization: bearer(TOKENS.owner) }, + }), + ); + const replayMessages = expectArray((await readJson(replayResponse))["events"]) + .map(expectRecord) + .filter((event) => event["type"] === "agent.message.delta"); + + expect(fragments.join("").length).toBeGreaterThan(384 * 1024); + expect(liveMessages).toEqual(replayMessages); + expect(liveMessages).toEqual([ + expect.objectContaining({ content: fragments.join(""), id: terminalEventId }), + ]); + }, 15_000); + + test("retains exact prefix metadata for more than one public event page of messages", async () => { + const database = await createPublicHttpContractDatabase(); + const app = createPublicThreadApiTestApp(); + const liveEvents = createPublicEventSessionNamespace(); + const threadId = generatedPublicThreadId(271); + const runId = PUBLIC_API_TEST_IDS.run; + const messageId = "message-before-full-state-page"; + const eventId = (seq: number) => generatedPublicThreadId(20_000 + seq); + + await insertPublicThread(database, { + id: threadId, + title: "Unbounded active message metadata", + updatedAt: 1_000, + }); + const response = await requestPublicApi( + app, + database, + new Request(`https://api.example.com/api/v1/threads/${threadId}/events/stream`, { + headers: { Authorization: bearer(TOKENS.owner) }, + }), + { sessionNamespace: liveEvents.binding }, + ); + const reader = response.body?.getReader(); + if (!reader) { + throw new Error("Expected stream response body."); + } + const decoder = new TextDecoder(); + let sseText = ""; + const readUntil = async (marker: string): Promise => { + while (!sseText.includes(marker)) { + const chunk = await Promise.race([ + reader.read(), + Bun.sleep(5_000).then(() => { + throw new Error(`Timed out waiting for ${marker}.`); + }), + ]); + if (chunk.done) { + throw new Error(`SSE closed before ${marker}.`); + } + sseText += decoder.decode(chunk.value, { stream: true }); + } + }; + + await readUntil(": connected"); + await insertRuntimeEvent(database, { + eventId: eventId(1), + kind: "run.started", + occurredAt: 1_000, + payload: { startedAt: "1970-01-01T00:00:01.000Z" }, + runId, + seq: 1, + sessionId: threadId, + }); + await insertRuntimeEvent(database, { + eventId: eventId(2), + kind: "message.started", + occurredAt: 2_000, + payload: { messageId, role: "agent" }, + runId, + seq: 2, + sessionId: threadId, + }); + await insertRuntimeEvent(database, { + eventId: eventId(3), + kind: "message.delta", + occurredAt: 3_000, + payload: { contentDelta: "prefix", messageId, role: "agent" }, + runId, + seq: 3, + sessionId: threadId, + }); + liveEvents.emit(); + await readUntil(`id: ${eventId(3)}`); + + for (let seq = 4; seq <= 1_004; seq += 1) { + await insertRuntimeEvent(database, { + eventId: eventId(seq), + kind: "message.started", + occurredAt: seq * 1_000, + payload: { messageId: `parallel-message-${seq}`, role: "agent" }, + runId, + seq, + sessionId: threadId, + }); + } + await insertRuntimeEvent(database, { + eventId: eventId(1_005), + kind: "tool.call.updated", + occurredAt: 1_005_000, + payload: { + rawInput: "{}", + status: "running", + title: "state_page_marker", + toolCallId: "state-page-marker", + }, + runId, + seq: 1_005, + sessionId: threadId, + }); + liveEvents.emit(); + await readUntil(`id: ${eventId(1_005)}`); + + await insertRuntimeEvent(database, { + eventId: eventId(1_006), + kind: "message.added", + occurredAt: 1_006_000, + payload: { content: "prefix", messageId, role: "agent" }, + runId, + seq: 1_006, + sessionId: threadId, + }); + await insertRuntimeEvent(database, { + eventId: eventId(1_007), + kind: "message.delta", + occurredAt: 1_007_000, + payload: { contentDelta: " suffix", messageId, role: "agent" }, + runId, + seq: 1_007, + sessionId: threadId, + }); + await insertRuntimeEvent(database, { + eventId: eventId(1_008), + kind: "message.completed", + occurredAt: 1_008_000, + payload: { messageId, role: "agent" }, + runId, + seq: 1_008, + sessionId: threadId, + }); + liveEvents.emit(); + await readUntil(`id: ${eventId(1_008)}`); + await reader.cancel(); + liveEvents.close(); + + const liveContent = sseText + .split("\n") + .filter((line) => line.startsWith("data: ")) + .map((line) => expectRecord(JSON.parse(line.slice("data: ".length)))) + .filter((event) => event["type"] === "agent.message.delta") + .map((event) => event["content"]) + .filter((content): content is string => typeof content === "string") + .join(""); + + expect(liveContent).toBe("prefix suffix"); + }, 15_000); + + test("replaces standalone assistant snapshots before publishing the terminal text", async () => { + const database = await createPublicHttpContractDatabase(); + const app = createPublicThreadApiTestApp(); + const liveEvents = createPublicEventSessionNamespace(); + const threadId = generatedPublicThreadId(258); + const messageId = "standalone-authoritative-snapshot"; + const eventId = (seq: number) => generatedPublicThreadId(9_000 + seq); + + await insertPublicThread(database, { + id: threadId, + title: "Standalone authoritative snapshot", + updatedAt: 1_000, + }); + + const response = await requestPublicApi( + app, + database, + new Request(`https://api.example.com/api/v1/threads/${threadId}/events/stream`, { + headers: { Authorization: bearer(TOKENS.owner) }, + }), + { sessionNamespace: liveEvents.binding }, + ); + const reader = response.body?.getReader(); + if (!reader) { + throw new Error("Expected stream response body."); + } + + await insertRuntimeEvent(database, { + eventId: eventId(1), + kind: "message.added", + occurredAt: 1_100, + payload: { content: "Obsolete snapshot", messageId, role: "agent" }, + seq: 1, + sessionId: threadId, + }); + await insertRuntimeEvent(database, { + eventId: eventId(2), + kind: "message.added", + occurredAt: 1_200, + payload: { content: "Final \uE200ci", messageId, role: "agent" }, + seq: 2, + sessionId: threadId, + }); + await insertRuntimeEvent(database, { + eventId: eventId(3), + kind: "message.delta", + occurredAt: 1_300, + payload: { contentDelta: "te\uE202private\uE201world", messageId, role: "agent" }, + seq: 3, + sessionId: threadId, + }); + const terminalEventId = eventId(4); + await insertRuntimeEvent(database, { + eventId: terminalEventId, + kind: "message.completed", + occurredAt: 1_400, + payload: { messageId, role: "agent" }, + seq: 4, + sessionId: threadId, + }); + liveEvents.emit(); + + const decoder = new TextDecoder(); + let sseText = ""; + + while (!sseText.includes(`id: ${terminalEventId}`)) { + const chunk = await Promise.race([ + reader.read(), + Bun.sleep(3_000).then(() => { + throw new Error("Timed out waiting for the terminal assistant snapshot."); + }), + ]); + + if (chunk.done) { + throw new Error("SSE closed before the terminal assistant snapshot."); + } + sseText += decoder.decode(chunk.value, { stream: true }); + } + + await reader.cancel(); + liveEvents.close(); + const liveContent = sseText + .split("\n") + .filter((line) => line.startsWith("data: ")) + .map((line) => expectRecord(JSON.parse(line.slice("data: ".length)))["content"]) + .filter((content): content is string => typeof content === "string") + .join(""); + const replayResponse = await requestPublicApi( + app, + database, + new Request(`https://api.example.com/api/v1/threads/${threadId}/events`, { + headers: { Authorization: bearer(TOKENS.owner) }, + }), + ); + const replayContent = expectArray((await readJson(replayResponse))["events"]) + .map((event) => expectRecord(event)["content"]) + .filter((content): content is string => typeof content === "string") + .join(""); + + expect(liveContent).toBe("Final world"); + expect(replayContent).toBe(liveContent); + expect(sseText).not.toContain("Obsolete snapshot"); + expect(sseText).not.toContain("private"); + }, 10_000); + + test.each([ + { + deltaContent: "world", + snapshotContent: "Final ", + terminalKind: "message.completed", + terminalBeforeConnect: true, + threadIndex: 254, + timing: "after the terminal", + }, + { + deltaContent: "world", + snapshotContent: "Final ", + terminalKind: "message.completed", + terminalBeforeConnect: false, + threadIndex: 255, + timing: "between the snapshot and terminal", + }, + { + deltaContent: "world", + snapshotContent: "Final ", + terminalKind: "run.failed", + terminalBeforeConnect: false, + threadIndex: 259, + timing: "before a repaired run terminal", + }, + { + deltaContent: "te\uE202private\uE201world", + snapshotContent: "Final \uE200ci", + terminalKind: "message.completed", + terminalBeforeConnect: false, + threadIndex: 260, + timing: "across a private citation chunk boundary", + }, + ] as const)( + "hydrates canonical snapshot text when connecting $timing", + async ({ deltaContent, snapshotContent, terminalBeforeConnect, terminalKind, threadIndex }) => { + const database = await createPublicHttpContractDatabase(); + const app = createPublicThreadApiTestApp(); + const liveEvents = createPublicEventSessionNamespace(); + const threadId = generatedPublicThreadId(threadIndex); + const messageId = `initial-canonical-${String(threadIndex)}`; + const runId = terminalKind === "run.failed" ? PUBLIC_API_TEST_IDS.run : null; + + await insertPublicThread(database, { + id: threadId, + title: "Initial canonical snapshot", + updatedAt: 1_000, + }); + await insertRuntimeEvent(database, { + kind: "message.started", + occurredAt: 1_100, + payload: { messageId, role: "agent" }, + runId, + seq: 1, + sessionId: threadId, + }); + await insertRuntimeEvent(database, { + kind: "message.delta", + occurredAt: 1_200, + payload: { contentDelta: "Draft ", messageId, role: "agent" }, + runId, + seq: 2, + sessionId: threadId, + }); + await insertRuntimeEvent(database, { + kind: "message.added", + occurredAt: 1_300, + payload: { content: snapshotContent, messageId, role: "agent" }, + runId, + seq: 3, + sessionId: threadId, + }); + + const completeSnapshot = async () => { + await insertRuntimeEvent(database, { + kind: "message.delta", + occurredAt: 1_400, + payload: { contentDelta: deltaContent, messageId, role: "agent" }, + runId, + seq: 4, + sessionId: threadId, + }); + await insertRuntimeEvent(database, { + kind: terminalKind, + occurredAt: 1_500, + payload: + terminalKind === "message.completed" + ? { messageId, role: "agent" } + : { + error: { + code: "runtime.driver_terminal", + details: {}, + message: "Driver disconnected.", + retryable: true, + }, + recoverable: true, + }, + runId, + seq: 5, + sessionId: threadId, + }); + }; + + if (terminalBeforeConnect) { + await completeSnapshot(); + } + + const response = await requestPublicApi( + app, + database, + new Request(`https://api.example.com/api/v1/threads/${threadId}/events/stream`, { + headers: { Authorization: bearer(TOKENS.owner) }, + }), + { sessionNamespace: liveEvents.binding }, + ); + const reader = response.body?.getReader(); + if (!reader) { + throw new Error("Expected stream response body."); + } + + const decoder = new TextDecoder(); + let sseText = ""; + const readUntil = async (marker: string): Promise => { + while (!sseText.includes(marker)) { + const chunk = await Promise.race([ + reader.read(), + Bun.sleep(3_000).then(() => { + throw new Error(`Timed out waiting for ${marker}.`); + }), + ]); + + if (chunk.done) { + throw new Error(`SSE closed before ${marker}.`); + } + sseText += decoder.decode(chunk.value, { stream: true }); + } + }; + + await readUntil( + terminalBeforeConnect ? "id: 01J00000000000000000000014" : "id: 01J00000000000000000000012", + ); + if (!terminalBeforeConnect) { + await completeSnapshot(); + liveEvents.emit(); + await readUntil("id: 01J00000000000000000000014"); + } + await reader.cancel(); + liveEvents.close(); + + const liveContent = sseText + .split("\n") + .filter((line) => line.startsWith("data: ")) + .map((line) => expectRecord(JSON.parse(line.slice("data: ".length)))) + .filter((event) => event["type"] === "agent.message.delta") + .map((event) => event["content"]) + .filter((content): content is string => typeof content === "string") + .join(""); + const replayResponse = await requestPublicApi( + app, + database, + new Request(`https://api.example.com/api/v1/threads/${threadId}/events`, { + headers: { Authorization: bearer(TOKENS.owner) }, + }), + ); + const replayContent = expectArray((await readJson(replayResponse))["events"]) + .map(expectRecord) + .filter((event) => event["type"] === "agent.message.delta") + .map((event) => event["content"]) + .filter((content): content is string => typeof content === "string") + .join(""); + + expect(liveContent).toBe("Final world"); + expect(replayContent).toBe(liveContent); + expect(liveContent).not.toContain("Draft"); + expect(sseText).not.toContain("private"); + expect(sseText).not.toContain("\uE200"); + }, + 10_000, + ); + + test("keeps a citation parser alive across a repaired run terminal", async () => { + const database = await createPublicHttpContractDatabase(); + const app = createPublicThreadApiTestApp(); + const liveEvents = createPublicEventSessionNamespace(); + const threadId = generatedPublicThreadId(262); + const messageId = "terminal-citation-recovery"; + const runId = PUBLIC_API_TEST_IDS.run; + const eventId = (seq: number) => generatedPublicThreadId(12_000 + seq); + + await insertPublicThread(database, { + id: threadId, + title: "Terminal citation recovery", + updatedAt: 1_000, + }); + const response = await requestPublicApi( + app, + database, + new Request(`https://api.example.com/api/v1/threads/${threadId}/events/stream`, { + headers: { Authorization: bearer(TOKENS.owner) }, + }), + { sessionNamespace: liveEvents.binding }, + ); + const reader = response.body?.getReader(); + if (!reader) { + throw new Error("Expected stream response body."); + } + + const decoder = new TextDecoder(); + let sseText = ""; + const readUntil = async (marker: string): Promise => { + while (!sseText.includes(marker)) { + const chunk = await Promise.race([ + reader.read(), + Bun.sleep(3_000).then(() => { + throw new Error(`Timed out waiting for ${marker}.`); + }), + ]); + if (chunk.done) { + throw new Error(`SSE closed before ${marker}.`); + } + sseText += decoder.decode(chunk.value, { stream: true }); + } + }; + + await readUntil(": connected"); + await insertRuntimeEvent(database, { + eventId: eventId(1), + kind: "message.started", + occurredAt: 1_100, + payload: { messageId, role: "agent" }, + runId, + seq: 1, + sessionId: threadId, + }); + await insertRuntimeEvent(database, { + eventId: eventId(2), + kind: "message.delta", + occurredAt: 1_200, + payload: { contentDelta: "before\uE200ci", messageId, role: "agent" }, + runId, + seq: 2, + sessionId: threadId, + }); + liveEvents.emit(); + await readUntil(`id: ${eventId(2)}`); + await insertRuntimeEvent(database, { + eventId: eventId(3), + kind: "run.failed", + occurredAt: 1_300, + payload: { + error: { + code: "runtime.driver_terminal", + details: {}, + message: "Driver disconnected.", + retryable: true, + }, + recoverable: true, + }, + runId, + seq: 3, + sessionId: threadId, + }); + liveEvents.emit(); + await readUntil(`id: ${eventId(3)}`); + await insertRuntimeEvent(database, { + eventId: eventId(4), + kind: "message.delta", + occurredAt: 1_250, + payload: { + contentDelta: "te\uE202SECRET\uE201after", + messageId, + role: "agent", + }, + runId, + seq: 4, + sessionId: threadId, + }); + liveEvents.emit(); + await readUntil(`id: ${eventId(4)}`); + await reader.cancel(); + liveEvents.close(); + + const liveMessageContent = sseText + .split("\n") + .filter((line) => line.startsWith("data: ")) + .map((line) => expectRecord(JSON.parse(line.slice("data: ".length)))) + .filter((event) => event["type"] === "agent.message.delta") + .map((event) => event["content"]) + .filter((content): content is string => typeof content === "string") + .join(""); + const replayResponse = await requestPublicApi( + app, + database, + new Request(`https://api.example.com/api/v1/threads/${threadId}/events`, { + headers: { Authorization: bearer(TOKENS.owner) }, + }), + ); + const replayEvents = expectArray((await readJson(replayResponse))["events"]).map(expectRecord); + const replayMessages = replayEvents.filter((event) => event["type"] === "agent.message.delta"); + + expect(liveMessageContent).toBe("beforeafter"); + expect(replayMessages).toHaveLength(1); + expect(replayMessages[0]?.["content"]).toBe(liveMessageContent); + expect(sseText).not.toContain("SECRET"); + expect(sseText).not.toContain("\uE200"); + }, 10_000); + + test("keeps one canonical stream when an authoritative snapshot arrives after repair", async () => { + const database = await createPublicHttpContractDatabase(); + const app = createPublicThreadApiTestApp(); + const liveEvents = createPublicEventSessionNamespace(); + const threadId = generatedPublicThreadId(263); + const messageId = "terminal-snapshot-recovery"; + const runId = PUBLIC_API_TEST_IDS.run; + const eventId = (seq: number) => generatedPublicThreadId(12_100 + seq); + + await insertPublicThread(database, { + id: threadId, + title: "Terminal snapshot recovery", + updatedAt: 1_000, + }); + const response = await requestPublicApi( + app, + database, + new Request(`https://api.example.com/api/v1/threads/${threadId}/events/stream`, { + headers: { Authorization: bearer(TOKENS.owner) }, + }), + { sessionNamespace: liveEvents.binding }, + ); + const reader = response.body?.getReader(); + if (!reader) { + throw new Error("Expected stream response body."); + } + + const decoder = new TextDecoder(); + let sseText = ""; + const readUntil = async (marker: string): Promise => { + while (!sseText.includes(marker)) { + const chunk = await Promise.race([ + reader.read(), + Bun.sleep(3_000).then(() => { + throw new Error(`Timed out waiting for ${marker}.`); + }), + ]); + if (chunk.done) { + throw new Error(`SSE closed before ${marker}.`); + } + sseText += decoder.decode(chunk.value, { stream: true }); + } + }; + + await readUntil(": connected"); + await insertRuntimeEvent(database, { + eventId: eventId(1), + kind: "message.started", + occurredAt: 1_100, + payload: { messageId, role: "agent" }, + runId, + seq: 1, + sessionId: threadId, + }); + await insertRuntimeEvent(database, { + eventId: eventId(2), + kind: "message.delta", + occurredAt: 1_200, + payload: { contentDelta: "Draft", messageId, role: "agent" }, + runId, + seq: 2, + sessionId: threadId, + }); + liveEvents.emit(); + await readUntil(`id: ${eventId(2)}`); + await insertRuntimeEvent(database, { + eventId: eventId(3), + kind: "run.failed", + occurredAt: 1_500, + payload: { + error: { + code: "runtime.driver_terminal", + details: {}, + message: "Driver disconnected.", + retryable: true, + }, + recoverable: true, + }, + runId, + seq: 3, + sessionId: threadId, + }); + liveEvents.emit(); + await readUntil(`id: ${eventId(3)}`); + await insertRuntimeEvent(database, { + eventId: eventId(4), + kind: "message.started", + occurredAt: 1_250, + payload: { messageId, role: "agent" }, + runId, + seq: 4, + sessionId: threadId, + }); + await insertRuntimeEvent(database, { + eventId: eventId(5), + kind: "message.added", + occurredAt: 1_300, + payload: { content: "Obsolete", messageId, role: "agent" }, + runId, + seq: 5, + sessionId: threadId, + }); + await insertRuntimeEvent(database, { + eventId: eventId(6), + kind: "message.added", + occurredAt: 1_350, + payload: { content: "Final ", messageId, role: "agent" }, + runId, + seq: 6, + sessionId: threadId, + }); + await insertRuntimeEvent(database, { + eventId: eventId(7), + kind: "message.delta", + occurredAt: 1_400, + payload: { contentDelta: "world", messageId, role: "agent" }, + runId, + seq: 7, + sessionId: threadId, + }); + await insertRuntimeEvent(database, { + eventId: eventId(8), + kind: "tool.call.updated", + occurredAt: 1_600, + payload: { + rawInput: "{}", + status: "running", + title: "recovery_marker", + toolCallId: "terminal-snapshot-marker", + }, + runId, + seq: 8, + sessionId: threadId, + }); + liveEvents.emit(); + await readUntil(`id: ${eventId(8)}`); + await reader.cancel(); + liveEvents.close(); + + const liveMessages = sseText + .split("\n") + .filter((line) => line.startsWith("data: ")) + .map((line) => expectRecord(JSON.parse(line.slice("data: ".length)))) + .filter((event) => event["type"] === "agent.message.delta"); + const replayResponse = await requestPublicApi( + app, + database, + new Request(`https://api.example.com/api/v1/threads/${threadId}/events`, { + headers: { Authorization: bearer(TOKENS.owner) }, + }), + ); + const replayMessages = expectArray((await readJson(replayResponse))["events"]) + .map(expectRecord) + .filter((event) => event["type"] === "agent.message.delta"); + + expect(liveMessages.map((event) => event["content"]).join("")).toBe("Draft"); + expect(replayMessages).toHaveLength(1); + expect(replayMessages[0]?.["content"]).toBe("Final world"); + expect(sseText).not.toContain("Obsolete"); + }, 10_000); + + test("keeps final stream identity stable when its message terminal arrives after repair", async () => { + const database = await createPublicHttpContractDatabase(); + const app = createPublicThreadApiTestApp(); + const liveEvents = createPublicEventSessionNamespace(); + const threadId = generatedPublicThreadId(268); + const messageId = "late-message-terminal"; + const runId = PUBLIC_API_TEST_IDS.run; + const eventId = (seq: number) => generatedPublicThreadId(16_000 + seq); + + await insertPublicThread(database, { + id: threadId, + title: "Late message terminal identity", + updatedAt: 1_000, + }); + const response = await requestPublicApi( + app, + database, + new Request(`https://api.example.com/api/v1/threads/${threadId}/events/stream`, { + headers: { Authorization: bearer(TOKENS.owner) }, + }), + { sessionNamespace: liveEvents.binding }, + ); + const reader = response.body?.getReader(); + if (!reader) { + throw new Error("Expected stream response body."); + } + const decoder = new TextDecoder(); + let sseText = ""; + const readUntil = async (marker: string): Promise => { + while (!sseText.includes(marker)) { + const chunk = await Promise.race([ + reader.read(), + Bun.sleep(3_000).then(() => { + throw new Error(`Timed out waiting for ${marker}.`); + }), + ]); + if (chunk.done) { + throw new Error(`SSE closed before ${marker}.`); + } + sseText += decoder.decode(chunk.value, { stream: true }); + } + }; + + await readUntil(": connected"); + await insertRuntimeEvent(database, { + eventId: eventId(1), + kind: "message.started", + occurredAt: 1_100, + payload: { messageId, role: "agent" }, + runId, + seq: 1, + sessionId: threadId, + }); + await insertRuntimeEvent(database, { + eventId: eventId(2), + kind: "message.added", + occurredAt: 1_200, + payload: { content: "Final ", messageId, role: "agent" }, + runId, + seq: 2, + sessionId: threadId, + }); + await insertRuntimeEvent(database, { + eventId: eventId(3), + kind: "message.delta", + occurredAt: 1_300, + payload: { contentDelta: "world", messageId, role: "agent" }, + runId, + seq: 3, + sessionId: threadId, + }); + await insertRuntimeEvent(database, { + eventId: eventId(4), + kind: "run.failed", + occurredAt: 1_400, + payload: { + error: { + code: "runtime.driver_terminal", + details: {}, + message: "Driver disconnected.", + retryable: true, + }, + recoverable: true, + }, + runId, + seq: 4, + sessionId: threadId, + }); + liveEvents.emit(); + await readUntil(`id: ${eventId(4)}`); + await insertRuntimeEvent(database, { + eventId: eventId(5), + kind: "message.completed", + occurredAt: 1_350, + payload: { messageId, role: "agent" }, + runId, + seq: 5, + sessionId: threadId, + }); + await insertRuntimeEvent(database, { + eventId: eventId(6), + kind: "tool.call.updated", + occurredAt: 1_500, + payload: { + rawInput: "{}", + status: "running", + title: "late_terminal_marker", + toolCallId: "late-terminal-marker", + }, + runId, + seq: 6, + sessionId: threadId, + }); + liveEvents.emit(); + await readUntil(`id: ${eventId(6)}`); + await reader.cancel(); + liveEvents.close(); + + const liveEventsBody = sseText + .split("\n") + .filter((line) => line.startsWith("data: ")) + .map((line) => expectRecord(JSON.parse(line.slice("data: ".length)))); + const replayResponse = await requestPublicApi( + app, + database, + new Request(`https://api.example.com/api/v1/threads/${threadId}/events`, { + headers: { Authorization: bearer(TOKENS.owner) }, + }), + ); + const replayEvents = expectArray((await readJson(replayResponse))["events"]).map(expectRecord); + const liveMessage = liveEventsBody.filter((event) => event["type"] === "agent.message.delta"); + const replayMessage = replayEvents.filter((event) => event["type"] === "agent.message.delta"); + + expect(liveMessage).toEqual(replayMessage); + expect(liveMessage).toEqual([ + expect.objectContaining({ content: "Final world", id: eventId(2) }), + ]); + expect(sseText).not.toContain(`id: ${eventId(5)}`); + }, 10_000); + + test("retains an undisplayed stream parser across the initial event limit", async () => { + const database = await createPublicHttpContractDatabase(); + const app = createPublicThreadApiTestApp(); + const liveEvents = createPublicEventSessionNamespace(); + const threadId = generatedPublicThreadId(264); + const messageId = "limited-citation-stream"; + const eventId = (seq: number) => generatedPublicThreadId(12_200 + seq); + + await insertPublicThread(database, { + id: threadId, + title: "Limited citation stream", + updatedAt: 1_000, + }); + await insertRuntimeEvent(database, { + eventId: eventId(1), + kind: "message.started", + occurredAt: 1_100, + payload: { messageId, role: "agent" }, + seq: 1, + sessionId: threadId, + }); + await insertRuntimeEvent(database, { + eventId: eventId(2), + kind: "message.delta", + occurredAt: 1_200, + payload: { contentDelta: "before\uE200ci", messageId, role: "agent" }, + seq: 2, + sessionId: threadId, + }); + await insertRuntimeEvent(database, { + eventId: eventId(3), + kind: "tool.call.updated", + occurredAt: 1_300, + payload: { + rawInput: "{}", + status: "running", + title: "limit_marker", + toolCallId: "limited-citation-marker", + }, + seq: 3, + sessionId: threadId, + }); + + const response = await requestPublicApi( + app, + database, + new Request(`https://api.example.com/api/v1/threads/${threadId}/events/stream?limit=1`, { + headers: { Authorization: bearer(TOKENS.owner) }, + }), + { sessionNamespace: liveEvents.binding }, + ); + const reader = response.body?.getReader(); + if (!reader) { + throw new Error("Expected stream response body."); + } + + const decoder = new TextDecoder(); + let sseText = ""; + const readUntil = async (marker: string): Promise => { + while (!sseText.includes(marker)) { + const chunk = await Promise.race([ + reader.read(), + Bun.sleep(3_000).then(() => { + throw new Error(`Timed out waiting for ${marker}.`); + }), + ]); + if (chunk.done) { + throw new Error(`SSE closed before ${marker}.`); + } + sseText += decoder.decode(chunk.value, { stream: true }); + } + }; + + await readUntil(`id: ${eventId(3)}`); + await insertRuntimeEvent(database, { + eventId: eventId(4), + kind: "message.delta", + occurredAt: 1_400, + payload: { + contentDelta: "te\uE202SECRET\uE201after", + messageId, + role: "agent", + }, + seq: 4, + sessionId: threadId, + }); + liveEvents.emit(); + await readUntil(`id: ${eventId(4)}`); + await reader.cancel(); + liveEvents.close(); + + expect(sseText).toContain('"content":"after"'); + expect(sseText).not.toContain("SECRET"); + expect(sseText).not.toContain("\uE200"); + }, 10_000); + + test("fails closed for an old stream outside the initial scan", async () => { + const database = await createPublicHttpContractDatabase(); + const app = createPublicThreadApiTestApp(); + const liveEvents = createPublicEventSessionNamespace(); + const threadId = generatedPublicThreadId(265); + const messageId = "unscanned-citation-stream"; + const eventId = (seq: number) => generatedPublicThreadId(14_000 + seq); + + await insertPublicThread(database, { + id: threadId, + title: "Unscanned citation stream", + updatedAt: 1_000, + }); + await insertRuntimeEvent(database, { + eventId: eventId(1), + kind: "message.started", + occurredAt: 1_000, + payload: { messageId, role: "agent" }, + seq: 1, + sessionId: threadId, + }); + await insertRuntimeEvent(database, { + eventId: eventId(2), + kind: "message.delta", + occurredAt: 2_000, + payload: { contentDelta: "before\uE200ci", messageId, role: "agent" }, + seq: 2, + sessionId: threadId, + }); + + for (let seq = 3; seq <= 1_003; seq += 1) { + await insertRuntimeEvent(database, { + eventId: eventId(seq), + kind: "tool.call.updated", + occurredAt: seq * 1_000, + payload: { + rawInput: "{}", + status: "running", + title: "scan_filler", + toolCallId: `scan-filler-${seq}`, + }, + seq, + sessionId: threadId, + }); + } + + const response = await requestPublicApi( + app, + database, + new Request(`https://api.example.com/api/v1/threads/${threadId}/events/stream?limit=1`, { + headers: { Authorization: bearer(TOKENS.owner) }, + }), + { sessionNamespace: liveEvents.binding }, + ); + const reader = response.body?.getReader(); + if (!reader) { + throw new Error("Expected stream response body."); + } + const decoder = new TextDecoder(); + let sseText = ""; + const readUntil = async (marker: string): Promise => { + while (!sseText.includes(marker)) { + const chunk = await Promise.race([ + reader.read(), + Bun.sleep(3_000).then(() => { + throw new Error(`Timed out waiting for ${marker}.`); + }), + ]); + if (chunk.done) { + throw new Error(`SSE closed before ${marker}.`); + } + sseText += decoder.decode(chunk.value, { stream: true }); + } + }; + + await readUntil(`id: ${eventId(1_003)}`); + await insertRuntimeEvent(database, { + eventId: eventId(1_004), + kind: "message.delta", + occurredAt: 1_004_000, + payload: { + contentDelta: "te\uE202SECRET\uE201after", + messageId, + role: "agent", + }, + seq: 1_004, + sessionId: threadId, + }); + await insertRuntimeEvent(database, { + eventId: eventId(1_005), + kind: "tool.call.updated", + occurredAt: 1_005_000, + payload: { + rawInput: "{}", + status: "running", + title: "post_scan_marker", + toolCallId: "post-scan-marker", + }, + seq: 1_005, + sessionId: threadId, + }); + liveEvents.emit(); + await readUntil(`id: ${eventId(1_005)}`); + await reader.cancel(); + liveEvents.close(); + + expect(sseText).not.toContain("SECRET"); + expect(sseText).not.toContain("\uE200"); + expect(sseText).not.toContain(`id: ${eventId(1_004)}`); + }, 10_000); + + test("trusts a delta parser when an exact full page reaches the database start", async () => { + const database = await createPublicHttpContractDatabase(); + const app = createPublicThreadApiTestApp(); + const liveEvents = createPublicEventSessionNamespace(); + const threadId = generatedPublicThreadId(269); + const messageId = "exact-page-citation-stream"; + const eventId = (seq: number) => generatedPublicThreadId(17_000 + seq); + + await insertPublicThread(database, { + id: threadId, + title: "Exact page citation stream", + updatedAt: 1_000, + }); + await insertRuntimeEvent(database, { + eventId: eventId(1), + kind: "message.delta", + occurredAt: 1_000, + payload: { contentDelta: "before\uE200ci", messageId, role: "agent" }, + seq: 1, + sessionId: threadId, + }); + + for (let seq = 2; seq <= 1_000; seq += 1) { + await insertRuntimeEvent(database, { + eventId: eventId(seq), + kind: "tool.call.updated", + occurredAt: seq * 1_000, + payload: { + rawInput: "{}", + status: "running", + title: "page_filler", + toolCallId: `page-filler-${seq}`, + }, + seq, + sessionId: threadId, + }); + } + + const response = await requestPublicApi( + app, + database, + new Request(`https://api.example.com/api/v1/threads/${threadId}/events/stream?limit=1`, { + headers: { Authorization: bearer(TOKENS.owner) }, + }), + { sessionNamespace: liveEvents.binding }, + ); + const reader = response.body?.getReader(); + if (!reader) { + throw new Error("Expected stream response body."); + } + const decoder = new TextDecoder(); + let sseText = ""; + const readUntil = async (marker: string): Promise => { + while (!sseText.includes(marker)) { + const chunk = await Promise.race([ + reader.read(), + Bun.sleep(3_000).then(() => { + throw new Error(`Timed out waiting for ${marker}.`); + }), + ]); + if (chunk.done) { + throw new Error(`SSE closed before ${marker}.`); + } + sseText += decoder.decode(chunk.value, { stream: true }); + } + }; + + await readUntil(`id: ${eventId(1_000)}`); + await insertRuntimeEvent(database, { + eventId: eventId(1_001), + kind: "message.delta", + occurredAt: 1_001_000, + payload: { + contentDelta: "te\uE202SECRET\uE201after", + messageId, + role: "agent", + }, + seq: 1_001, + sessionId: threadId, + }); + liveEvents.emit(); + await readUntil(`id: ${eventId(1_001)}`); + await reader.cancel(); + liveEvents.close(); + + expect(sseText).toContain('"content":"after"'); + expect(sseText).not.toContain("SECRET"); + expect(sseText).not.toContain("\uE200"); + }, 10_000); + + test("does not revive an omitted terminal snapshot on a later event", async () => { + const database = await createPublicHttpContractDatabase(); + const app = createPublicThreadApiTestApp(); + const liveEvents = createPublicEventSessionNamespace(); + const threadId = generatedPublicThreadId(266); + const messageId = "omitted-terminal-snapshot"; + const runId = PUBLIC_API_TEST_IDS.run; + const eventId = (seq: number) => generatedPublicThreadId(15_100 + seq); + const failure = { + error: { + code: "runtime.driver_terminal", + details: {}, + message: "Driver disconnected.", + retryable: true, + }, + recoverable: true, + }; + + await insertPublicThread(database, { + id: threadId, + title: "Omitted terminal snapshot", + updatedAt: 1_000, + }); + await insertRuntimeEvent(database, { + eventId: eventId(1), + kind: "message.started", + occurredAt: 1_100, + payload: { messageId, role: "agent" }, + runId, + seq: 1, + sessionId: threadId, + }); + await insertRuntimeEvent(database, { + eventId: eventId(2), + kind: "message.added", + occurredAt: 1_200, + payload: { content: "OLD OMITTED", messageId, role: "agent" }, + runId, + seq: 2, + sessionId: threadId, + }); + await insertRuntimeEvent(database, { + eventId: eventId(3), + kind: "message.completed", + occurredAt: 1_300, + payload: { messageId, role: "agent" }, + runId, + seq: 3, + sessionId: threadId, + }); + await insertRuntimeEvent(database, { + eventId: eventId(4), + kind: "run.failed", + occurredAt: 1_400, + payload: failure, + runId, + seq: 4, + sessionId: threadId, + }); + await insertRuntimeEvent(database, { + eventId: eventId(5), + kind: "tool.call.updated", + occurredAt: 1_500, + payload: { + rawInput: "{}", + status: "running", + title: "initial_marker", + toolCallId: "initial-terminal-marker", + }, + runId, + seq: 5, + sessionId: threadId, + }); + + const response = await requestPublicApi( + app, + database, + new Request(`https://api.example.com/api/v1/threads/${threadId}/events/stream?limit=1`, { + headers: { Authorization: bearer(TOKENS.owner) }, + }), + { sessionNamespace: liveEvents.binding }, + ); + const reader = response.body?.getReader(); + if (!reader) { + throw new Error("Expected stream response body."); + } + const decoder = new TextDecoder(); + let sseText = ""; + const readUntil = async (marker: string): Promise => { + while (!sseText.includes(marker)) { + const chunk = await Promise.race([ + reader.read(), + Bun.sleep(3_000).then(() => { + throw new Error(`Timed out waiting for ${marker}.`); + }), + ]); + if (chunk.done) { + throw new Error(`SSE closed before ${marker}.`); + } + sseText += decoder.decode(chunk.value, { stream: true }); + } + }; + + await readUntil(`id: ${eventId(5)}`); + await insertRuntimeEvent(database, { + eventId: eventId(6), + kind: "tool.call.updated", + occurredAt: 1_600, + payload: { + rawInput: "{}", + status: "running", + title: "later_marker", + toolCallId: "later-terminal-marker", + }, + runId, + seq: 6, + sessionId: threadId, + }); + liveEvents.emit(); + await readUntil(`id: ${eventId(6)}`); + await reader.cancel(); + liveEvents.close(); + + expect(sseText).not.toContain("OLD OMITTED"); + expect(sseText).not.toContain(`id: ${eventId(2)}`); + }, 10_000); + + test("releases completed run state and rejects late fragments after the next run", async () => { + const database = await createPublicHttpContractDatabase(); + const app = createPublicThreadApiTestApp(); + const liveEvents = createPublicEventSessionNamespace(); + const threadId = generatedPublicThreadId(267); + const firstRunId = PUBLIC_API_TEST_IDS.run; + const secondRunId = generatedPublicThreadId(15_250); + const messageId = "released-run-citation"; + const eventId = (seq: number) => generatedPublicThreadId(15_300 + seq); + + await insertPublicThread(database, { + id: threadId, + title: "Released run state", + updatedAt: 1_000, + }); + const response = await requestPublicApi( + app, + database, + new Request(`https://api.example.com/api/v1/threads/${threadId}/events/stream`, { + headers: { Authorization: bearer(TOKENS.owner) }, + }), + { sessionNamespace: liveEvents.binding }, + ); + const reader = response.body?.getReader(); + if (!reader) { + throw new Error("Expected stream response body."); + } + const decoder = new TextDecoder(); + let sseText = ""; + const readUntil = async (marker: string): Promise => { + while (!sseText.includes(marker)) { + const chunk = await Promise.race([ + reader.read(), + Bun.sleep(3_000).then(() => { + throw new Error(`Timed out waiting for ${marker}.`); + }), + ]); + if (chunk.done) { + throw new Error(`SSE closed before ${marker}.`); + } + sseText += decoder.decode(chunk.value, { stream: true }); + } + }; + + await readUntil(": connected"); + await insertRuntimeEvent(database, { + eventId: eventId(1), + kind: "message.started", + occurredAt: 1_100, + payload: { messageId, role: "agent" }, + runId: firstRunId, + seq: 1, + sessionId: threadId, + }); + await insertRuntimeEvent(database, { + eventId: eventId(2), + kind: "message.delta", + occurredAt: 1_200, + payload: { contentDelta: "before\uE200ci", messageId, role: "agent" }, + runId: firstRunId, + seq: 2, + sessionId: threadId, + }); + await insertRuntimeEvent(database, { + eventId: eventId(3), + kind: "run.failed", + occurredAt: 1_300, + payload: { + error: { + code: "runtime.driver_terminal", + details: {}, + message: "Driver disconnected.", + retryable: true, + }, + recoverable: true, + }, + runId: firstRunId, + seq: 3, + sessionId: threadId, + }); + liveEvents.emit(); + await readUntil(`id: ${eventId(3)}`); + await insertRuntimeEvent(database, { + eventId: eventId(4), + kind: "run.started", + occurredAt: 1_400, + payload: { startedAt: "1970-01-01T00:00:01.400Z" }, + runId: secondRunId, + seq: 4, + sessionId: threadId, + }); + await insertRuntimeEvent(database, { + eventId: eventId(5), + kind: "message.delta", + occurredAt: 1_250, + payload: { + contentDelta: "te\uE202SECRET\uE201after", + messageId, + role: "agent", + }, + runId: firstRunId, + seq: 5, + sessionId: threadId, + }); + await insertRuntimeEvent(database, { + eventId: eventId(6), + kind: "tool.call.updated", + occurredAt: 1_500, + payload: { + rawInput: "{}", + status: "running", + title: "next_run_marker", + toolCallId: "next-run-marker", + }, + runId: secondRunId, + seq: 6, + sessionId: threadId, + }); + liveEvents.emit(); + await readUntil(`id: ${eventId(6)}`); + await reader.cancel(); + liveEvents.close(); + + expect(sseText).toContain('"content":"before"'); + expect(sseText).not.toContain("SECRET"); + expect(sseText).not.toContain(`id: ${eventId(5)}`); + }, 10_000); + + test("reads a complete retained stream across public event pages", async () => { + const database = await createPublicHttpContractDatabase(); + const app = createPublicThreadApiTestApp(); + const threadId = generatedPublicThreadId(256); + const runId = PUBLIC_API_TEST_IDS.run; + const fragmentCount = 520; + const eventId = (seq: number) => generatedPublicThreadId(4_000 + seq); + + await insertPublicThread(database, { + id: threadId, + title: "Paged public streams", + updatedAt: 1_000, + }); + await insertRuntimeEvent(database, { + eventId: eventId(1), + kind: "run.started", + occurredAt: 1_000, + payload: { startedAt: "1970-01-01T00:00:01.000Z" }, + runId, + seq: 1, + sessionId: threadId, + }); + await insertRuntimeEvent(database, { + eventId: eventId(2), + kind: "thought.started", + occurredAt: 2_000, + payload: { thoughtId: "thought-a" }, + runId, + seq: 2, + sessionId: threadId, + }); + await insertRuntimeEvent(database, { + eventId: eventId(3), + kind: "thought.started", + occurredAt: 3_000, + payload: { thoughtId: "thought-b" }, + runId, + seq: 3, + sessionId: threadId, + }); + + for (let index = 0; index < fragmentCount; index += 1) { + const seq = index * 2 + 4; + await insertRuntimeEvent(database, { + eventId: eventId(seq), + kind: "thought.delta", + occurredAt: seq * 1_000, + payload: { contentDelta: "a", thoughtId: "thought-a" }, + runId, + seq, + sessionId: threadId, + }); + await insertRuntimeEvent(database, { + eventId: eventId(seq + 1), + kind: "thought.delta", + occurredAt: (seq + 1) * 1_000, + payload: { contentDelta: "b", thoughtId: "thought-b" }, + runId, + seq: seq + 1, + sessionId: threadId, + }); + } + + const firstTerminalSeq = fragmentCount * 2 + 4; + for (const [offset, thoughtId] of ["thought-a", "thought-b"].entries()) { + const seq = firstTerminalSeq + offset; + await insertRuntimeEvent(database, { + eventId: eventId(seq), + kind: "thought.completed", + occurredAt: seq * 1_000, + payload: { thoughtId }, + runId, + seq, + sessionId: threadId, + }); + } + await insertRuntimeEvent(database, { + eventId: eventId(firstTerminalSeq + 2), + kind: "run.completed", + occurredAt: (firstTerminalSeq + 2) * 1_000, + payload: { stopReason: "end_turn" }, + runId, + seq: firstTerminalSeq + 2, + sessionId: threadId, + }); + + const response = await requestPublicApi( + app, + database, + new Request(`https://api.example.com/api/v1/threads/${threadId}/events?limit=2`, { + headers: { Authorization: bearer(TOKENS.owner) }, + }), + ); + const body = await readJson(response); + const events = expectArray(body["events"]).map(expectRecord); + + expect(body["truncated"]).toBe(true); + expect(events.map((event) => event["type"])).toEqual(["agent.thinking.delta", "run.completed"]); + expect(events[0]?.["content"]).toBe("b".repeat(fragmentCount)); + }); + + test("orders public pages by durable sequence rather than driver time", async () => { + const database = await createPublicHttpContractDatabase(); + const app = createPublicThreadApiTestApp(); + const threadId = generatedPublicThreadId(261); + const runId = PUBLIC_API_TEST_IDS.run; + const fragmentCount = 1_002; + const eventId = (seq: number) => generatedPublicThreadId(10_000 + seq); + + await insertPublicThread(database, { + id: threadId, + title: "Occurrence-ordered public stream", + updatedAt: 1_000, + }); + await insertRuntimeEvent(database, { + eventId: eventId(1), + kind: "run.started", + occurredAt: 1_000, + payload: { startedAt: "1970-01-01T00:00:01.000Z" }, + runId, + seq: 1, + sessionId: threadId, + }); + await insertRuntimeEvent(database, { + eventId: eventId(2), + kind: "message.started", + occurredAt: 100_000, + payload: { messageId: "out-of-order-public-message", role: "agent" }, + runId, + seq: 2, + sessionId: threadId, + }); + + for (let index = 0; index < fragmentCount; index += 1) { + const seq = index + 3; + await insertRuntimeEvent(database, { + eventId: eventId(seq), + kind: "message.delta", + occurredAt: 101_000 + index, + payload: { + contentDelta: "x", + messageId: "out-of-order-public-message", + role: "agent", + }, + runId, + seq, + sessionId: threadId, + }); + } + + const toolSeq = fragmentCount + 3; + await insertRuntimeEvent(database, { + eventId: eventId(toolSeq), + kind: "tool.call.updated", + occurredAt: 2_000, + payload: { + rawInput: "{}", + status: "running", + title: "read_file", + toolCallId: "out-of-order-tool", + }, + runId, + seq: toolSeq, + sessionId: threadId, + }); + const runTerminalSeq = toolSeq + 1; + await insertRuntimeEvent(database, { + eventId: eventId(runTerminalSeq), + kind: "run.completed", + occurredAt: 3_000, + payload: { stopReason: "end_turn" }, + runId, + seq: runTerminalSeq, + sessionId: threadId, + }); + + const response = await requestPublicApi( + app, + database, + new Request(`https://api.example.com/api/v1/threads/${threadId}/events?limit=2`, { + headers: { Authorization: bearer(TOKENS.owner) }, + }), + ); + const body = await readJson(response); + const events = expectArray(body["events"]).map(expectRecord); + + expect(body["truncated"]).toBe(true); + expect(events.map((event) => event["type"])).toEqual(["tool.use.started", "run.completed"]); + expect(events[0]?.["toolCallId"]).toBe("out-of-order-tool"); + }); + + test("fails closed for a public message cut by the raw scan ceiling", async () => { + const database = await createPublicHttpContractDatabase(); + const app = createPublicThreadApiTestApp(); + const liveEvents = createPublicEventSessionNamespace(); + const threadId = generatedPublicThreadId(257); + const runId = PUBLIC_API_TEST_IDS.run; + const fragmentCount = 20_001; + const eventId = (seq: number) => generatedPublicThreadId(7_000 + seq); + + await insertPublicThread(database, { + id: threadId, + title: "Ceiling-bounded public stream", + updatedAt: 1_000, + }); + await insertRuntimeEvent(database, { + eventId: eventId(1), + kind: "message.started", + occurredAt: 1_000, + payload: { messageId: "ceiling-message", role: "agent" }, + runId, + seq: 1, + sessionId: threadId, + }); + + for (let index = 0; index < fragmentCount; index += 1) { + const seq = index + 2; + await insertRuntimeEvent(database, { + eventId: eventId(seq), + kind: "message.delta", + occurredAt: seq * 1_000, + payload: { + contentDelta: index === 0 ? "before\uE200ci" : index === 1 ? "te\uE202SECRET" : "SECRET", + messageId: "ceiling-message", + role: "agent", + }, + runId, + seq, + sessionId: threadId, + }); + } + + const messageTerminalSeq = fragmentCount + 2; + await insertRuntimeEvent(database, { + eventId: eventId(messageTerminalSeq), + kind: "message.completed", + occurredAt: messageTerminalSeq * 1_000, + payload: { messageId: "ceiling-message", role: "agent" }, + runId, + seq: messageTerminalSeq, + sessionId: threadId, + }); + const finalMessageSeq = messageTerminalSeq + 1; + await insertRuntimeEvent(database, { + eventId: eventId(finalMessageSeq), + kind: "message.added", + occurredAt: finalMessageSeq * 1_000, + payload: { + content: "Complete final answer", + messageId: "complete-final-message", + role: "agent", + }, + runId, + seq: finalMessageSeq, + sessionId: threadId, + }); + const runTerminalSeq = finalMessageSeq + 1; + const runTerminalEventId = eventId(runTerminalSeq); + await insertRuntimeEvent(database, { + eventId: runTerminalEventId, + kind: "run.completed", + occurredAt: runTerminalSeq * 1_000, + payload: { stopReason: "end_turn" }, + runId, + seq: runTerminalSeq, + sessionId: threadId, + }); + + const listResponse = await requestPublicApi( + app, + database, + new Request(`https://api.example.com/api/v1/threads/${threadId}/events?limit=2`, { + headers: { Authorization: bearer(TOKENS.owner) }, + }), + ); + const listBody = await readJson(listResponse); + const listEvents = expectArray(listBody["events"]).map(expectRecord); + expect(listBody["truncated"]).toBe(true); + expect(listEvents.map((event) => event["type"])).toEqual([ + "agent.message.delta", + "run.completed", + ]); + expect(listEvents[0]?.["content"]).toBe("Complete final answer"); + + const streamResponse = await requestPublicApi( + app, + database, + new Request(`https://api.example.com/api/v1/threads/${threadId}/events/stream?limit=2`, { + headers: { Authorization: bearer(TOKENS.owner) }, + }), + { sessionNamespace: liveEvents.binding }, + ); + const reader = streamResponse.body?.getReader(); + if (!reader) { + throw new Error("Expected stream response body."); + } + const decoder = new TextDecoder(); + let sseText = ""; + + while (!sseText.includes(`id: ${runTerminalEventId}`)) { + const chunk = await reader.read(); + if (chunk.done) { + throw new Error("SSE closed before the initial canonical event."); + } + sseText += decoder.decode(chunk.value, { stream: true }); + } + + const lateDeltaSeq = runTerminalSeq + 1; + await insertRuntimeEvent(database, { + eventId: eventId(lateDeltaSeq), + kind: "message.delta", + occurredAt: lateDeltaSeq * 1_000, + payload: { + contentDelta: "SECRET\uE201after", + messageId: "ceiling-message", + role: "agent", + }, + runId, + seq: lateDeltaSeq, + sessionId: threadId, + }); + const markerSeq = lateDeltaSeq + 1; + const markerEventId = eventId(markerSeq); + await insertRuntimeEvent(database, { + eventId: markerEventId, + kind: "tool.call.updated", + occurredAt: markerSeq * 1_000, + payload: { + rawInput: "{}", + status: "running", + title: "ceiling_marker", + toolCallId: "ceiling-marker", + }, + runId, + seq: markerSeq, + sessionId: threadId, + }); + liveEvents.emit(); + + while (!sseText.includes(`id: ${markerEventId}`)) { + const chunk = await reader.read(); + if (chunk.done) { + throw new Error("SSE closed before the post-ceiling marker."); + } + sseText += decoder.decode(chunk.value, { stream: true }); + } + + await reader.cancel(); + liveEvents.close(); + const liveEventsBody = sseText + .split("\n") + .filter((line) => line.startsWith("data: ")) + .map((line) => expectRecord(JSON.parse(line.slice("data: ".length)))); + + expect(liveEventsBody.slice(0, listEvents.length)).toEqual(listEvents); + expect(liveEventsBody.at(-1)?.["id"]).toBe(markerEventId); + expect(sseText).not.toContain("SECRET"); + expect(sseText).not.toContain("\uE200"); + }, 30_000); + + test("reads row-scoped pre-0014 stream identities after migration", async () => { + const database = await createPre0014PublicHttpContractDatabase(); + const app = createPublicThreadApiTestApp(); + const threadId = generatedPublicThreadId(999); + const eventIds = ["01J0000000000000000000001F", "01J0000000000000000000001G"]; + + await database + .prepare( + `INSERT INTO session ( + agent_id, archived_at, end_user_id, created_at, creator_account_id, + deployment_version_id, deployment_version_number, id, kind, + metadata_json, model, app_id, provider, renamed, runtime_id, status, + title, type, updated_at + ) VALUES (?, NULL, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, + ) + .bind( + PUBLIC_API_TEST_IDS.agent, + "customer-123", + 1_000, + PUBLIC_API_TEST_IDS.ownerAccount, + PUBLIC_API_TEST_IDS.deployment, + 1, + threadId, + "pet", + JSON.stringify({ + public_api: { + created_by: { + token_id: PUBLIC_API_TEST_IDS.patOwner, + token_label: PUBLIC_API_TEST_IDS.patOwner, + }, + idempotency_key: null, + source: "public_api", + }, + }), + "gpt-5.4", + PUBLIC_API_TEST_IDS.app, + "openai", + false, + "openai-runtime", + "IDLE", + "Pre-0014 stream fixture", + "ui", + 1_000, + ) + .run(); + for (const [index, eventId] of eventIds.entries()) { + await database + .prepare( + `INSERT INTO session_event ( + id, session_id, agent_id, seq, content_text, ended_at, event_type, + family, process_status, process_type, source, source_event_id, + visibility, occurred_at, created_at + ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, + ) + .bind( + eventId, + threadId, + PUBLIC_API_TEST_IDS.agent, + index + 1, + `Legacy row ${index + 1}`, + 1_100 + index, + "message.delta", + "process", + "available", + "agent.message.delta", + "driver", + eventId, + "all_consumers", + 1_100 + index, + 1_100 + index, + ) + .run(); + } + + migratePre0014PublicHttpContractDatabase(database); + + const rows = await database + .prepare( + `SELECT content_text, ended_at, event_type, id, occurred_at, + process_status, process_type, run_id, seq, stream_id, tokens + FROM session_event + WHERE session_id = ? + ORDER BY seq`, + ) + .bind(threadId) + .all(); + expect(rows.results.map((row) => row.stream_id)).toEqual(eventIds); + await expect( + database + .prepare( + "SELECT semantic_hash, tool_parent_message_id, tool_result_message_id, tool_status FROM session_event WHERE session_id = ? ORDER BY seq", + ) + .bind(threadId) + .all(), + ).resolves.toMatchObject({ + results: eventIds.map(() => ({ + semantic_hash: null, + tool_parent_message_id: null, + tool_result_message_id: null, + tool_status: null, + })), + success: true, + }); + await expect( + database + .prepare("UPDATE session_event SET semantic_hash = 'not-a-sha256' WHERE id = ?") + .bind(eventIds[0]) + .run(), + ).rejects.toThrow(); + expect( + createSessionProcessEventsFromSessionEventRows(rows.results).map((event) => event.content), + ).toEqual(["Legacy row 1", "Legacy row 2"]); + + const response = await requestPublicApi( + app, + database, + new Request(`https://api.example.com/api/v1/threads/${threadId}/events`, { + headers: { Authorization: bearer(TOKENS.owner) }, + }), + ); + expect(response.status).toBe(200); + expect( + expectArray((await readJson(response))["events"]).map( + (event) => expectRecord(event)["content"], + ), + ).toEqual(["Legacy row 1", "Legacy row 2"]); + + await database + .prepare( + "UPDATE session_event SET event_type = 'tool.call.updated', run_id = ?, tool_call_id = 'tool-1', tool_status = 'completed', mcp_command_id = '01J0000000000000000000001J' WHERE id = ?", + ) + .bind(PUBLIC_API_TEST_IDS.run, eventIds[0]) + .run(); + await expect( + database + .prepare( + "UPDATE session_event SET event_type = 'tool.call.updated', run_id = ?, tool_call_id = 'tool-1', tool_status = 'running' WHERE id = ?", + ) + .bind(PUBLIC_API_TEST_IDS.run, eventIds[1]) + .run(), + ).resolves.toBeDefined(); + await expect( + database + .prepare( + "UPDATE session_event SET tool_status = 'failed', mcp_command_id = '01J0000000000000000000001J' WHERE id = ?", + ) + .bind(eventIds[1]) + .run(), + ).rejects.toThrow("UNIQUE constraint failed"); + }); + test("bounds public Thread lists on stable latest ordering", async () => { const database = await createPublicHttpContractDatabase(); const app = createPublicThreadApiTestApp(); @@ -1261,7 +3621,7 @@ describe("Public Thread API e2e", () => { const app = createPublicThreadApiTestApp(); const bucket = new PublicApiMemoryFileBucket(); const requestThreadApi = (request: Request) => - requestPublicApi(app, database, request, { fileBucket: bucket as unknown as R2Bucket }); + requestPublicApi(app, database, request, { fileBucket: bucket }); await withProviderProbeMock(async () => { const createThreadResponse = await requestThreadApi( @@ -1585,7 +3945,7 @@ describe("Public Thread API e2e", () => { const app = createPublicThreadApiTestApp(); const bucket = new PublicApiMemoryFileBucket(); const requestThreadApi = (request: Request) => - requestPublicApi(app, database, request, { fileBucket: bucket as unknown as R2Bucket }); + requestPublicApi(app, database, request, { fileBucket: bucket }); await withProviderProbeMock(async () => { const fileBody = "Launch note.\n"; @@ -1805,7 +4165,7 @@ describe("Public Thread API e2e", () => { const app = createPublicThreadApiTestApp(); const bucket = new PublicApiMemoryFileBucket(); const requestThreadApi = (request: Request) => - requestPublicApi(app, database, request, { fileBucket: bucket as unknown as R2Bucket }); + requestPublicApi(app, database, request, { fileBucket: bucket }); const threadId = generatedPublicThreadId(130); await insertPublicThread(database, { diff --git a/apps/api/tests/rpc-wire-v3.test.ts b/apps/api/tests/rpc-wire-v3.test.ts new file mode 100644 index 00000000..3f6bcfe1 --- /dev/null +++ b/apps/api/tests/rpc-wire-v3.test.ts @@ -0,0 +1,585 @@ +import { describe, expect, test } from "bun:test"; + +import { ExternalToolEffectClaim } from "@mosoo/contracts/external-tool-effect"; +import { + DURABLE_RUN_ERROR_MAX_UTF8_BYTES, + measureDurableRunErrorJson, +} from "@mosoo/contracts/session-run"; +import { parseSchemaValue } from "@mosoo/contracts/validation"; +import { PLATFORM_ID_FIXTURES } from "@mosoo/id/testing"; +import { createRuntimeEvent } from "@mosoo/runtime-events"; + +import { + parseDriverCommandUpdateInput, + parseDriverCompletionInput, + parseDriverEventBatchInput, + parseDriverEventBatchOutput, + parseDriverExternalToolEffectClaimInput, + parseDriverExternalToolEffectObserveInput, + parseDriverExternalToolEffectSettleInput, + parseDriverFailureInput, + parseDriverHeartbeatInput, + parseDriverHeartbeatOutput, + parseDriverHelloInput, + parseDriverHelloOutput, + parseDriverLogBatchInput, + parseDriverLogBatchOutput, + parseDriverNextCommandInput, + parseDriverNextCommandOutput, + parseDriverReadyInput, +} from "../src/modules/runtime/infrastructure/driver-instance/rpc-wire"; + +const EXTERNAL_TOOL_EFFECT_CLAIM_TOKEN = "123e4567-e89b-42d3-a456-426614174000"; + +const invalidPositiveSafeIntegers = [ + 0, + -1, + 0.5, + Number.NaN, + Number.POSITIVE_INFINITY, + Number.NEGATIVE_INFINITY, + Number.MAX_SAFE_INTEGER + 1, +] as const; +const invalidNonNegativeSafeIntegers = invalidPositiveSafeIntegers.slice(1); + +function helloInput(overrides: Record = {}) { + return { + capabilities: [], + driverVersion: "test", + pid: 1, + protocolVersion: 3, + runtime: "acp-fallback", + startedAt: "2026-08-29T00:00:00.000Z", + ...overrides, + }; +} + +function helloOutput(runConfig: Record = {}, runId: string | null = null) { + return { + acceptedCapabilities: [], + connectionId: "connection-1", + driverInstanceId: "driver-1", + heartbeatIntervalMs: 250, + runConfig: { + commandLeaseMs: 0, + envPolicy: "strict", + eventBatchMaxSize: 1, + organizationPath: "/workspace", + ...runConfig, + }, + runId, + }; +} + +function heartbeatInput(pid: number, at = "2026-08-29T00:00:00.000Z") { + return { at, pid, reason: "interval" }; +} + +function readyInput(pid: number) { + return { + at: "2026-08-29T00:00:00.000Z", + driverInstanceId: "driver-1", + pid, + }; +} + +function logBatch(seq: number) { + return { + driverInstanceId: "driver-1", + logs: [{ level: "info", message: "message", seq, timestamp: "now" }], + }; +} + +function eventBatchOutput(seq: number, type = "message.delta") { + return { accepted: [{ eventId: "source-1", seq, type }] }; +} + +describe("API Driver RPC wire v3", () => { + test("rejects undeclared keys throughout fixed RPC shapes", () => { + const event = createRuntimeEvent({ + id: PLATFORM_ID_FIXTURES.runtimeEvent, + kind: "diagnostic.reported", + occurredAt: "2026-08-29T00:00:00.000Z", + payload: { message: "ok" }, + sessionId: PLATFORM_ID_FIXTURES.session, + }); + const cases: ReadonlyArray unknown]> = [ + ["hello", () => parseDriverHelloInput(helloInput({ extra: true }))], + ["hello output", () => parseDriverHelloOutput({ ...helloOutput(), extra: true })], + ["run config", () => parseDriverHelloOutput(helloOutput({ extra: true }))], + ["heartbeat", () => parseDriverHeartbeatInput({ ...heartbeatInput(1), extra: true })], + [ + "heartbeat output", + () => parseDriverHeartbeatOutput({ heartbeatCount: 0, extra: true, ok: true }), + ], + ["ready", () => parseDriverReadyInput({ ...readyInput(1), extra: true })], + [ + "command update", + () => + parseDriverCommandUpdateInput({ + commandId: "command-1", + driverInstanceId: "driver-1", + extra: true, + status: "accepted", + }), + ], + [ + "effect claim", + () => + parseDriverExternalToolEffectClaimInput({ + claimToken: EXTERNAL_TOOL_EFFECT_CLAIM_TOKEN, + commandId: "command-1", + driverInstanceId: "driver-1", + extra: true, + }), + ], + [ + "effect observe", + () => + parseDriverExternalToolEffectObserveInput({ + commandId: "command-1", + driverInstanceId: "driver-1", + extra: true, + }), + ], + [ + "effect settle", + () => + parseDriverExternalToolEffectSettleInput({ + claimToken: EXTERNAL_TOOL_EFFECT_CLAIM_TOKEN, + commandId: "command-1", + driverInstanceId: "driver-1", + effectId: "effect-1", + extra: true, + settlement: { kind: "unknown" }, + }), + ], + [ + "next command", + () => parseDriverNextCommandInput({ driverInstanceId: "driver-1", extra: true }), + ], + ["next command output", () => parseDriverNextCommandOutput({ command: null, extra: true })], + [ + "completion", + () => + parseDriverCompletionInput({ driverInstanceId: "driver-1", extra: true, runId: "run-1" }), + ], + [ + "failure", + () => + parseDriverFailureInput({ + driverInstanceId: "driver-1", + error: { code: "failed", details: {}, message: "failed", retryable: false }, + extra: true, + runId: "run-1", + }), + ], + [ + "event batch", + () => + parseDriverEventBatchInput({ + driverInstanceId: "driver-1", + events: [{ event, eventId: "event-1" }], + extra: true, + }), + ], + [ + "event envelope", + () => + parseDriverEventBatchInput({ + driverInstanceId: "driver-1", + events: [{ event, eventId: "event-1", extra: true }], + }), + ], + [ + "event body", + () => + parseDriverEventBatchInput({ + driverInstanceId: "driver-1", + events: [{ event: { ...event, extra: true }, eventId: "event-1" }], + }), + ], + [ + "event context", + () => + parseDriverEventBatchInput({ + driverInstanceId: "driver-1", + events: [{ event: { ...event, context: {} }, eventId: "event-1" }], + }), + ], + [ + "event surface", + () => + parseDriverEventBatchInput({ + driverInstanceId: "driver-1", + events: [{ event: { ...event, surface: {} }, eventId: "event-1" }], + }), + ], + ["event output", () => parseDriverEventBatchOutput({ ...eventBatchOutput(0), extra: true })], + [ + "event receipt", + () => + parseDriverEventBatchOutput({ + accepted: [{ eventId: "event-1", extra: true, seq: 0, type: "message.delta" }], + }), + ], + ["log batch", () => parseDriverLogBatchInput({ ...logBatch(0), extra: true })], + [ + "log entry", + () => + parseDriverLogBatchInput({ + driverInstanceId: "driver-1", + logs: [{ ...logBatch(0).logs[0], extra: true }], + }), + ], + [ + "log context", + () => + parseDriverLogBatchInput({ + driverInstanceId: "driver-1", + logs: [{ ...logBatch(0).logs[0], context: { extra: true } }], + }), + ], + [ + "log error", + () => + parseDriverLogBatchInput({ + driverInstanceId: "driver-1", + logs: [ + { ...logBatch(0).logs[0], error: { extra: true, message: "failed", name: "Error" } }, + ], + }), + ], + ["log output", () => parseDriverLogBatchOutput({ extra: true, ok: true })], + ]; + + for (const [label, parse] of cases) { + expect(parse, label).toThrow(); + } + }); + + test("keeps the intentional primitive log fields extension point", () => { + expect( + parseDriverLogBatchInput({ + driverInstanceId: "driver-1", + logs: [{ ...logBatch(0).logs[0], fields: { futureField: true } }], + }).logs[0]?.fields, + ).toEqual({ futureField: true }); + }); + + test("bounds event and log batches at 64 entries", () => { + const event = createRuntimeEvent({ + id: PLATFORM_ID_FIXTURES.runtimeEvent, + kind: "diagnostic.reported", + occurredAt: "2026-08-29T00:00:00.000Z", + payload: { message: "ok" }, + sessionId: PLATFORM_ID_FIXTURES.session, + }); + const parsers = [ + (length: number) => + parseDriverEventBatchInput({ + driverInstanceId: "driver-1", + events: Array.from({ length }, () => ({ event, eventId: "event-1" })), + }), + (length: number) => + parseDriverLogBatchInput({ + driverInstanceId: "driver-1", + logs: Array.from({ length }, () => logBatch(0).logs[0]), + }), + ] as const; + + for (const parse of parsers) { + expect(() => parse(64)).not.toThrow(); + expect(() => parse(65)).toThrow(); + } + }); + + test.each(["accepted", "cancelled", "completed", "failed"] as const)( + "accepts Driver-owned command status %s", + (status) => { + const terminal = + status === "failed" + ? { + error: { + code: "driver.command_failed", + details: {}, + message: "failed", + retryable: false, + }, + } + : status === "completed" + ? { result: { requestId: "request-1" } } + : {}; + + expect( + parseDriverCommandUpdateInput({ + commandId: "command-1", + driverInstanceId: "driver-1", + ...terminal, + status, + }).status, + ).toBe(status); + }, + ); + + test.each([ + ["accepted with result", { result: { requestId: "request-1" }, status: "accepted" }], + [ + "accepted with error", + { + error: { code: "failed", details: {}, message: "failed", retryable: false }, + status: "accepted", + }, + ], + ["cancelled with result", { result: { requestId: "request-1" }, status: "cancelled" }], + [ + "cancelled with error", + { + error: { code: "failed", details: {}, message: "failed", retryable: false }, + status: "cancelled", + }, + ], + ["completed with null result", { result: null, status: "completed" }], + [ + "completed with error", + { + error: { code: "failed", details: {}, message: "failed", retryable: false }, + status: "completed", + }, + ], + ["failed without error", { status: "failed" }], + [ + "failed with result", + { + error: { code: "failed", details: {}, message: "failed", retryable: false }, + result: { requestId: "request-1" }, + status: "failed", + }, + ], + ] as const)("rejects command update payload mismatch: %s", (_label, update) => { + expect(() => + parseDriverCommandUpdateInput({ + commandId: "command-1", + driverInstanceId: "driver-1", + ...update, + }), + ).toThrow(); + }); + + test("rejects duplicate capability ids in hello input and output", () => { + const capability = { id: "text_stream", status: "supported", version: 1 } as const; + + expect(() => + parseDriverHelloInput(helloInput({ capabilities: [capability, capability] })), + ).toThrow(); + expect(() => + parseDriverHelloOutput({ + ...helloOutput(), + acceptedCapabilities: [capability, capability], + }), + ).toThrow(); + }); + + test.each(["queued", "delivered", "expired"] as const)( + "rejects host-owned command status %s at the RPC boundary", + (status) => { + expect(() => + parseDriverCommandUpdateInput({ + commandId: "command-1", + driverInstanceId: "driver-1", + status, + }), + ).toThrow(); + }, + ); + + test("accepts a canonical UUID external-effect claim token", () => { + expect( + parseDriverExternalToolEffectClaimInput({ + claimToken: EXTERNAL_TOOL_EFFECT_CLAIM_TOKEN, + commandId: "command-1", + driverInstanceId: "driver-1", + }), + ).toMatchObject({ claimToken: EXTERNAL_TOOL_EFFECT_CLAIM_TOKEN }); + expect( + parseDriverExternalToolEffectSettleInput({ + claimToken: EXTERNAL_TOOL_EFFECT_CLAIM_TOKEN, + commandId: "command-1", + driverInstanceId: "driver-1", + effectId: "effect-1", + settlement: { kind: "unknown" }, + }), + ).toMatchObject({ claimToken: EXTERNAL_TOOL_EFFECT_CLAIM_TOKEN }); + }); + + test.each([ + ["empty", ""], + ["uppercase UUID", EXTERNAL_TOOL_EFFECT_CLAIM_TOKEN.toUpperCase()], + ["extra-hyphen UUID", "123e4567-e89b-42d3-a456-42661417-00"], + ["non-v4 UUID", "123e4567-e89b-f2d3-a456-426614174000"], + ["invalid-variant UUID", "123e4567-e89b-42d3-c456-426614174000"], + ["non-UUID", "claim-token"], + ["37-character", "x".repeat(37)], + ["one-megabyte", "x".repeat(1_000_000)], + ] as const)("rejects a %s external-effect claim token", (_label, claimToken) => { + expect(() => + parseDriverExternalToolEffectClaimInput({ + claimToken, + commandId: "command-1", + driverInstanceId: "driver-1", + }), + ).toThrow(); + expect(() => + parseDriverExternalToolEffectSettleInput({ + claimToken, + commandId: "command-1", + driverInstanceId: "driver-1", + effectId: "effect-1", + settlement: { kind: "unknown" }, + }), + ).toThrow(); + }); + + test.each([2, 4] as const)("rejects protocol version %d", (protocolVersion) => { + expect(() => parseDriverHelloInput(helloInput({ protocolVersion }))).toThrow(); + }); + + test.each(invalidPositiveSafeIntegers)( + "rejects %p at every positive safe-integer field", + (value) => { + expect(() => parseDriverHelloInput(helloInput({ pid: value }))).toThrow(); + expect(() => parseDriverHeartbeatInput(heartbeatInput(value))).toThrow(); + expect(() => parseDriverReadyInput(readyInput(value))).toThrow(); + expect(() => parseDriverHelloOutput(helloOutput({ eventBatchMaxSize: value }))).toThrow(); + expect(() => + parseSchemaValue(ExternalToolEffectClaim, { + attempt: value, + effectId: "effect-1", + idempotencyKey: "idempotency-1", + kind: "claimed", + }), + ).toThrow(); + }, + ); + + test("bounds the negotiated event batch size to the wire limit", () => { + expect(parseDriverHelloOutput(helloOutput({ eventBatchMaxSize: 64 })).runConfig).toMatchObject({ + eventBatchMaxSize: 64, + }); + expect(() => parseDriverHelloOutput(helloOutput({ eventBatchMaxSize: 65 }))).toThrow(); + }); + + test.each([ + 0, + 249, + 250.5, + Number.NaN, + Number.POSITIVE_INFINITY, + Number.NEGATIVE_INFINITY, + Number.MAX_SAFE_INTEGER + 1, + ] as const)("rejects invalid heartbeat interval %p", (heartbeatIntervalMs) => { + expect(() => parseDriverHelloOutput({ ...helloOutput(), heartbeatIntervalMs })).toThrow(); + }); + + test.each(invalidNonNegativeSafeIntegers)( + "rejects %p at every non-negative safe-integer field", + (value) => { + expect(() => parseDriverHelloOutput(helloOutput({ commandLeaseMs: value }))).toThrow(); + expect(() => parseDriverHeartbeatOutput({ heartbeatCount: value, ok: true })).toThrow(); + expect(() => parseDriverLogBatchInput(logBatch(value))).toThrow(); + expect(() => parseDriverEventBatchOutput(eventBatchOutput(value))).toThrow(); + }, + ); + + test("accepts zero at every non-negative safe-integer field", () => { + expect(parseDriverHelloOutput(helloOutput()).runConfig.commandLeaseMs).toBe(0); + expect(parseDriverHeartbeatOutput({ heartbeatCount: 0, ok: true }).heartbeatCount).toBe(0); + expect(parseDriverLogBatchInput(logBatch(0)).logs[0]?.seq).toBe(0); + expect(parseDriverEventBatchOutput(eventBatchOutput(0)).accepted[0]?.seq).toBe(0); + }); + + test.each([ + [ + "failure message", + () => + parseDriverFailureInput({ + driverInstanceId: "driver-1", + error: { code: "failed", details: {}, message: "", retryable: false }, + runId: "run-1", + }), + ], + ["receipt type", () => parseDriverEventBatchOutput(eventBatchOutput(0, ""))], + ] as const)("rejects an empty %s", (_label, parse) => { + expect(parse).toThrow(); + }); + + test("bounds Driver failure errors with the durable JSON budget", () => { + const base = { code: "failed", details: {}, message: "", retryable: false }; + const exact = { + ...base, + message: "x".repeat(DURABLE_RUN_ERROR_MAX_UTF8_BYTES - measureDurableRunErrorJson(base)), + }; + + expect(measureDurableRunErrorJson(exact)).toBe(DURABLE_RUN_ERROR_MAX_UTF8_BYTES); + expect( + parseDriverFailureInput({ driverInstanceId: "driver-1", error: exact, runId: "run-1" }).error, + ).toEqual(exact); + expect(() => + parseDriverFailureInput({ + driverInstanceId: "driver-1", + error: { ...exact, message: `${exact.message}x` }, + runId: "run-1", + }), + ).toThrow(); + }); + + test("requires an exact run id on terminal RPCs", () => { + expect(() => parseDriverCompletionInput({ driverInstanceId: "driver-1" })).toThrow("runId"); + expect(() => + parseDriverFailureInput({ + driverInstanceId: "driver-1", + error: { code: "failed", details: {}, message: "failed", retryable: false }, + }), + ).toThrow("runId"); + }); + + test.each([ + ["missing", { accepted: [{ seq: 0, type: "message.delta" }] }], + ["empty", { accepted: [{ eventId: "", seq: 0, type: "message.delta" }] }], + ["non-string", { accepted: [{ eventId: 1, seq: 0, type: "message.delta" }] }], + ] as const)("rejects %s receipt eventId", (_label, input) => { + expect(() => parseDriverEventBatchOutput(input)).toThrow(); + }); + + test("rejects empty handshake identity fields", () => { + expect(() => parseDriverHelloInput(helloInput({ startedAt: "" }))).toThrow(); + expect(() => parseDriverHeartbeatInput(heartbeatInput(1, ""))).toThrow(); + expect(() => parseDriverHelloOutput(helloOutput({}, ""))).toThrow(); + }); + + test("rejects an unknown event receipt kind", () => { + expect(() => parseDriverEventBatchOutput(eventBatchOutput(0, "future.event"))).toThrow(); + }); + + test("preserves empty log and tracing strings", () => { + expect( + parseDriverLogBatchInput({ + driverInstanceId: "driver-1", + logs: [ + { + context: { spanId: "", traceId: "" }, + error: { message: "", name: "" }, + level: "error", + message: "", + seq: 0, + timestamp: "now", + }, + ], + }).logs[0], + ).toMatchObject({ + context: { spanId: "", traceId: "" }, + error: { message: "", name: "" }, + message: "", + }); + }); +}); diff --git a/apps/api/tests/runtime-artifact-migration.test.ts b/apps/api/tests/runtime-artifact-migration.test.ts new file mode 100644 index 00000000..e7a1f0d6 --- /dev/null +++ b/apps/api/tests/runtime-artifact-migration.test.ts @@ -0,0 +1,286 @@ +import { describe, expect, test } from "bun:test"; + +import type { FileId, SessionId } from "@mosoo/id"; + +import { fileStore } from "../src/modules/files/application/file-store"; +import { applyDrizzleMigration, applyDrizzleMigrationsBefore } from "./helpers/drizzle-migrations"; +import { SqliteD1Database } from "./helpers/sqlite-d1"; + +const MIGRATION_TAG = "0016_durable-event-side-effects"; + +const ACCOUNT_ID = "01J0000000000000000000001K"; +const AGENT_ID = "01J0000000000000000000001J"; +const APP_ID = "01J0000000000000000000001M"; +const SESSION_ID = "01J0000000000000000000001H" as SessionId; +const ORPHAN_SESSION_ID = "01J0000000000000000000001P" as SessionId; + +const OLDER_FILE_ID = "01J00000000000000000000039" as FileId; +const TIE_LOSER_FILE_ID = "01J00000000000000000000031" as FileId; +const WINNER_FILE_ID = "01J00000000000000000000032" as FileId; +const ORPHAN_FILE_ID = "01J00000000000000000000033" as FileId; +const INVALID_PARENT_FILE_ID = "01J00000000000000000000034" as FileId; +const INVALID_PATH_FILE_ID = "01J00000000000000000000035" as FileId; +const DELETING_FILE_ID = "01J00000000000000000000036" as FileId; +const LATE_REPORT_FILE_ID = "01J00000000000000000000037" as FileId; +const LATE_PATH_NEWER_FILE_ID = "01J0000000000000000000003A" as FileId; +const V3_FILE_ID = "01J0000000000000000000003D" as FileId; +const V3_LATE_LEGACY_FILE_ID = "01J0000000000000000000003E" as FileId; +const MISSING_HEAD_FILE_ID = "01J0000000000000000000003F" as FileId; +const MISSING_HEAD_LATE_FILE_ID = "01J0000000000000000000003G" as FileId; +const SAME_TIME_LOSER_FILE_ID = "01J0000000000000000000003H" as FileId; +const SAME_TIME_WINNER_FILE_ID = "01J0000000000000000000003J" as FileId; + +interface ArtifactHeadRow { + file_id: string | null; + runtime_event_seq: number; + session_id: string; + source_event_id: string; + source_path: string; + updated_at: number; +} + +function legacyRuntimeOutputParentPath(sourcePath: string, contentSha256: string): string { + return `runtime-output/${sourcePath}/${contentSha256}`; +} + +async function createPre0016Database(): Promise { + const database = new SqliteD1Database(); + applyDrizzleMigrationsBefore(database, MIGRATION_TAG); + + return database; +} + +async function insertSession(database: SqliteD1Database): Promise { + await database + .prepare( + `INSERT INTO session ( + agent_id, created_at, creator_account_id, id, kind, model, app_id, + provider, renamed, runtime_id, status, updated_at + ) VALUES (?, 1, ?, ?, 'agent', 'gpt-5.4', ?, 'openai', 0, 'codex', 'IDLE', 1)`, + ) + .bind(AGENT_ID, ACCOUNT_ID, SESSION_ID, APP_ID) + .run(); +} + +async function insertLegacyArtifact( + database: SqliteD1Database, + input: { + createdAt: number; + fileId: FileId; + parentPath: string; + sessionId?: SessionId; + status?: "deleting" | "ready"; + }, +): Promise { + const name = `artifact-${input.fileId}.txt`; + + await database + .prepare( + `INSERT INTO file_record ( + committed, created_at, created_by_account_id, etag, expires_at, id, + mime_type, name, object_key, owner_id, owner_kind, parent_path, path, + purpose, scope_id, scope_kind, session_kind, size, status, updated_at, + version + ) VALUES ( + 1, ?, ?, ?, NULL, ?, 'text/plain', ?, ?, ?, 'session', ?, ?, + 'session_artifact', ?, 'session', 'artifact', ?, ?, ?, 1 + )`, + ) + .bind( + input.createdAt, + ACCOUNT_ID, + `etag-${input.fileId}`, + input.fileId, + name, + `objects/${input.fileId}`, + input.sessionId ?? SESSION_ID, + input.parentPath, + `session-artifacts/${input.fileId}/${name}`, + input.sessionId ?? SESSION_ID, + input.fileId === WINNER_FILE_ID ? 32 : 1, + input.status ?? "ready", + input.createdAt, + ) + .run(); +} + +describe("runtime artifact migration", () => { + test("backfills only the deterministic visible and restorable legacy artifact head", async () => { + const database = await createPre0016Database(); + const preMigrationColumns = await database + .prepare("PRAGMA table_info(file_record)") + .all<{ name: string }>(); + + expect(preMigrationColumns.results.map(({ name }) => name)).not.toContain("runtime_event_seq"); + + await insertSession(database); + await insertLegacyArtifact(database, { + createdAt: 10, + fileId: OLDER_FILE_ID, + parentPath: legacyRuntimeOutputParentPath("outputs/report.txt", "a".repeat(64)), + }); + await insertLegacyArtifact(database, { + createdAt: 20, + fileId: TIE_LOSER_FILE_ID, + parentPath: legacyRuntimeOutputParentPath("outputs/report.txt", "b".repeat(64)), + }); + await insertLegacyArtifact(database, { + createdAt: 20, + fileId: WINNER_FILE_ID, + parentPath: legacyRuntimeOutputParentPath("outputs/report.txt", "c".repeat(64)), + }); + await insertLegacyArtifact(database, { + createdAt: 30, + fileId: ORPHAN_FILE_ID, + parentPath: legacyRuntimeOutputParentPath("outputs/orphan.txt", "d".repeat(64)), + sessionId: ORPHAN_SESSION_ID, + }); + await insertLegacyArtifact(database, { + createdAt: 30, + fileId: INVALID_PARENT_FILE_ID, + parentPath: legacyRuntimeOutputParentPath("outputs/invalid-parent.txt", "g".repeat(64)), + }); + await insertLegacyArtifact(database, { + createdAt: 30, + fileId: INVALID_PATH_FILE_ID, + parentPath: `runtime-output/outputs/../invalid-path.txt/${"e".repeat(64)}`, + }); + await insertLegacyArtifact(database, { + createdAt: 30, + fileId: DELETING_FILE_ID, + parentPath: legacyRuntimeOutputParentPath("outputs/deleting.txt", "f".repeat(64)), + status: "deleting", + }); + + applyDrizzleMigration(database, MIGRATION_TAG); + + const heads = await database + .prepare( + `SELECT file_id, runtime_event_seq, session_id, source_event_id, source_path, updated_at + FROM session_artifact_head + ORDER BY session_id, source_path`, + ) + .all(); + + expect(heads.results).toEqual([ + { + file_id: WINNER_FILE_ID, + runtime_event_seq: 0, + session_id: SESSION_ID, + source_event_id: `legacy-file:${WINNER_FILE_ID}`, + source_path: "outputs/report.txt", + updated_at: 20, + }, + ]); + + const viewerFiles = await fileStore.listReadySessionFiles(database, SESSION_ID); + expect(viewerFiles.map(({ id }) => id)).toEqual([WINNER_FILE_ID]); + + const restoreSources = await fileStore.listLatestReadySessionArtifactSources( + database, + SESSION_ID, + ); + expect(restoreSources).toEqual([ + { + objectKey: `objects/${WINNER_FILE_ID}`, + size: 32, + sourcePath: "outputs/report.txt", + }, + ]); + + // Once the migration creates the head ledger, later headless writes from + // an old Worker are not durable artifact authority. + await insertLegacyArtifact(database, { + createdAt: 40, + fileId: LATE_REPORT_FILE_ID, + parentPath: legacyRuntimeOutputParentPath("outputs/report.txt", "1".repeat(64)), + }); + await insertLegacyArtifact(database, { + createdAt: 35, + fileId: LATE_PATH_NEWER_FILE_ID, + parentPath: legacyRuntimeOutputParentPath("outputs/late.txt", "3".repeat(64)), + }); + + expect( + (await fileStore.listReadySessionFiles(database, SESSION_ID)).map(({ id }) => id), + ).toEqual([WINNER_FILE_ID]); + expect(await fileStore.listLatestReadySessionArtifactSources(database, SESSION_ID)).toEqual([ + { + objectKey: `objects/${WINNER_FILE_ID}`, + size: 32, + sourcePath: "outputs/report.txt", + }, + ]); + }); + + test("keeps v3 and invalid legacy heads authoritative over late legacy rows", async () => { + const database = await createPre0016Database(); + await insertSession(database); + applyDrizzleMigration(database, MIGRATION_TAG); + + await insertLegacyArtifact(database, { + createdAt: 10, + fileId: V3_FILE_ID, + parentPath: legacyRuntimeOutputParentPath("outputs/v3.txt", "6".repeat(64)), + }); + await database + .prepare("UPDATE file_record SET runtime_event_seq = 2 WHERE id = ?") + .bind(V3_FILE_ID) + .run(); + await database + .prepare( + `INSERT INTO session_artifact_head ( + file_id, runtime_event_seq, session_id, source_event_id, source_path, updated_at + ) VALUES (?, 2, ?, 'v3:upsert', 'outputs/v3.txt', 10)`, + ) + .bind(V3_FILE_ID, SESSION_ID) + .run(); + await insertLegacyArtifact(database, { + createdAt: 20, + fileId: V3_LATE_LEGACY_FILE_ID, + parentPath: legacyRuntimeOutputParentPath("outputs/v3.txt", "7".repeat(64)), + }); + + await database + .prepare( + `INSERT INTO session_artifact_head ( + file_id, runtime_event_seq, session_id, source_event_id, source_path, updated_at + ) VALUES (?, 0, ?, ?, 'outputs/missing.txt', 10)`, + ) + .bind(MISSING_HEAD_FILE_ID, SESSION_ID, `legacy-file:${MISSING_HEAD_FILE_ID}`) + .run(); + await insertLegacyArtifact(database, { + createdAt: 20, + fileId: MISSING_HEAD_LATE_FILE_ID, + parentPath: legacyRuntimeOutputParentPath("outputs/missing.txt", "8".repeat(64)), + }); + + await insertLegacyArtifact(database, { + createdAt: 30, + fileId: SAME_TIME_LOSER_FILE_ID, + parentPath: legacyRuntimeOutputParentPath("outputs/tie.txt", "9".repeat(64)), + }); + await insertLegacyArtifact(database, { + createdAt: 30, + fileId: SAME_TIME_WINNER_FILE_ID, + parentPath: legacyRuntimeOutputParentPath("outputs/tie.txt", "a".repeat(64)), + }); + + const viewerFileIds = (await fileStore.listReadySessionFiles(database, SESSION_ID)).map( + ({ id }) => id, + ); + expect(viewerFileIds).toContain(V3_FILE_ID); + expect(viewerFileIds).not.toContain(V3_LATE_LEGACY_FILE_ID); + expect(viewerFileIds).not.toContain(MISSING_HEAD_LATE_FILE_ID); + expect(viewerFileIds).not.toContain(SAME_TIME_WINNER_FILE_ID); + expect(viewerFileIds).not.toContain(SAME_TIME_LOSER_FILE_ID); + + expect(await fileStore.listLatestReadySessionArtifactSources(database, SESSION_ID)).toEqual([ + { + objectKey: `objects/${V3_FILE_ID}`, + size: 1, + sourcePath: "outputs/v3.txt", + }, + ]); + }); +}); diff --git a/apps/api/tests/runtime-command-store.test.ts b/apps/api/tests/runtime-command-store.test.ts index 36be482e..2373b851 100644 --- a/apps/api/tests/runtime-command-store.test.ts +++ b/apps/api/tests/runtime-command-store.test.ts @@ -1,18 +1,20 @@ import { describe, expect, test } from "bun:test"; +import { RUNTIME_COMMAND_MAX_UTF8_BYTES } from "@mosoo/contracts/runtime-command"; import type { RuntimeCommand, RuntimeCommandStatus } from "@mosoo/contracts/runtime-command"; +import type { RunError } from "@mosoo/contracts/session-run"; import { parsePlatformId } from "@mosoo/id"; import type { DriverCommandId, DriverInstanceId, SessionRunId } from "@mosoo/id"; import { - claimNextQueuedRuntimeCommandRecord, - createRuntimeCommandRecord, + claimNextQueuedRuntimeCommandRecord as claimNextQueuedRuntimeCommand, + createRuntimeCommandRecord as persistRuntimeCommandRecord, expireUndeliveredInputStartCommandsForRun, + getRuntimeCommandRecord as readRuntimeCommandRecord, + maintainRuntimeCommandRecords as maintainRuntimeCommands, + markRuntimeCommandRecordDelivered as markRuntimeCommandDelivered, repairRuntimeCommandRecords, - getRuntimeCommandRecord, - markRuntimeCommandRecordDelivered, - maintainRuntimeCommandRecords, - updateRuntimeCommandRecord, + updateRuntimeCommandRecord as updateStoredRuntimeCommandRecord, } from "../src/modules/runtime/infrastructure/session-runs/runtime-command-store.repository"; import { decideRuntimeCommandTransition, @@ -26,6 +28,7 @@ import { SqliteD1Database } from "./helpers/sqlite-d1"; const DRIVER_INSTANCE_ID = parsePlatformId("01J00000000000000000000009"); const TERMINAL_DRIVER_INSTANCE_ID = parsePlatformId("01J0000000000000000000000G"); const SESSION_RUN_ID = parsePlatformId("01J0000000000000000000000N"); +const DRIVER_GENERATION = 0; const COMMAND_IDS = { accepted: parsePlatformId("01J00000000000000000000015"), current: parsePlatformId("01J00000000000000000000017"), @@ -60,10 +63,57 @@ const EXPECTED_PREVIOUS_STATUSES = { completed: ["delivered", "accepted"], delivered: ["queued"], expired: ["queued", "delivered", "accepted"], - failed: ["delivered", "accepted"], + failed: ["queued", "delivered", "accepted"], queued: ["delivered"], } as const satisfies Record; +function createRuntimeCommandRecord( + database: D1Database, + input: Omit[1], "driverGeneration">, +) { + return persistRuntimeCommandRecord(database, { ...input, driverGeneration: DRIVER_GENERATION }); +} + +function getRuntimeCommandRecord( + database: D1Database, + driverInstanceId: DriverInstanceId, + commandId: DriverCommandId, +) { + return readRuntimeCommandRecord(database, driverInstanceId, DRIVER_GENERATION, commandId); +} + +function updateRuntimeCommandRecord( + database: D1Database, + input: Omit[1], "driverGeneration">, +) { + return updateStoredRuntimeCommandRecord(database, { + ...input, + driverGeneration: DRIVER_GENERATION, + }); +} + +function markRuntimeCommandRecordDelivered( + database: D1Database, + input: Omit[1], "driverGeneration">, +) { + return markRuntimeCommandDelivered(database, { ...input, driverGeneration: DRIVER_GENERATION }); +} + +function maintainRuntimeCommandRecords( + database: D1Database, + input: Omit[1], "driverGeneration">, +) { + return maintainRuntimeCommands(database, { ...input, driverGeneration: DRIVER_GENERATION }); +} + +function claimNextQueuedRuntimeCommandRecord( + database: D1Database, + driverInstanceId: DriverInstanceId, + connectionId: string, +) { + return claimNextQueuedRuntimeCommand(database, driverInstanceId, DRIVER_GENERATION, connectionId); +} + function createRuntimeCommandDatabase(): SqliteD1Database { const database = new SqliteD1Database({ foreignKeys: false }); @@ -71,6 +121,7 @@ function createRuntimeCommandDatabase(): SqliteD1Database { CREATE TABLE driver_instance ( command_seq_cursor integer DEFAULT 0 NOT NULL, connection_id text, + generation integer DEFAULT 0 NOT NULL, id text PRIMARY KEY NOT NULL, status text DEFAULT 'ready' NOT NULL ); @@ -79,6 +130,7 @@ function createRuntimeCommandDatabase(): SqliteD1Database { acked_at integer, completed_at integer, delivery_connection_id text, + driver_generation integer, driver_instance_id text NOT NULL, error_json text, expires_at integer, @@ -91,8 +143,60 @@ function createRuntimeCommandDatabase(): SqliteD1Database { status text NOT NULL ); + CREATE TABLE session_run ( + driver_instance_id text, + error_code text, + error_details_json text, + error_message text, + error_retryable integer, + id text PRIMARY KEY NOT NULL, + session_id text NOT NULL DEFAULT 'session-1', + status text NOT NULL + ); + + CREATE TABLE session_event ( + event_type text NOT NULL, + id text PRIMARY KEY NOT NULL, + mcp_command_id text, + run_id text, + source_event_id text NOT NULL, + tool_call_id text, + tool_input_json text, + tool_name text, + tool_output_text text, + tool_status text + ); + + CREATE TABLE external_tool_effect ( + attempt_count integer DEFAULT 0 NOT NULL, + claim_token text, + command_id text NOT NULL, + driver_instance_id text NOT NULL, + id text PRIMARY KEY NOT NULL, + idempotency_key text NOT NULL, + provider_receipt_json text, + result_json text, + status text NOT NULL, + updated_at integer NOT NULL + ); + + CREATE TABLE external_tool_effect_attempt ( + attempt integer NOT NULL, + claim_token text NOT NULL, + completed_at integer, + created_at integer NOT NULL, + effect_id text NOT NULL, + provider_receipt_json text, + result_json text, + status text NOT NULL, + PRIMARY KEY (effect_id, attempt) + ); + INSERT INTO driver_instance (id, connection_id, status) VALUES ('${DRIVER_INSTANCE_ID}', 'connection-1', 'ready'); + + INSERT INTO session_run (driver_instance_id, id, status) + VALUES ('${DRIVER_INSTANCE_ID}', '${SESSION_RUN_ID}', 'running'); `); return database; @@ -110,7 +214,510 @@ function inputStartCommand(id: DriverCommandId): RuntimeCommand { }; } +function sessionStopCommand(id: DriverCommandId): RuntimeCommand { + return { + commandId: id, + kind: "session.stop", + reason: "terminal Driver repair", + }; +} + +function mcpExecuteCommand(id: DriverCommandId): Extract { + return { + argumentsJson: '{"title":"durable"}', + commandId: id, + kind: "mcp.execute", + requestId: "request-1", + runId: SESSION_RUN_ID, + serverId: "01J0000000000000000000000Y", + toolCallId: "tool-1", + toolName: "createIssue", + }; +} + +function insertInputTerminalFact( + database: SqliteD1Database, + input: + | { error: RunError; status: "failed" } + | { error?: RunError; status: "cancelled" | "expired" } + | { status: "completed" }, +): void { + const error = "error" in input ? (input.error ?? null) : null; + const eventType = + input.status === "failed" + ? "run.failed" + : input.status === "completed" + ? "run.completed" + : "run.cancelled"; + database + .prepare( + `UPDATE session_run + SET error_code = ?, error_details_json = ?, error_message = ?, error_retryable = ?, status = ? + WHERE id = ?`, + ) + .bind( + error?.code ?? null, + error === null ? null : JSON.stringify(error.details), + error?.message ?? null, + error === null ? null : Number(error.retryable), + input.status, + SESSION_RUN_ID, + ) + .run(); + database + .prepare( + `INSERT INTO session_event (event_type, id, run_id, source_event_id) + VALUES (?, ?, ?, ?)`, + ) + .bind( + eventType, + `event-input-${input.status}`, + SESSION_RUN_ID, + `session-run-terminal:${SESSION_RUN_ID}:${eventType}`, + ) + .run(); +} + +function insertMcpTerminalFact( + database: SqliteD1Database, + command: Extract, + input: + | { outputText: string; status: "completed" } + | { error: RunError; status: "failed" } + | { status: "cancelled" }, +): void { + const outputText = + input.status === "completed" + ? input.outputText + : input.status === "failed" + ? input.error.message + : null; + database + .prepare( + `INSERT INTO session_event ( + event_type, id, mcp_command_id, run_id, source_event_id, tool_call_id, + tool_input_json, tool_name, tool_output_text, tool_status + ) VALUES ('tool.call.updated', ?, ?, ?, ?, ?, ?, ?, ?, ?)`, + ) + .bind( + `event-mcp-${input.status}`, + command.commandId, + command.runId, + `mcp.execute.${input.status}:${command.commandId}`, + command.toolCallId, + command.argumentsJson, + command.toolName, + outputText, + input.status, + ) + .run(); +} + describe("runtime command store", () => { + test("measures and writes the same serialized command and terminal payload", async () => { + const database = createRuntimeCommandDatabase(); + let commandSerializations = 0; + const command = { + ...inputStartCommand(COMMAND_IDS.first), + toJSON() { + commandSerializations += 1; + return { + ...inputStartCommand(COMMAND_IDS.first), + input: { text: `serialized-${commandSerializations}` }, + }; + }, + }; + + await createRuntimeCommandRecord(database, { + command, + driverInstanceId: DRIVER_INSTANCE_ID, + status: "accepted", + }); + expect(commandSerializations).toBe(1); + await expect( + database + .prepare("SELECT payload_json FROM driver_command WHERE id = ?") + .bind(COMMAND_IDS.first) + .first(), + ).resolves.toEqual({ + payload_json: JSON.stringify({ + ...inputStartCommand(COMMAND_IDS.first), + input: { text: "serialized-1" }, + }), + }); + + let errorSerializations = 0; + const error = { + code: "test.serialized", + details: {}, + message: "original", + retryable: false, + toJSON() { + errorSerializations += 1; + return { + code: "test.serialized", + details: {}, + message: `serialized-${errorSerializations}`, + retryable: false, + }; + }, + }; + insertInputTerminalFact(database, { + error: { + code: "test.serialized", + details: {}, + message: "serialized-1", + retryable: false, + }, + status: "failed", + }); + await expect( + updateRuntimeCommandRecord(database, { + commandId: COMMAND_IDS.first, + driverInstanceId: DRIVER_INSTANCE_ID, + error, + status: "failed", + }), + ).resolves.toMatchObject({ kind: "applied" }); + expect(errorSerializations).toBe(1); + await expect( + database + .prepare("SELECT error_json FROM driver_command WHERE id = ?") + .bind(COMMAND_IDS.first) + .first(), + ).resolves.toEqual({ + error_json: JSON.stringify({ + code: "test.serialized", + details: {}, + message: "serialized-1", + retryable: false, + }), + }); + + const resultDatabase = createRuntimeCommandDatabase(); + await createRuntimeCommandRecord(resultDatabase, { + command: { ...inputStartCommand(COMMAND_IDS.second), requestId: "serialized-1" }, + driverInstanceId: DRIVER_INSTANCE_ID, + status: "accepted", + }); + insertInputTerminalFact(resultDatabase, { status: "completed" }); + let resultSerializations = 0; + const result = { + requestId: "original", + toJSON() { + resultSerializations += 1; + return { requestId: `serialized-${resultSerializations}` }; + }, + }; + await expect( + updateRuntimeCommandRecord(resultDatabase, { + commandId: COMMAND_IDS.second, + driverInstanceId: DRIVER_INSTANCE_ID, + result, + status: "completed", + }), + ).resolves.toMatchObject({ kind: "applied" }); + expect(resultSerializations).toBe(1); + await expect( + resultDatabase + .prepare("SELECT result_json FROM driver_command WHERE id = ?") + .bind(COMMAND_IDS.second) + .first(), + ).resolves.toEqual({ result_json: '{"requestId":"serialized-1"}' }); + }); + + test("rejects invalid command JSON before allocating a sequence or writing a row", async () => { + const database = createRuntimeCommandDatabase(); + const emptyCommand = { + ...inputStartCommand(COMMAND_IDS.first), + input: { text: "" }, + }; + const emptyPayloadBytes = new TextEncoder().encode(JSON.stringify(emptyCommand)).byteLength; + const oversizedCommand = { + ...emptyCommand, + input: { + text: "x".repeat(RUNTIME_COMMAND_MAX_UTF8_BYTES - emptyPayloadBytes + 1), + }, + }; + + await expect( + createRuntimeCommandRecord(database, { + command: { ...emptyCommand, input: { text: "" } }, + driverInstanceId: DRIVER_INSTANCE_ID, + }), + ).rejects.toThrow(); + await expect( + createRuntimeCommandRecord(database, { + command: oversizedCommand, + driverInstanceId: DRIVER_INSTANCE_ID, + }), + ).rejects.toThrow(`${RUNTIME_COMMAND_MAX_UTF8_BYTES} UTF-8 bytes`); + await expect( + createRuntimeCommandRecord(database, { + command: inputStartCommand(COMMAND_IDS.second), + driverInstanceId: DRIVER_INSTANCE_ID, + status: "invalid" as never, + }), + ).rejects.toThrow(); + await expect( + database + .prepare( + "SELECT command_seq_cursor, (SELECT count(*) FROM driver_command) AS command_count FROM driver_instance WHERE id = ?", + ) + .bind(DRIVER_INSTANCE_ID) + .first(), + ).resolves.toEqual({ command_count: 0, command_seq_cursor: 0 }); + }); + + test("rejects terminal payloads that do not match the command kind without mutating it", async () => { + const database = createRuntimeCommandDatabase(); + await createRuntimeCommandRecord(database, { + command: inputStartCommand(COMMAND_IDS.first), + driverInstanceId: DRIVER_INSTANCE_ID, + status: "accepted", + }); + + await expect( + updateRuntimeCommandRecord(database, { + commandId: COMMAND_IDS.first, + driverInstanceId: DRIVER_INSTANCE_ID, + result: { + outputText: "wrong result kind", + requestId: "request-1", + serverId: "server-1", + toolName: "tool-1", + }, + status: "completed", + }), + ).rejects.toThrow(); + const validError = { + code: "test.invalid_status_payload", + details: {}, + message: "must not be stored", + retryable: false, + }; + await expect( + updateRuntimeCommandRecord(database, { + commandId: COMMAND_IDS.first, + driverInstanceId: DRIVER_INSTANCE_ID, + error: validError, + status: "completed", + }), + ).rejects.toThrow(); + await expect( + updateRuntimeCommandRecord(database, { + commandId: COMMAND_IDS.first, + driverInstanceId: DRIVER_INSTANCE_ID, + result: { requestId: "request-1" }, + status: "failed", + }), + ).rejects.toThrow(); + await expect( + updateRuntimeCommandRecord(database, { + commandId: COMMAND_IDS.first, + driverInstanceId: DRIVER_INSTANCE_ID, + error: validError, + status: "delivered", + }), + ).rejects.toThrow(); + await expect( + updateRuntimeCommandRecord(database, { + commandId: COMMAND_IDS.first, + driverInstanceId: DRIVER_INSTANCE_ID, + status: "invalid" as never, + }), + ).rejects.toThrow(); + await expect( + updateRuntimeCommandRecord(database, { + commandId: COMMAND_IDS.first, + driverInstanceId: DRIVER_INSTANCE_ID, + error: { + code: "test.invalid", + details: { nested: { invalid: true } } as never, + message: "invalid nested details", + retryable: false, + }, + status: "failed", + }), + ).rejects.toThrow(); + await expect( + database + .prepare("SELECT error_json, result_json, status FROM driver_command WHERE id = ?") + .bind(COMMAND_IDS.first) + .first(), + ).resolves.toEqual({ error_json: null, result_json: null, status: "accepted" }); + }); + + test("mirrors the succeeded effect result bytes when completing an MCP command", async () => { + const database = createRuntimeCommandDatabase(); + const command = mcpExecuteCommand(COMMAND_IDS.first); + const authoritativeResult = + '{"toolName":"tool-1","outputText":"done","serverId":"server-1","requestId":"request-1"}'; + database.execute(` + INSERT INTO driver_command ( + driver_generation, driver_instance_id, id, issued_at, kind, payload_json, seq, status + ) VALUES ( + ${DRIVER_GENERATION}, '${DRIVER_INSTANCE_ID}', '${COMMAND_IDS.first}', 1, 'mcp.execute', '${JSON.stringify(command)}', 1, 'accepted' + ); + INSERT INTO external_tool_effect ( + attempt_count, command_id, driver_instance_id, id, idempotency_key, + result_json, status, updated_at + ) VALUES ( + 1, '${COMMAND_IDS.first}', '${DRIVER_INSTANCE_ID}', 'effect-1', + 'idempotency-1', '${authoritativeResult}', 'succeeded', 1 + ); + `); + insertMcpTerminalFact(database, command, { outputText: "done", status: "completed" }); + + await expect( + updateRuntimeCommandRecord(database, { + commandId: COMMAND_IDS.first, + driverInstanceId: DRIVER_INSTANCE_ID, + result: { + isError: false, + outputText: "done", + requestId: "request-1", + serverId: "server-1", + toolName: "tool-1", + }, + status: "completed", + }), + ).resolves.toMatchObject({ kind: "applied" }); + await expect( + database + .prepare("SELECT result_json FROM driver_command WHERE id = ?") + .bind(COMMAND_IDS.first) + .first(), + ).resolves.toEqual({ result_json: authoritativeResult }); + + const exactReplay = { + isError: false, + outputText: "done", + requestId: "request-1", + serverId: "server-1", + toolName: "tool-1", + }; + await expect( + updateRuntimeCommandRecord(database, { + commandId: COMMAND_IDS.first, + driverInstanceId: DRIVER_INSTANCE_ID, + result: exactReplay, + status: "completed", + }), + ).resolves.toEqual({ kind: "duplicate", status: "completed" }); + await expect( + updateRuntimeCommandRecord(database, { + commandId: COMMAND_IDS.first, + driverInstanceId: DRIVER_INSTANCE_ID, + result: { ...exactReplay, outputText: "changed" }, + status: "completed", + }), + ).rejects.toThrow("conflicts with its durable terminal payload"); + + database.execute(` + UPDATE external_tool_effect + SET result_json = NULL, status = 'unknown' + WHERE command_id = '${COMMAND_IDS.first}' + `); + await expect( + updateRuntimeCommandRecord(database, { + commandId: COMMAND_IDS.first, + driverInstanceId: DRIVER_INSTANCE_ID, + result: exactReplay, + status: "completed", + }), + ).rejects.toThrow("has no succeeded durable effect"); + }); + + test("accepts only exact failed command replays", async () => { + const database = createRuntimeCommandDatabase(); + const error = { + code: "test.command_failed", + details: { attempt: 1, source: "driver" }, + message: "Command failed.", + retryable: true, + }; + await createRuntimeCommandRecord(database, { + command: inputStartCommand(COMMAND_IDS.first), + driverInstanceId: DRIVER_INSTANCE_ID, + status: "accepted", + }); + insertInputTerminalFact(database, { error, status: "failed" }); + await expect( + updateRuntimeCommandRecord(database, { + commandId: COMMAND_IDS.first, + driverInstanceId: DRIVER_INSTANCE_ID, + error, + status: "failed", + }), + ).resolves.toMatchObject({ kind: "applied" }); + await expect( + updateRuntimeCommandRecord(database, { + commandId: COMMAND_IDS.first, + driverInstanceId: DRIVER_INSTANCE_ID, + error: { + message: error.message, + retryable: error.retryable, + details: { source: "driver", attempt: 1 }, + code: error.code, + }, + status: "failed", + }), + ).resolves.toEqual({ kind: "duplicate", status: "failed" }); + await expect( + updateRuntimeCommandRecord(database, { + commandId: COMMAND_IDS.first, + driverInstanceId: DRIVER_INSTANCE_ID, + error: { ...error, retryable: false }, + status: "failed", + }), + ).rejects.toThrow("conflicts with its durable terminal payload"); + }); + + test("settles cancelled input commands while preserving the Run cancellation error", async () => { + const database = createRuntimeCommandDatabase(); + const error = { + code: "runtime.operation_cancelled", + details: { operationId: "operation-1" }, + message: "The runtime operation cancelled this run.", + retryable: false, + }; + await createRuntimeCommandRecord(database, { + command: inputStartCommand(COMMAND_IDS.first), + driverInstanceId: DRIVER_INSTANCE_ID, + status: "accepted", + }); + insertInputTerminalFact(database, { error, status: "cancelled" }); + + await expect( + updateRuntimeCommandRecord(database, { + commandId: COMMAND_IDS.first, + driverInstanceId: DRIVER_INSTANCE_ID, + status: "cancelled", + }), + ).resolves.toEqual({ kind: "applied", status: "cancelled" }); + await expect( + updateRuntimeCommandRecord(database, { + commandId: COMMAND_IDS.first, + driverInstanceId: DRIVER_INSTANCE_ID, + status: "cancelled", + }), + ).resolves.toEqual({ kind: "duplicate", status: "cancelled" }); + await expect( + database + .prepare( + "SELECT error_code, error_message, error_retryable, status FROM session_run WHERE id = ?", + ) + .bind(SESSION_RUN_ID) + .first(), + ).resolves.toEqual({ + error_code: error.code, + error_message: error.message, + error_retryable: 0, + status: "cancelled", + }); + }); + test("keeps runtime command transitions on the owner matrix", () => { for (const targetStatus of RUNTIME_COMMAND_STATUSES) { expect(getRuntimeCommandPreviousStatuses(targetStatus)).toEqual( @@ -410,13 +1017,13 @@ describe("runtime command store", () => { expect(recovered?.status).toBe("queued"); }); - test("repairs queued, delivered, and accepted commands globally", async () => { + test("recovers and expires commands globally without bypassing terminal orchestration", async () => { const database = createRuntimeCommandDatabase(); const nowMs = Date.now(); database.execute(` INSERT INTO driver_instance (id, connection_id, status) - VALUES ('${TERMINAL_DRIVER_INSTANCE_ID}', 'terminal-connection', 'stopped') + VALUES ('${TERMINAL_DRIVER_INSTANCE_ID}', 'terminal-connection', 'ready') `); await createRuntimeCommandRecord(database, { command: inputStartCommand(COMMAND_IDS.globalExpired), @@ -429,7 +1036,7 @@ describe("runtime command store", () => { expiresAt: nowMs + 60_000, }); await createRuntimeCommandRecord(database, { - command: inputStartCommand(COMMAND_IDS.globalAccepted), + command: sessionStopCommand(COMMAND_IDS.globalAccepted), driverInstanceId: TERMINAL_DRIVER_INSTANCE_ID, expiresAt: nowMs + 60_000, }); @@ -453,19 +1060,19 @@ describe("runtime command store", () => { UPDATE driver_instance SET connection_id = 'connection-2' WHERE id = '${DRIVER_INSTANCE_ID}' + ; + UPDATE driver_instance + SET status = 'stopped' + WHERE id = '${TERMINAL_DRIVER_INSTANCE_ID}' `); - await expect(repairRuntimeCommandRecords(database, { nowMs })).resolves.toEqual({ + const repair = await repairRuntimeCommandRecords(database, { nowMs }); + expect(repair).toEqual({ expired: { appliedCount: 1, kind: "batch_applied", status: "expired", }, - failed: { - appliedCount: 1, - kind: "batch_applied", - status: "failed", - }, recovered: { appliedCount: 1, kind: "batch_applied", @@ -473,6 +1080,10 @@ describe("runtime command store", () => { }, }); + await expect( + getRuntimeCommandRecord(database, TERMINAL_DRIVER_INSTANCE_ID, COMMAND_IDS.globalAccepted), + ).resolves.toMatchObject({ status: "accepted" }); + const expired = await getRuntimeCommandRecord( database, DRIVER_INSTANCE_ID, @@ -483,7 +1094,7 @@ describe("runtime command store", () => { DRIVER_INSTANCE_ID, COMMAND_IDS.globalStale, ); - const failed = await getRuntimeCommandRecord( + const pendingTerminalRepair = await getRuntimeCommandRecord( database, TERMINAL_DRIVER_INSTANCE_ID, COMMAND_IDS.globalAccepted, @@ -491,13 +1102,17 @@ describe("runtime command store", () => { expect(expired?.status).toBe("expired"); expect(recovered?.status).toBe("queued"); - expect(failed?.status).toBe("failed"); - expect(failed?.error?.code).toBe("driver.command_driver_terminal"); + expect(pendingTerminalRepair?.status).toBe("accepted"); + expect(pendingTerminalRepair?.error).toBeNull(); }); test("expires undelivered input commands for one run", async () => { const database = createRuntimeCommandDatabase(); const otherRunId = parsePlatformId("01J0000000000000000000000P"); + database.execute(` + INSERT INTO session_run (driver_instance_id, id, status) + VALUES ('${DRIVER_INSTANCE_ID}', '${otherRunId}', 'running') + `); await createRuntimeCommandRecord(database, { command: inputStartCommand(COMMAND_IDS.currentRunQueued), @@ -635,12 +1250,14 @@ describe("runtime command store", () => { expiresAt: Date.now() + 60_000, }); await claimNextQueuedRuntimeCommandRecord(database, DRIVER_INSTANCE_ID, "connection-1"); + insertInputTerminalFact(database, { status: "completed" }); await expect( updateRuntimeCommandRecord(database, { commandId: COMMAND_IDS.illegal, deliveryConnectionId: "connection-1", driverInstanceId: DRIVER_INSTANCE_ID, + result: { requestId: inputStartCommand(COMMAND_IDS.illegal).requestId }, status: "completed", }), ).resolves.toEqual({ diff --git a/apps/api/tests/runtime-conversation-idle-sweep.test.ts b/apps/api/tests/runtime-conversation-idle-sweep.test.ts index 50ca5a29..5a4638e6 100644 --- a/apps/api/tests/runtime-conversation-idle-sweep.test.ts +++ b/apps/api/tests/runtime-conversation-idle-sweep.test.ts @@ -15,10 +15,12 @@ function createDatabase(): SqliteD1Database { database.execute(` CREATE TABLE sandbox_session ( cloudflare_session_id text NOT NULL, + cleanup_operation_id text, created_at integer NOT NULL, cwd text NOT NULL, origin_json text NOT NULL, sandbox_id text NOT NULL, + sandbox_incarnation integer DEFAULT 0 NOT NULL, session_id text PRIMARY KEY NOT NULL, status text NOT NULL, updated_at integer NOT NULL @@ -37,6 +39,7 @@ function createDatabase(): SqliteD1Database { CREATE TABLE session_run ( id text PRIMARY KEY NOT NULL, driver_instance_id text, + session_id text NOT NULL, status text NOT NULL ); @@ -44,6 +47,7 @@ function createDatabase(): SqliteD1Database { id text PRIMARY KEY NOT NULL, kind text NOT NULL, last_run_id text, + runtime_provisioning_operation_id text, workspace_checkpoint_required integer DEFAULT 0 NOT NULL ); @@ -51,8 +55,10 @@ function createDatabase(): SqliteD1Database { dir text NOT NULL, id text PRIMARY KEY NOT NULL, sandbox_id text NOT NULL, + sandbox_incarnation integer NOT NULL, session_run_id text, - status text NOT NULL + status text NOT NULL, + workspace_session_id text ); `); @@ -86,8 +92,9 @@ async function insertConversation( await database .prepare( `INSERT INTO sandbox_session ( - cloudflare_session_id, created_at, cwd, origin_json, sandbox_id, session_id, status, updated_at - ) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`, + cloudflare_session_id, created_at, cwd, origin_json, sandbox_id, sandbox_incarnation, + session_id, status, updated_at + ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`, ) .bind( `cf-${input.sessionId}`, @@ -95,6 +102,7 @@ async function insertConversation( "cwd", "{}", input.sandboxId, + 1, input.sessionId, input.status, input.updatedAt, @@ -104,15 +112,17 @@ async function insertConversation( async function insertActiveRunLease( database: D1Database, - input: { readonly runId: string; readonly sandboxId: string }, + input: { readonly runId: string; readonly sandboxId: string; readonly sessionId: string }, ): Promise { await database .prepare("INSERT INTO driver_instance (id, sandbox_id) VALUES (?, ?)") .bind(`driver-${input.runId}`, input.sandboxId) .run(); await database - .prepare("INSERT INTO session_run (id, driver_instance_id, status) VALUES (?, ?, ?)") - .bind(input.runId, `driver-${input.runId}`, "running") + .prepare( + "INSERT INTO session_run (id, driver_instance_id, session_id, status) VALUES (?, ?, ?, ?)", + ) + .bind(input.runId, `driver-${input.runId}`, input.sessionId, "running") .run(); } @@ -127,8 +137,10 @@ describe("idle session-scoped conversation sweep", () => { updatedAt: NOW - GRACE_MS - 1, }); await database - .prepare("INSERT INTO session_run (id, driver_instance_id, status) VALUES (?, NULL, ?)") - .bind("run-checkpoint", "completed") + .prepare( + "INSERT INTO session_run (id, driver_instance_id, session_id, status) VALUES (?, NULL, ?, ?)", + ) + .bind("run-checkpoint", "session-checkpoint", "completed") .run(); await database .prepare("UPDATE session SET last_run_id = ?, workspace_checkpoint_required = 1 WHERE id = ?") @@ -146,16 +158,27 @@ describe("idle session-scoped conversation sweep", () => { idleSinceLte: NOW - GRACE_MS, now: NOW, runtimeSubjectId: "sb-checkpoint" as never, + sandboxIncarnation: 1, sandboxSessionId: "cf-session-checkpoint" as never, sessionId: "session-checkpoint" as never, }), - ).resolves.toBe(false); + ).resolves.toBeNull(); await database .prepare( - "INSERT INTO sandbox_backup (dir, id, sandbox_id, session_run_id, status) VALUES (?, ?, ?, ?, ?)", + `INSERT INTO sandbox_backup ( + dir, id, sandbox_id, sandbox_incarnation, session_run_id, status, workspace_session_id + ) VALUES (?, ?, ?, ?, ?, ?, ?)`, + ) + .bind( + "cwd", + "backup-checkpoint", + "sb-checkpoint", + 1, + "run-checkpoint", + "ready", + "session-checkpoint", ) - .bind("cwd", "backup-checkpoint", "sb-checkpoint", "run-checkpoint", "ready") .run(); await expect( @@ -176,8 +199,10 @@ describe("idle session-scoped conversation sweep", () => { updatedAt: NOW - GRACE_MS - 1, }); await database - .prepare("INSERT INTO session_run (id, driver_instance_id, status) VALUES (?, NULL, ?)") - .bind("run-legacy", "completed") + .prepare( + "INSERT INTO session_run (id, driver_instance_id, session_id, status) VALUES (?, NULL, ?, ?)", + ) + .bind("run-legacy", "session-legacy", "completed") .run(); await database .prepare("UPDATE session SET last_run_id = ? WHERE id = ?") @@ -195,10 +220,11 @@ describe("idle session-scoped conversation sweep", () => { idleSinceLte: NOW - GRACE_MS, now: NOW, runtimeSubjectId: "sb-legacy" as never, + sandboxIncarnation: 1, sandboxSessionId: "cf-session-legacy" as never, sessionId: "session-legacy" as never, }), - ).resolves.toBe(true); + ).resolves.not.toBeNull(); }); test("lists only idle active cattle conversations without a run lease", async () => { @@ -238,7 +264,11 @@ describe("idle session-scoped conversation sweep", () => { status: "active", updatedAt: NOW - GRACE_MS - 1, }); - await insertActiveRunLease(database, { runId: "run-busy", sandboxId: "sb-busy" }); + await insertActiveRunLease(database, { + runId: "run-busy", + sandboxId: "sb-busy", + sessionId: "session-busy", + }); const idle = await listIdleSessionScopedConversationSessions(database, { idleSinceLte: NOW - GRACE_MS, @@ -248,7 +278,7 @@ describe("idle session-scoped conversation sweep", () => { expect(idle).toEqual([{ sandboxId: "sb-idle", sessionId: "session-idle" }]); }); - test("atomic claim closes an idle conversation but loses to any re-activation", async () => { + test("atomic claim schedules an idle conversation cleanup but loses to any re-activation", async () => { const database = createDatabase(); const idleSinceLte = NOW - GRACE_MS; const claim = (sandboxId: string, sessionId: string, sandboxSessionId: string) => @@ -256,6 +286,7 @@ describe("idle session-scoped conversation sweep", () => { idleSinceLte, now: NOW, runtimeSubjectId: sandboxId as never, + sandboxIncarnation: 1, sandboxSessionId: sandboxSessionId as never, sessionId: sessionId as never, }); @@ -267,7 +298,7 @@ describe("idle session-scoped conversation sweep", () => { .first<{ status: string }>() )?.status; - // (1) still-idle, same session instance, no lease → claim wins, row closed. + // (1) still-idle, same session instance, no lease → claim wins. await insertConversation(database, { kind: "cattle", sandboxId: "sb-a", @@ -275,8 +306,8 @@ describe("idle session-scoped conversation sweep", () => { status: "active", updatedAt: NOW - GRACE_MS - 1, }); - expect(await claim("sb-a", "sess-a", "cf-sess-a")).toBe(true); - expect(await statusOf("sess-a")).toBe("closed"); + expect(await claim("sb-a", "sess-a", "cf-sess-a")).not.toBeNull(); + expect(await statusOf("sess-a")).toBe("cleanup_pending"); // (2) a follow-up refreshed updated_at past the grace → claim loses, untouched. await insertConversation(database, { @@ -286,7 +317,7 @@ describe("idle session-scoped conversation sweep", () => { status: "active", updatedAt: NOW, // refreshed by ensureSandboxConversationSession }); - expect(await claim("sb-b", "sess-b", "cf-sess-b")).toBe(false); + expect(await claim("sb-b", "sess-b", "cf-sess-b")).toBeNull(); expect(await statusOf("sess-b")).toBe("active"); // (3) the session was rebuilt (new cloudflare_session_id) → claim loses. @@ -297,7 +328,7 @@ describe("idle session-scoped conversation sweep", () => { status: "active", updatedAt: NOW - GRACE_MS - 1, }); - expect(await claim("sb-c", "sess-c", "cf-STALE")).toBe(false); + expect(await claim("sb-c", "sess-c", "cf-STALE")).toBeNull(); expect(await statusOf("sess-c")).toBe("active"); // (4) an active run lease appeared → claim loses. @@ -308,8 +339,12 @@ describe("idle session-scoped conversation sweep", () => { status: "active", updatedAt: NOW - GRACE_MS - 1, }); - await insertActiveRunLease(database, { runId: "run-d", sandboxId: "sb-d" }); - expect(await claim("sb-d", "sess-d", "cf-sess-d")).toBe(false); + await insertActiveRunLease(database, { + runId: "run-d", + sandboxId: "sb-d", + sessionId: "sess-d", + }); + expect(await claim("sb-d", "sess-d", "cf-sess-d")).toBeNull(); expect(await statusOf("sess-d")).toBe("active"); }); }); diff --git a/apps/api/tests/runtime-conversation-session-record.test.ts b/apps/api/tests/runtime-conversation-session-record.test.ts index 505e278e..2ae3516a 100644 --- a/apps/api/tests/runtime-conversation-session-record.test.ts +++ b/apps/api/tests/runtime-conversation-session-record.test.ts @@ -15,6 +15,7 @@ function createConversationSessionDatabase(): SqliteD1Database { cwd text NOT NULL, origin_json text NOT NULL, sandbox_id text NOT NULL, + sandbox_incarnation integer DEFAULT 0 NOT NULL, session_id text PRIMARY KEY NOT NULL, status text NOT NULL ); @@ -25,7 +26,8 @@ function createConversationSessionDatabase(): SqliteD1Database { id text PRIMARY KEY NOT NULL, sandbox_id text NOT NULL, session_run_id text, - status text NOT NULL + status text NOT NULL, + workspace_session_id text ); CREATE TABLE session ( @@ -71,13 +73,21 @@ async function insertBackup( readonly dir: string; readonly id: string; readonly status: string; + readonly workspaceSessionId?: string | null; }, ): Promise { await database .prepare( - "INSERT INTO sandbox_backup (created_at, dir, id, sandbox_id, status) VALUES (?, ?, ?, ?, ?)", + "INSERT INTO sandbox_backup (created_at, dir, id, sandbox_id, status, workspace_session_id) VALUES (?, ?, ?, ?, ?, ?)", + ) + .bind( + input.createdAt, + input.dir, + input.id, + "01J0000000000000000000000D", + input.status, + input.workspaceSessionId === undefined ? "session-1" : input.workspaceSessionId, ) - .bind(input.createdAt, input.dir, input.id, "01J0000000000000000000000D", input.status) .run(); } @@ -105,6 +115,20 @@ describe("runtime conversation session record", () => { }); await insertBackup(database, { createdAt: 4, + dir: SESSION_CWD, + id: "backup-other-owner", + status: "ready", + workspaceSessionId: "session-2", + }); + await insertBackup(database, { + createdAt: 5, + dir: SESSION_CWD, + id: "backup-legacy-unowned", + status: "ready", + workspaceSessionId: null, + }); + await insertBackup(database, { + createdAt: 6, dir: OTHER_CWD, id: "backup-other", status: "ready", diff --git a/apps/api/tests/runtime-event-persistence-compactor.test.ts b/apps/api/tests/runtime-event-persistence-compactor.test.ts deleted file mode 100644 index 9c84340e..00000000 --- a/apps/api/tests/runtime-event-persistence-compactor.test.ts +++ /dev/null @@ -1,551 +0,0 @@ -import { describe, expect, test } from "bun:test"; - -import type { DriverEventEnvelope } from "@mosoo/agent-driver/events"; -import { createRuntimeEvent } from "@mosoo/runtime-events"; -import type { RuntimeEventEnvelope, RuntimeEventKind } from "@mosoo/runtime-events"; - -import type { ProjectedRuntimeEventRecord } from "../src/modules/runtime/infrastructure/driver-instance/event-types"; -import { RuntimeEventPersistenceCompactor } from "../src/modules/runtime/infrastructure/driver-instance/runtime-event-persistence-compactor"; -import { filterDurablyAcceptedRuntimeStreamReplays } from "../src/modules/runtime/infrastructure/driver-instance/runtime-event-replay-filter"; - -function runtimeEvent(input: { - delivery?: RuntimeEventEnvelope["delivery"]; - id: string; - kind: RuntimeEventKind; - occurredAtMs: number; - payload: unknown; - runId?: string; -}): RuntimeEventEnvelope { - return createRuntimeEvent({ - id: input.id, - kind: input.kind, - occurredAt: new Date(input.occurredAtMs).toISOString(), - payload: input.payload, - ...(input.delivery === undefined ? {} : { delivery: input.delivery }), - ...(input.runId === undefined ? {} : { runId: input.runId }), - sessionId: "session-1", - }); -} - -function record(input: { - delivery?: RuntimeEventEnvelope["delivery"]; - id: string; - kind: RuntimeEventKind; - occurredAtMs: number; - payload: unknown; - runId?: string; -}): ProjectedRuntimeEventRecord { - const event = runtimeEvent(input); - - return { - event, - occurredAt: input.occurredAtMs, - sourceEventId: `source-${input.id}`, - }; -} - -function envelope(projectedRecord: ProjectedRuntimeEventRecord): DriverEventEnvelope { - return { - event: projectedRecord.event, - eventId: projectedRecord.sourceEventId ?? projectedRecord.event.id, - occurredAt: projectedRecord.occurredAt, - }; -} - -describe("runtime event persistence compactor", () => { - test("stores message and thinking streams as complete content events", () => { - const compactor = new RuntimeEventPersistenceCompactor(); - - expect( - compactor.compact([ - record({ - id: "message-start", - kind: "message.started", - occurredAtMs: 1_000, - payload: { messageId: "message-1", role: "agent" }, - runId: "run-1", - }), - record({ - id: "message-delta-1", - kind: "message.delta", - occurredAtMs: 1_010, - payload: { contentDelta: "Hello ", messageId: "message-1", role: "agent" }, - runId: "run-1", - }), - ]), - ).toEqual([]); - - const compacted = compactor.compact([ - record({ - id: "message-delta-2", - kind: "message.delta", - occurredAtMs: 1_020, - payload: { contentDelta: "world", messageId: "message-1", role: "agent" }, - runId: "run-1", - }), - record({ - id: "message-end", - kind: "message.completed", - occurredAtMs: 1_030, - payload: { messageId: "message-1", role: "agent" }, - runId: "run-1", - }), - record({ - id: "thought-start", - kind: "thought.started", - occurredAtMs: 1_040, - payload: { channel: "summary", thoughtId: "thought-1" }, - runId: "run-1", - }), - record({ - id: "thought-delta", - kind: "thought.delta", - occurredAtMs: 1_050, - payload: { channel: "summary", contentDelta: "Check inputs", thoughtId: "thought-1" }, - runId: "run-1", - }), - record({ - id: "thought-end", - kind: "thought.completed", - occurredAtMs: 1_060, - payload: { channel: "summary", thoughtId: "thought-1" }, - runId: "run-1", - }), - ]); - - expect(compacted.map((entry) => entry.event.kind)).toEqual([ - "message.added", - "thought.completed", - ]); - expect(compacted[0]?.occurredAt).toBe(1_000); - expect(compacted[0]?.sourceEventId).toBe("source-message-end"); - expect(compacted[0]?.event.payload).toMatchObject({ - content: "Hello world", - messageId: "message-1", - role: "agent", - }); - expect(compacted[1]?.event.payload).toMatchObject({ - channel: "summary", - content: "Check inputs", - thoughtId: "thought-1", - }); - }); - - test("appends repeated text deltas instead of treating them as snapshots", () => { - const compactor = new RuntimeEventPersistenceCompactor(); - - const compacted = compactor.compact([ - record({ - id: "message-delta-1", - kind: "message.delta", - occurredAtMs: 1_000, - payload: { contentDelta: "a", messageId: "message-1", role: "agent" }, - runId: "run-1", - }), - record({ - id: "message-delta-2", - kind: "message.delta", - occurredAtMs: 1_010, - payload: { contentDelta: "a", messageId: "message-1", role: "agent" }, - runId: "run-1", - }), - record({ - id: "message-end", - kind: "message.completed", - occurredAtMs: 1_020, - payload: { messageId: "message-1", role: "agent" }, - runId: "run-1", - }), - ]); - - expect(compacted[0]?.event.payload).toMatchObject({ - content: "aa", - messageId: "message-1", - }); - }); - - test("preserves structured message.added text blocks", () => { - const compactor = new RuntimeEventPersistenceCompactor(); - - const compacted = compactor.compact([ - record({ - id: "user-message", - kind: "message.added", - occurredAtMs: 1_000, - payload: { - content: [ - { text: "hello ", type: "text" }, - { text: "world", type: "text" }, - ], - messageId: "message-1", - role: "user", - }, - runId: "run-1", - }), - ]); - - expect(compacted.map((entry) => entry.event.kind)).toEqual(["message.added"]); - expect(compacted[0]?.event.payload).toMatchObject({ - content: "hello world", - messageId: "message-1", - role: "user", - }); - }); - - test("preserves an existing message role when terminal events omit it", () => { - const compactor = new RuntimeEventPersistenceCompactor(); - - const compacted = compactor.compact([ - record({ - id: "message-start", - kind: "message.started", - occurredAtMs: 1_000, - payload: { messageId: "message-1", role: "user" }, - runId: "run-1", - }), - record({ - id: "message-delta", - kind: "message.delta", - occurredAtMs: 1_010, - payload: { contentDelta: "hello", messageId: "message-1" }, - runId: "run-1", - }), - record({ - id: "message-end", - kind: "message.completed", - occurredAtMs: 1_020, - payload: { messageId: "message-1" }, - runId: "run-1", - }), - ]); - - expect(compacted[0]?.event.payload).toMatchObject({ - content: "hello", - messageId: "message-1", - role: "user", - }); - }); - - test("stores tool call snapshots as one completed call", () => { - const compactor = new RuntimeEventPersistenceCompactor(); - - expect( - compactor.compact([ - record({ - id: "tool-start", - kind: "item.started", - occurredAtMs: 2_000, - payload: { - itemId: "tool-1", - itemType: "tool_call", - parentMessageId: "message-1", - title: "Search", - }, - runId: "run-1", - }), - record({ - id: "tool-running", - kind: "tool.call.updated", - occurredAtMs: 2_010, - payload: { - rawInput: '{"q":', - status: "running", - toolCallId: "tool-1", - }, - runId: "run-1", - }), - ]), - ).toEqual([]); - - const compacted = compactor.compact([ - record({ - id: "tool-input", - kind: "tool.call.updated", - occurredAtMs: 2_020, - payload: { - rawInput: '"cats"}', - status: "running", - toolCallId: "tool-1", - }, - runId: "run-1", - }), - record({ - id: "tool-output", - kind: "tool.call.updated", - occurredAtMs: 2_050, - payload: { - messageId: "message-1", - rawOutput: "ok", - status: "completed", - toolCallId: "tool-1", - }, - runId: "run-1", - }), - record({ - id: "tool-end", - kind: "item.completed", - occurredAtMs: 2_060, - payload: { - itemId: "tool-1", - itemType: "tool_call", - status: "completed", - }, - runId: "run-1", - }), - ]); - - expect(compacted.map((entry) => entry.event.kind)).toEqual(["tool.call.updated"]); - expect(compacted[0]?.occurredAt).toBe(2_000); - expect(compacted[0]?.sourceEventId).toBe("source-tool-end"); - expect(compacted[0]?.event.payload).toMatchObject({ - messageId: "message-1", - parentMessageId: "message-1", - rawInput: '{"q":"cats"}', - rawOutput: "ok", - status: "completed", - title: "Search", - toolCallId: "tool-1", - }); - }); - - test("waits until run end before storing repeated completed tool snapshots", () => { - const compactor = new RuntimeEventPersistenceCompactor(); - - expect( - compactor.compact([ - record({ - id: "tool-start", - kind: "item.started", - occurredAtMs: 3_000, - payload: { - itemId: "tool-1", - itemType: "tool_call", - title: "Shell", - }, - runId: "run-1", - }), - ]), - ).toEqual([]); - expect( - compactor.compact([ - record({ - id: "tool-snapshot-1", - kind: "tool.call.updated", - occurredAtMs: 3_010, - payload: { - rawOutput: "one", - status: "completed", - toolCallId: "tool-1", - }, - runId: "run-1", - }), - ]), - ).toEqual([]); - expect( - compactor.compact([ - record({ - id: "tool-snapshot-2", - kind: "tool.call.updated", - occurredAtMs: 3_020, - payload: { - rawOutput: "one two", - status: "completed", - toolCallId: "tool-1", - }, - runId: "run-1", - }), - ]), - ).toEqual([]); - - const compacted = compactor.compact([ - record({ - id: "run-end", - kind: "run.completed", - occurredAtMs: 3_100, - payload: { stopReason: "end_turn" }, - runId: "run-1", - }), - ]); - - expect(compacted.map((entry) => entry.event.kind)).toEqual([ - "tool.call.updated", - "run.completed", - ]); - expect(compacted[0]?.event.payload).toMatchObject({ - rawOutput: "one two", - status: "completed", - title: "Shell", - toolCallId: "tool-1", - }); - }); - - test("stores terminal run failure as the pending tool result", () => { - const compactor = new RuntimeEventPersistenceCompactor(); - - expect( - compactor.compact([ - record({ - id: "tool-start", - kind: "item.started", - occurredAtMs: 3_000, - payload: { - itemId: "tool-1", - itemType: "tool_call", - title: "Shell", - }, - runId: "run-1", - }), - record({ - id: "tool-running", - kind: "tool.call.updated", - occurredAtMs: 3_010, - payload: { - rawInput: '{"cmd":"pwd"}', - status: "running", - toolCallId: "tool-1", - }, - runId: "run-1", - }), - ]), - ).toEqual([]); - - const compacted = compactor.compact([ - record({ - id: "run-failed", - kind: "run.failed", - occurredAtMs: 3_100, - payload: { - error: { - code: "runtime.failed", - message: "Runtime driver control socket is not connected.", - }, - }, - runId: "run-1", - }), - ]); - - expect(compacted.map((entry) => entry.event.kind)).toEqual(["tool.call.updated", "run.failed"]); - expect(compacted[0]?.event.payload).toMatchObject({ - rawInput: '{"cmd":"pwd"}', - rawOutput: - "Tool failed before returning a result: Runtime driver control socket is not connected.", - status: "failed", - title: "Shell", - toolCallId: "tool-1", - }); - }); - - test("drops stream fragment replays shadowed by durable terminal receipts", () => { - const messageStart = record({ - id: "message-start", - kind: "message.started", - occurredAtMs: 4_000, - payload: { messageId: "message-1", role: "agent" }, - runId: "run-1", - }); - const messageDelta = record({ - id: "message-delta", - kind: "message.delta", - occurredAtMs: 4_010, - payload: { contentDelta: "Hello", messageId: "message-1", role: "agent" }, - runId: "run-1", - }); - const messageEnd = record({ - id: "message-end", - kind: "message.completed", - occurredAtMs: 4_020, - payload: { messageId: "message-1", role: "agent" }, - runId: "run-1", - }); - - expect( - filterDurablyAcceptedRuntimeStreamReplays( - [messageStart, messageDelta, messageEnd].map(envelope), - new Set(["source-message-end"]), - ), - ).toEqual([]); - }); - - test("keeps a later final assistant message after a completed progress message replays", () => { - const progressStart = record({ - id: "progress-start", - kind: "message.started", - occurredAtMs: 4_000, - payload: { messageId: "message-progress", role: "agent" }, - runId: "run-1", - }); - const progressDelta = record({ - id: "progress-delta", - kind: "message.delta", - occurredAtMs: 4_010, - payload: { contentDelta: "进度:工具已完成。", messageId: "message-progress", role: "agent" }, - runId: "run-1", - }); - const progressEnd = record({ - id: "progress-end", - kind: "message.completed", - occurredAtMs: 4_020, - payload: { messageId: "message-progress", role: "agent" }, - runId: "run-1", - }); - const finalStart = record({ - id: "final-start", - kind: "message.started", - occurredAtMs: 4_030, - payload: { messageId: "message-final", role: "agent" }, - runId: "run-1", - }); - const finalDelta = record({ - id: "final-delta", - kind: "message.delta", - occurredAtMs: 4_040, - payload: { contentDelta: "最终:中文表格完整。", messageId: "message-final", role: "agent" }, - runId: "run-1", - }); - const finalEnd = record({ - id: "final-end", - kind: "message.completed", - occurredAtMs: 4_050, - payload: { messageId: "message-final", role: "agent" }, - runId: "run-1", - }); - - expect( - filterDurablyAcceptedRuntimeStreamReplays( - [progressStart, progressDelta, progressEnd, finalStart, finalDelta, finalEnd].map(envelope), - new Set(["source-progress-end"]), - ).map((entry) => entry.event.id), - ).toEqual(["final-start", "final-delta", "final-end"]); - }); - - test("drops open stream fragment replays shadowed by a durable run terminal receipt", () => { - const messageDelta = record({ - id: "message-delta", - kind: "message.delta", - occurredAtMs: 5_000, - payload: { contentDelta: "partial", messageId: "message-1", role: "agent" }, - runId: "run-1", - }); - const toolDelta = record({ - id: "tool-delta", - kind: "tool.call.updated", - occurredAtMs: 5_010, - payload: { rawInput: "{}", status: "running", toolCallId: "tool-1" }, - runId: "run-1", - }); - const runEnd = record({ - id: "run-end", - kind: "run.completed", - occurredAtMs: 5_100, - payload: { stopReason: "end_turn" }, - runId: "run-1", - }); - - expect( - filterDurablyAcceptedRuntimeStreamReplays( - [messageDelta, toolDelta, runEnd].map(envelope), - new Set(["source-run-end"]), - ), - ).toEqual([]); - }); -}); diff --git a/apps/api/tests/runtime-final-output-ingestion.test.ts b/apps/api/tests/runtime-final-output-ingestion.test.ts index ab3ee3a0..56ff94c0 100644 --- a/apps/api/tests/runtime-final-output-ingestion.test.ts +++ b/apps/api/tests/runtime-final-output-ingestion.test.ts @@ -1,46 +1,75 @@ import { afterEach, describe, expect, test } from "bun:test"; +import { createMcpExecuteFailedEventIdentity } from "@mosoo/agent-driver/events"; import type { DriverEventEnvelope } from "@mosoo/agent-driver/events"; -import type { DriverEventReceipt } from "@mosoo/agent-driver/orpc"; import { createPlatformId } from "@mosoo/id"; import type { DriverInstanceId, + DriverCommandId, RuntimeEventId, + RuntimeOperationId, SessionId, SessionMessageId, SessionRunId, } from "@mosoo/id"; -import { createRuntimeEvent } from "@mosoo/runtime-events"; +import { createRuntimeEvent, createRuntimeToolResultMessageId } from "@mosoo/runtime-events"; import type { RuntimeEventKind } from "@mosoo/runtime-events"; import { readPublicThreadRunFinalOutput } from "../src/modules/public-api/public-thread-events"; import { - createReceiptsForDriverEvents, - filterNewDriverEvents, - readReceiptsForProcessedDriverEvents, - rememberDriverEventReceipts, -} from "../src/modules/runtime/infrastructure/driver-instance/driver-event-receipts"; + recordCanonicalSessionRunFailure, + recordCanonicalSessionRunTerminal, +} from "../src/modules/runtime/application/session-runs/session-run-terminal-failure.service"; +import { + createFailedSessionRunRuntimeEvent, + createSessionRunUpdatedEvent, +} from "../src/modules/runtime/application/session-runs/session-run-view-events.service"; +import { prepareAssistantMessageProjection } from "../src/modules/runtime/infrastructure/driver-instance/assistant-message-projection"; +import { commitTerminalRunProjection } from "../src/modules/runtime/infrastructure/driver-instance/completed-run-commit.repository"; +import { canonicalizeDriverEventEnvelope } from "../src/modules/runtime/infrastructure/driver-instance/driver-event-canonicalization"; import type { RuntimeSessionLink } from "../src/modules/runtime/infrastructure/driver-instance/event-types"; import { DriverInstanceRpcEventIngestionController } from "../src/modules/runtime/infrastructure/driver-instance/rpc-event-ingestion-controller"; +import { + cleanupRuntimeArtifactAttempts, + parseRuntimeArtifactManifest, +} from "../src/modules/runtime/infrastructure/driver-instance/runtime-artifact-attempt.repository"; +import type { RuntimeArtifactManifest } from "../src/modules/runtime/infrastructure/driver-instance/runtime-artifact-attempt.repository"; +import { + RUNTIME_SESSION_OUTPUT_MAX_FILE_BYTES, + RUNTIME_SESSION_OUTPUT_MAX_TOTAL_BYTES, +} from "../src/modules/runtime/infrastructure/driver-instance/runtime-session-outputs"; import { RuntimeSessionViewCache } from "../src/modules/runtime/infrastructure/driver-instance/runtime-session-view-cache"; -import { recordDriverInstanceCompletion } from "../src/modules/runtime/infrastructure/driver-instance/terminal-driver-events"; +import { + recordDriverInstanceCompletion, + recordDriverInstanceFailure, +} from "../src/modules/runtime/infrastructure/driver-instance/terminal-driver-events"; +import { createRuntimeCommandRecord } from "../src/modules/runtime/infrastructure/session-runs/runtime-command-store.repository"; +import { getSessionRunSummary } from "../src/modules/runtime/infrastructure/session-runs/session-run-store.repository"; import { loadSessionViewerState } from "../src/modules/sessions/application/session-live-state.service"; +import type { SessionDeliveryEvent } from "../src/modules/sessions/application/session-live-state.service"; import { createSessionProcessEventsFromSessionEventRows } from "../src/modules/sessions/application/session-process-events.service"; import type { SessionEventProcessRow } from "../src/modules/sessions/application/session-process-events.service"; +import { getSessionRuntimeRecoveryMessages } from "../src/modules/sessions/application/session-runtime-recovery-query.service"; +import { syncSessionViewerState } from "../src/modules/sessions/infrastructure/session/client"; import { setServerProductAnalyticsTransportForTests } from "../src/platform/analytics/product-analytics"; import type { ApiBindings } from "../src/platform/cloudflare/worker-types"; import { createPublicHttpContractDatabase, - createPublicHttpTestBindings, + createPublicHttpTestBindings as createBasePublicHttpTestBindings, + insertNonOwnerSession, insertOwnerSession, PUBLIC_API_TEST_IDS, + PublicApiMemoryFileBucket, } from "./helpers/public-api-http-test-fixture"; import type { SqliteD1Database } from "./helpers/public-api-http-test-fixture"; +import { createRuntimeOutputSandbox } from "./helpers/runtime-output-sandbox"; +import type { RuntimeOutputSandboxOptions } from "./helpers/runtime-output-sandbox"; +import { insertRuntimeEvent } from "./public-thread-api-fixtures"; const DRIVER_ID = PUBLIC_API_TEST_IDS.driverOwner as DriverInstanceId; const RUN_ID = PUBLIC_API_TEST_IDS.run as SessionRunId; const SESSION_ID = PUBLIC_API_TEST_IDS.ownerSession as SessionId; -const TERMINAL_SOURCE_EVENT_ID = "canary:run-completed"; +const TERMINAL_SOURCE_EVENT_ID = `session-run-terminal:${RUN_ID}:run.completed`; const CANARY_LINES = Array.from({ length: 160 }, (_, index) => { const lineNumber = String(index + 1).padStart(3, "0"); return `${lineNumber}|中文长文本校验-Aa${index % 10}-表格字符|END${lineNumber}`; @@ -61,6 +90,8 @@ const FINAL_TEXT_LINES = [ "CANARY-FINAL-END", ]; const FINAL_TEXT = FINAL_TEXT_LINES.join("\n"); +const LARGE_FINAL_TEXT = `${FINAL_TEXT}\n${"0123456789abcdef中文\n".repeat(90_000)}`; +const LARGE_FINAL_TEXT_CHUNK_CHARACTERS = 300; const PROGRESS_TEXTS = [ "进度 1:正在读取上游报告。", "进度 2:工具调用已经完成。", @@ -72,35 +103,18 @@ afterEach(() => { }); interface TestDriverState { - createDriverEventReceipts(events: readonly DriverEventEnvelope[]): DriverEventReceipt[]; - filterUnprocessedDriverEvents(events: readonly DriverEventEnvelope[]): DriverEventEnvelope[]; hello: { pid: number }; - readProcessedDriverEventReceipts(events: readonly DriverEventEnvelope[]): DriverEventReceipt[]; - rememberProcessedDriverEventReceipts(receipts: DriverEventReceipt[]): void; + requireDriverGeneration(): number; requireDriverInstanceId(): DriverInstanceId; runtimeSessionLink: RuntimeSessionLink | null; setRuntimeSessionLink(link: RuntimeSessionLink): void; } function createDriverState(): TestDriverState { - const processedReceipts = new Map(); - let nextSeq = 0; - return { - createDriverEventReceipts(events) { - const result = createReceiptsForDriverEvents({ events, nextSeq }); - nextSeq = result.nextSeq; - return result.receipts; - }, - filterUnprocessedDriverEvents(events) { - return filterNewDriverEvents({ events, processedReceipts }); - }, hello: { pid: 1 }, - readProcessedDriverEventReceipts(events) { - return readReceiptsForProcessedDriverEvents({ events, processedReceipts }); - }, - rememberProcessedDriverEventReceipts(receipts) { - rememberDriverEventReceipts({ processedReceipts, receipts }); + requireDriverGeneration() { + return 0; }, requireDriverInstanceId() { return DRIVER_ID; @@ -112,38 +126,105 @@ function createDriverState(): TestDriverState { }; } -function createController(bindings: ApiBindings): DriverInstanceRpcEventIngestionController { +function createController( + bindings: ApiBindings, + enqueue: (sessionId: SessionId | null, events: SessionDeliveryEvent[]) => void = () => undefined, +): DriverInstanceRpcEventIngestionController { return new DriverInstanceRpcEventIngestionController({ env: bindings, state: createDriverState(), viewCache: new RuntimeSessionViewCache(), viewerEventDelivery: { - enqueue: () => undefined, + enqueue, + flush: async () => {}, + flushSafely: async () => {}, + requestStateSync: (sessionId) => { + void syncSessionViewerState(bindings, sessionId); + }, + resetAfterFlush: () => {}, }, } as never); } +function createSessionSyncNamespace(syncedSessionIds: string[]): ApiBindings["Session"] { + return { + get: () => ({ + syncViewers: async (sessionId: string) => { + syncedSessionIds.push(sessionId); + }, + }), + idFromName: (name: string) => name, + } as never; +} + +interface ArtifactSandboxOptions extends RuntimeOutputSandboxOptions { + readonly resolveError?: Error; +} + +type PublicHttpTestBindingOptions = NonNullable< + Parameters[1] +> & { readonly artifactSandbox?: ArtifactSandboxOptions }; + +function createPublicHttpTestBindings( + database: D1Database, + options: PublicHttpTestBindingOptions = {}, +): Record { + const { artifactSandbox, ...baseOptions } = options; + + return { + ...createBasePublicHttpTestBindings(database, baseOptions), + runtimeSubjectHandleFactory: () => { + if (artifactSandbox?.resolveError !== undefined) { + throw artifactSandbox.resolveError; + } + return createRuntimeOutputSandbox(artifactSandbox); + }, + }; +} + +async function readRuntimeArtifactManifest( + database: D1Database, + sourceEventId: string, +): Promise { + const row = await database + .prepare("SELECT artifact_manifest_json FROM session_event WHERE source_event_id = ?") + .bind(sourceEventId) + .first<{ artifact_manifest_json: string | null }>(); + if (row?.artifact_manifest_json === null || row?.artifact_manifest_json === undefined) { + throw new Error(`Runtime artifact manifest is missing for ${sourceEventId}.`); + } + return parseRuntimeArtifactManifest(row.artifact_manifest_json); +} + function runtimeEvent(input: { + correlationId?: string; kind: RuntimeEventKind; + occurredAt?: number; payload: unknown; + runId?: SessionRunId | null; sourceEventId: string; + visibility?: "owner_debug"; }): DriverEventEnvelope { - const occurredAt = Date.now(); + const occurredAt = input.occurredAt ?? Date.now(); const event = createRuntimeEvent({ + ...(input.correlationId === undefined ? {} : { correlationId: input.correlationId }), driverInstanceId: DRIVER_ID, id: createPlatformId(), kind: input.kind, occurredAt: new Date(occurredAt).toISOString(), payload: input.payload, - runId: RUN_ID, + ...(input.runId === null ? {} : { runId: input.runId ?? RUN_ID }), + runtimeId: "openai-runtime", sessionId: SESSION_ID, sourceEventId: input.sourceEventId, + traceId: "trace-canary", + ...(input.visibility === undefined ? {} : { visibility: input.visibility }), }); return { event, eventId: input.sourceEventId, - occurredAt, + occurredAt: new Date(occurredAt).toISOString(), }; } @@ -154,14 +235,9 @@ function messageEvents(input: { }): DriverEventEnvelope[] { return [ runtimeEvent({ - kind: "message.started", - payload: { messageId: input.messageId, role: "agent" }, - sourceEventId: `${input.sourcePrefix}:started`, - }), - runtimeEvent({ - kind: "message.delta", - payload: { contentDelta: input.text, messageId: input.messageId, role: "agent" }, - sourceEventId: `${input.sourcePrefix}:delta`, + kind: "message.added", + payload: { content: input.text, messageId: input.messageId, role: "agent" }, + sourceEventId: `${input.sourcePrefix}:snapshot`, }), runtimeEvent({ kind: "message.completed", @@ -181,37 +257,60 @@ function splitIntoBatches(values: readonly T[], size: number): T[][] { return batches; } +function measurePreparedQueries(database: D1Database): { + database: D1Database; + readCount: () => number; +} { + let count = 0; + const measured = new Proxy(database, { + get(target, property) { + if (property === "prepare") { + return (query: string) => { + count += 1; + return target.prepare(query); + }; + } + const value = Reflect.get(target, property, target); + return typeof value === "function" ? value.bind(target) : value; + }, + }); + return { database: measured, readCount: () => count }; +} + async function insertRuntimeFixture(database: SqliteD1Database): Promise { await insertOwnerSession(database); database.execute(` INSERT INTO sandbox ( - id, kind, subject_kind, subject_id, status, bind_mount_ready, + agent_id, app_id, id, incarnation, kind, network_constraints_hash, + owner_account_id, subject_kind, subject_id, status, bind_mount_ready, global_mounts_json, created_at, updated_at ) VALUES ( - '${PUBLIC_API_TEST_IDS.sandbox}', 'pet', 'agent', '${PUBLIC_API_TEST_IDS.agent}', + '${PUBLIC_API_TEST_IDS.agent}', '${PUBLIC_API_TEST_IDS.app}', + '${PUBLIC_API_TEST_IDS.sandbox}', 1, 'pet', '${"0".repeat(64)}', + '${PUBLIC_API_TEST_IDS.ownerAccount}', 'agent', '${PUBLIC_API_TEST_IDS.agent}', 'active', 1, '[]', 1, 1 ); INSERT INTO sandbox_session ( cloudflare_session_id, created_at, cwd, origin_json, sandbox_id, - session_id, status, updated_at + sandbox_incarnation, session_id, status, updated_at ) VALUES ( - 'canary-cloudflare-session', 1, '/workspace', + '${PUBLIC_API_TEST_IDS.operation}', 1, '/workspace', '{"callerUserId":"${PUBLIC_API_TEST_IDS.ownerAccount}","entrypoint":"api","executionOwnerUserId":"${PUBLIC_API_TEST_IDS.ownerAccount}","type":"agent"}', - '${PUBLIC_API_TEST_IDS.sandbox}', '${SESSION_ID}', 'active', 1 + '${PUBLIC_API_TEST_IDS.sandbox}', 1, '${SESSION_ID}', 'active', 1 ); INSERT INTO driver_instance ( id, boot_token_expires_at, boot_token_hash, connection_id, created_at, expires_at, heartbeat_count, protocol, protocol_version, runtime, - sandbox_id, sandbox_session_id, status, updated_at + sandbox_id, sandbox_incarnation, sandbox_session_id, status, updated_at ) VALUES ( '${DRIVER_ID}', 1, X'01', 'canary-connection', 1, 1, 0, 'orpc-ws', 1, 'openai-runtime', '${PUBLIC_API_TEST_IDS.sandbox}', - '${SESSION_ID}', 'ready', 1 + 1, '${SESSION_ID}', 'ready', 1 ); INSERT INTO session_run ( @@ -275,7 +374,7 @@ function failTerminalSessionEventInsert(database: D1Database): D1Database { return (query: string) => wrapStatement( target.prepare(query), - /insert\s+into\s+["`]session_event["`]/iu.test(query), + /insert\s+into\s+(?:["`]session_event["`]|session_event)/iu.test(query), false, ); } @@ -286,6 +385,198 @@ function failTerminalSessionEventInsert(database: D1Database): D1Database { }); } +function rejectSessionEventContentReads(database: D1Database): D1Database { + return new Proxy(database, { + get(target, property) { + if (property === "prepare") { + return (query: string) => { + if ( + /^\s*select\b/iu.test(query) && + /\bcontent_text\b/iu.test(query) && + /\bfrom\s+["`]?session_event["`]?/iu.test(query) + ) { + throw new Error("Terminal ingestion attempted to materialize session event content."); + } + return target.prepare(query); + }; + } + + const value = Reflect.get(target, property, target); + return typeof value === "function" ? value.bind(target) : value; + }, + }); +} + +function failPreparedProjection(database: D1Database, pattern: RegExp): D1Database { + function wrap(statement: D1PreparedStatement, shouldFail: boolean): D1PreparedStatement { + return new Proxy(statement, { + get(target, property) { + if (property === "bind") { + return (...values: unknown[]) => wrap(target.bind(...values), shouldFail); + } + if (property === "run" && shouldFail) { + return async () => { + throw new Error("injected durable side-effect projection failure"); + }; + } + + const value = Reflect.get(target, property); + return typeof value === "function" ? value.bind(target) : value; + }, + }); + } + + return new Proxy(database, { + get(target, property) { + if (property === "prepare") { + return (query: string) => wrap(target.prepare(query), pattern.test(query)); + } + + const value = Reflect.get(target, property); + return typeof value === "function" ? value.bind(target) : value; + }, + }); +} + +type RuntimeArtifactAckLossPhase = "claim" | "commit" | "create" | "put" | "seal"; + +function failAfterRuntimeArtifactDatabaseWrite( + database: D1Database, + phase: Exclude, +): { readonly database: D1Database; readonly wasInjected: () => boolean } { + const pattern = { + claim: "SET owned_object_keys_json = json_insert", + create: "INSERT INTO runtime_artifact_attempt", + seal: "SET manifest_json = ?, manifest_sha256 = ?, status = 'staged'", + }[phase]; + let injected = false; + + function wrap(statement: D1PreparedStatement): D1PreparedStatement { + return new Proxy(statement, { + get(target, property) { + if (property === "bind") { + return (...values: unknown[]) => wrap(target.bind(...values)); + } + if (property === "first") { + return async (...arguments_: unknown[]) => { + const result = await Reflect.apply(target.first, target, arguments_); + if (!injected) { + injected = true; + throw new Error(`injected artifact ${phase} acknowledgement loss`); + } + return result; + }; + } + + const value = Reflect.get(target, property, target); + return typeof value === "function" ? value.bind(target) : value; + }, + }); + } + + return { + database: new Proxy(database, { + get(target, property) { + if (property === "prepare") { + return (query: string) => { + const statement = target.prepare(query); + return query.includes(pattern) ? wrap(statement) : statement; + }; + } + + const value = Reflect.get(target, property, target); + return typeof value === "function" ? value.bind(target) : value; + }, + }), + wasInjected: () => injected, + }; +} + +function failAfterRuntimeArtifactCommit( + database: D1Database, + sourceEventId: string, +): { readonly database: D1Database; readonly wasInjected: () => boolean } { + let injected = false; + return { + database: new Proxy(database, { + get(target, property) { + if (property === "batch") { + return async (statements: D1PreparedStatement[]) => { + const result = await target.batch(statements); + const receipt = await target + .prepare("SELECT 1 AS found FROM session_event WHERE source_event_id = ?") + .bind(sourceEventId) + .first(); + if (!injected && receipt !== null) { + injected = true; + throw new Error("injected artifact commit acknowledgement loss"); + } + return result; + }; + } + + const value = Reflect.get(target, property, target); + return typeof value === "function" ? value.bind(target) : value; + }, + }), + wasInjected: () => injected, + }; +} + +function mutateBeforeFirstDatabaseBatch( + database: D1Database, + mutation: (database: D1Database) => Promise, +): { readonly database: D1Database; readonly wasInjected: () => boolean } { + let injected = false; + + return { + database: new Proxy(database, { + get(target, property) { + if (property === "batch") { + return async (statements: D1PreparedStatement[]) => { + if (!injected) { + injected = true; + await mutation(target); + } + return target.batch(statements); + }; + } + + const value = Reflect.get(target, property, target); + return typeof value === "function" ? value.bind(target) : value; + }, + }), + wasInjected: () => injected, + }; +} + +function failAfterRuntimeArtifactPut(bucket: PublicApiMemoryFileBucket): { + readonly bucket: R2Bucket; + readonly wasInjected: () => boolean; +} { + let injected = false; + return { + bucket: new Proxy(bucket, { + get(target, property) { + if (property === "put") { + return async (...arguments_: Parameters) => { + const result = await target.put(...arguments_); + if (!injected) { + injected = true; + throw new Error("injected artifact put acknowledgement loss"); + } + return result; + }; + } + + const value = Reflect.get(target, property, target); + return typeof value === "function" ? value.bind(target) : value; + }, + }), + wasInjected: () => injected, + }; +} + const activeContext = { assertActiveConnection: () => undefined, connectionId: "canary-connection", @@ -302,490 +593,4646 @@ async function pushFreshController( return result.accepted; } -describe("runtime final output ingestion", () => { - test.each([ - ["omits", false], - ["provides", true], - ] as const)( - "persists one final assistant snapshot when the driver %s it", - async (_driverBehavior, driverProvidesSnapshot) => { - const database = await createPublicHttpContractDatabase(); - await insertRuntimeFixture(database); - const capturedEvents: unknown[] = []; - setServerProductAnalyticsTransportForTests(async (_input, init) => { - capturedEvents.push(JSON.parse(init.body as string) as unknown); - return new Response(null, { status: 200 }); - }); - const bindings = { - ...createPublicHttpTestBindings(database), - POSTHOG_PROJECT_KEY: "phc_test", - } as ApiBindings; - const finalText = "The final answer."; - const fragmentTexts = ["The ", "final ", "answer."]; - const finalMessageId = createPlatformId(); - const events = [ - ...fragmentTexts.flatMap((text, index) => - messageEvents({ - messageId: createPlatformId(), - sourcePrefix: `fractured:${index + 1}`, - text, - }), - ), - ...(driverProvidesSnapshot - ? [ - runtimeEvent({ - kind: "message.added", - payload: { content: finalText, messageId: finalMessageId, role: "agent" }, - sourceEventId: "fractured:final-snapshot", - }), - ] - : []), - runtimeEvent({ - kind: "run.completed", - payload: { - finalMessageId, - finalMessageText: finalText, - stopReason: "end_turn", - }, - sourceEventId: "fractured:run-completed", - }), - ]; - - await pushFreshController(bindings, events); +async function insertRepairableCompletedAuthority( + database: D1Database, + sourcePrefix: string, +): Promise { + const bindings = createPublicHttpTestBindings(database) as ApiBindings; + const finalMessageId = createPlatformId(); + await pushFreshController(bindings, [ + ...messageEvents({ messageId: finalMessageId, sourcePrefix, text: FINAL_TEXT }), + runtimeEvent({ + kind: "run.completed", + payload: { finalMessageId, stopReason: "end_turn" }, + sourceEventId: `${sourcePrefix}:terminal`, + }), + ]); + await database.prepare("DELETE FROM session_message WHERE id = ?").bind(finalMessageId).run(); + await database + .prepare("UPDATE session SET message_seq_cursor = 0 WHERE id = ?") + .bind(SESSION_ID) + .run(); + return finalMessageId; +} - const rows = await database - .prepare( - `SELECT content_text, ended_at, event_type, id, occurred_at, process_status, - process_type, run_id, seq, tokens - FROM session_event - WHERE session_id = ? AND run_id = ? - ORDER BY seq`, - ) - .bind(SESSION_ID, RUN_ID) - .all(); - const assistantMessages = createSessionProcessEventsFromSessionEventRows(rows.results).filter( - (event) => event.type === "agent.message.delta", - ); +const VALID_RUNTIME_ARTIFACT_MANIFEST = { + captureStatus: "complete", + files: [], + mode: "delta", + semanticHash: "0".repeat(64), + sourceEventId: "artifact-manifest:valid", + version: 1, +}; - expect( - rows.results - .filter((row) => row.event_type === "message.added") - .map((row) => row.content_text), - ).toEqual([finalText]); - expect(assistantMessages.map((event) => event.content)).toEqual([finalText]); - expect(capturedEvents).toEqual([ - expect.objectContaining({ - event: "task_succeeded", - properties: expect.objectContaining({ - run_duration_ms: expect.any(Number), - sandbox_id: PUBLIC_API_TEST_IDS.sandbox, - sandbox_kind: "pet", - sandbox_subject_kind: "agent", - session_type: "ui", - }), - }), - ]); - }, - ); +describe("runtime artifact manifest validation", () => { + test.each([ + ["malformed JSON", "{"], + ["non-object root", null], + ["unsupported version", { ...VALID_RUNTIME_ARTIFACT_MANIFEST, version: 2 }], + ["unsupported capture status", { ...VALID_RUNTIME_ARTIFACT_MANIFEST, captureStatus: "lost" }], + ["unsupported mode", { ...VALID_RUNTIME_ARTIFACT_MANIFEST, mode: "append" }], + ["non-array files", { ...VALID_RUNTIME_ARTIFACT_MANIFEST, files: {} }], + ["empty source event ID", { ...VALID_RUNTIME_ARTIFACT_MANIFEST, sourceEventId: "" }], + ["invalid semantic hash", { ...VALID_RUNTIME_ARTIFACT_MANIFEST, semanticHash: "invalid" }], + [ + "invalid file operation", + { + ...VALID_RUNTIME_ARTIFACT_MANIFEST, + files: [{ operation: "rename", sourcePath: "outputs/report.txt" }], + }, + ], + [ + "non-empty omitted capture", + { + ...VALID_RUNTIME_ARTIFACT_MANIFEST, + captureStatus: "omitted_size_limit", + files: [{ operation: "delete", sourcePath: "outputs/report.txt" }], + }, + ], + [ + "duplicate file identity", + { + ...VALID_RUNTIME_ARTIFACT_MANIFEST, + files: [ + { operation: "delete", sourcePath: "outputs/report.txt" }, + { operation: "delete", sourcePath: "outputs/report.txt" }, + ], + }, + ], + ] as const)("rejects the %s boundary", (_name, value) => { + const manifestJson = typeof value === "string" ? value : JSON.stringify(value); + expect(() => parseRuntimeArtifactManifest(manifestJson)).toThrow(); + }); +}); - test("preserves a long final snapshot across hibernation, terminal failure, and replay", async () => { +describe("runtime final output ingestion", () => { + test("admits only canonical content-addressed MCP failures", async () => { const database = await createPublicHttpContractDatabase(); await insertRuntimeFixture(database); const bindings = createPublicHttpTestBindings(database) as ApiBindings; - const progressMessageIds = PROGRESS_TEXTS.map(() => createPlatformId()); - const finalMessageId = createPlatformId(); - const progressEvents = [ - runtimeEvent({ - kind: "run.started", - payload: { startedAt: new Date(1).toISOString() }, - sourceEventId: "canary:run-started", - }), - ...PROGRESS_TEXTS.flatMap((text, index) => - messageEvents({ - messageId: progressMessageIds[index], - sourcePrefix: `canary:progress:${index + 1}`, - text, + const commandId = "01J0000000000000000000000X" as DriverCommandId; + const command = { + argumentsJson: '{"issue":"A-1"}', + commandId, + kind: "mcp.execute", + requestId: "request-A-1", + runId: RUN_ID, + serverId: "01J0000000000000000000000Y", + toolCallId: "tool-A-1", + toolName: "createIssue", + } as const; + await createRuntimeCommandRecord(database, { + command, + driverGeneration: 0, + driverInstanceId: DRIVER_ID, + status: "accepted", + }); + const failed = createMcpExecuteFailedEventIdentity({ + commandId, + rawInput: command.argumentsJson, + rawOutput: "provider rejected the request", + title: command.toolName, + toolCallId: command.toolCallId, + }); + const event = runtimeEvent({ + correlationId: commandId, + kind: "tool.call.updated", + payload: failed.payload, + sourceEventId: failed.sourceEventId, + }); + + await expect(pushFreshController(bindings, [event])).resolves.toMatchObject([ + { eventId: failed.sourceEventId }, + ]); + await expect( + pushFreshController(bindings, [ + { + ...event, + event: { + ...event.event, + payload: { ...failed.payload, rawOutput: "tampered failure" }, + }, + }, + ]), + ).rejects.toThrow("canonical content identity"); + await expect( + pushFreshController(bindings, [ + runtimeEvent({ + correlationId: commandId, + kind: "tool.call.updated", + payload: failed.payload, + sourceEventId: `mcp.execute.failed:${"0".repeat(64)}`, }), - ), - ]; + ]), + ).rejects.toThrow("canonical content identity"); + }); - expect(await pushFreshController(bindings, progressEvents)).toHaveLength(progressEvents.length); + test("commits canonical durable state before enqueue and replays without projection writes", async () => { + const database = await createPublicHttpContractDatabase(); + await insertRuntimeFixture(database); + await database.prepare("UPDATE session SET title = NULL WHERE id = ?").bind(SESSION_ID).run(); + const syncedSessionIds: string[] = []; + const bindings = createPublicHttpTestBindings(database, { + sessionNamespace: createSessionSyncNamespace(syncedSessionIds), + }) as ApiBindings; + let durableCommitCompleted = false; + const disconnectingDatabase = new Proxy(database, { + get(target, property) { + if (property === "batch") { + return async (statements: D1PreparedStatement[]) => { + const result = await target.batch(statements); + durableCommitCompleted = true; + return result; + }; + } - const toolEvents = [ + const value = Reflect.get(target, property); + return typeof value === "function" ? value.bind(target) : value; + }, + }); + let failureInjected = false; + const disconnectAfterCommitContext = { + ...activeContext, + assertActiveConnection() { + if (durableCommitCompleted && !failureInjected) { + failureInjected = true; + throw new Error("injected disconnect after durable event commit"); + } + }, + } as never; + const taskSourceEventId = "task-replay:tasks"; + const snapshotSourceEventId = "task-replay:message-snapshot"; + const deltaSourceEventId = "task-replay:message-delta"; + const messageId = createPlatformId(); + const thoughtId = createPlatformId(); + const userMessageId = createPlatformId(); + const events = [ runtimeEvent({ - kind: "item.started", - payload: { - itemId: "tool-canary", - itemType: "tool_call", - parentMessageId: finalMessageId, - title: "Create artifact", + kind: "agent.tasks.replaced", + payload: { tasks: [{ taskId: "canonical-task", title: "Inspect" }] }, + sourceEventId: taskSourceEventId, + }), + runtimeEvent({ + kind: "message.added", + payload: { content: "canonical ", messageId, role: "agent" }, + sourceEventId: snapshotSourceEventId, + }), + runtimeEvent({ + kind: "message.delta", + payload: { + contentDelta: "state", + messageId, + role: "agent", }, - sourceEventId: "canary:tool:started", + sourceEventId: deltaSourceEventId, + }), + runtimeEvent({ + kind: "thought.started", + payload: { thoughtId }, + sourceEventId: "task-replay:thought-started", + }), + runtimeEvent({ + kind: "thought.delta", + payload: { contentDelta: "canonical reasoning", thoughtId }, + sourceEventId: "task-replay:thought-delta", + }), + runtimeEvent({ + kind: "thought.completed", + payload: { thoughtId }, + sourceEventId: "task-replay:thought-completed", + }), + runtimeEvent({ + kind: "message.added", + payload: { content: "canonical question", messageId: userMessageId, role: "user" }, + sourceEventId: "task-replay:user-message", }), runtimeEvent({ kind: "tool.call.updated", payload: { - rawOutput: "artifact created", + parentMessageId: messageId, + rawInputDelta: '{"command":', + status: "running", + title: "Shell", + toolCallId: "stable-tool", + }, + sourceEventId: "task-replay:tool-started", + }), + runtimeEvent({ + kind: "tool.call.updated", + payload: { + messageId, + rawInput: '{"command":"pwd"}', + status: "running", + title: "Shell", + toolCallId: "stable-tool", + }, + sourceEventId: "task-replay:tool-snapshot", + }), + runtimeEvent({ + kind: "tool.call.updated", + payload: { + rawOutput: "canonical output", status: "completed", - toolCallId: "tool-canary", + title: "Shell", + toolCallId: "stable-tool", }, - sourceEventId: "canary:tool:updated", + sourceEventId: "task-replay:tool-completed", }), runtimeEvent({ - kind: "item.completed", - payload: { itemId: "tool-canary", itemType: "tool_call", status: "completed" }, - sourceEventId: "canary:tool:completed", + kind: "tool.call.updated", + payload: { + status: "running", + title: "Orphan tool", + toolCallId: "orphan-tool", + }, + sourceEventId: "task-replay:orphan-tool-started", + }), + runtimeEvent({ + kind: "tool.call.updated", + payload: { + rawOutput: "orphan output", + status: "completed", + title: "Orphan tool", + toolCallId: "orphan-tool", + }, + sourceEventId: "task-replay:orphan-tool-completed", + }), + runtimeEvent({ + kind: "plan.updated", + payload: { + entries: [{ content: "Verify durable state", priority: "high", status: "completed" }], + }, + sourceEventId: "task-replay:plan", + }), + runtimeEvent({ + kind: "session.commands.updated", + payload: { + commands: [{ description: "Inspect the workspace", name: "inspect" }], + }, + sourceEventId: "task-replay:commands", + }), + runtimeEvent({ + kind: "session.config.updated", + payload: { + options: [ + { + currentValue: "brief", + id: "tone", + name: "Tone", + type: "select", + values: [{ name: "Brief", value: "brief" }], + }, + ], + }, + sourceEventId: "task-replay:config", + }), + runtimeEvent({ + kind: "session.mode.updated", + payload: { + availableModes: [{ id: "plan", name: "Plan" }], + currentMode: "plan", + }, + sourceEventId: "task-replay:mode", + }), + runtimeEvent({ + kind: "session.info.updated", + payload: { title: "Atomic durable title" }, + sourceEventId: "task-replay:title", + }), + runtimeEvent({ + kind: "usage.updated", + payload: { + callId: "atomic-call", + inputTokens: 21, + outputTokens: 4, + source: "prompt_response", + usageContract: "openai_total_with_cached_breakdown", + }, + sourceEventId: "task-replay:usage", + }), + runtimeEvent({ + kind: "runtime.resume.updated", + payload: { resumePointer: "atomic-thread" }, + sourceEventId: "task-replay:native-resume", }), ]; - expect(await pushFreshController(bindings, toolEvents)).toHaveLength(toolEvents.length); + const firstDelivery: SessionDeliveryEvent[] = []; + + await expect( + createController( + { ...bindings, DB: disconnectingDatabase } as ApiBindings, + (_sessionId, deliveryEvents) => firstDelivery.push(...deliveryEvents), + ).handlePushEvents({ driverInstanceId: DRIVER_ID, events }, disconnectAfterCommitContext), + ).rejects.toThrow("injected disconnect after durable event commit"); + + expect(failureInjected).toBe(true); + expect(firstDelivery).toEqual([]); + const durableRows = await database + .prepare( + `SELECT event_type, source_event_id FROM session_event WHERE source_event_id IN (${events + .map(() => "?") + .join(", ")}) ORDER BY seq`, + ) + .bind(...events.map((event) => event.eventId)) + .all<{ event_type: string; source_event_id: string }>(); - const finalTextChunks = FINAL_TEXT_LINES.map((line, index) => - index === FINAL_TEXT_LINES.length - 1 ? line : `${line}\n`, + expect(durableRows.results).toEqual( + events.map((event) => ({ + event_type: event.event.kind, + source_event_id: event.eventId, + })), ); - const finalStreamEvents = [ - runtimeEvent({ - kind: "message.started", - payload: { messageId: finalMessageId, role: "agent" }, - sourceEventId: "canary:final:started", - }), - ...finalTextChunks.map((contentDelta, index) => - runtimeEvent({ - kind: "message.delta", - payload: { contentDelta, messageId: finalMessageId, role: "agent" }, - sourceEventId: `canary:final:delta:${index + 1}`, - }), + const titleSeq = events.findIndex((event) => event.event.kind === "session.info.updated") + 1; + const usageSeq = events.findIndex((event) => event.event.kind === "usage.updated") + 1; + const nativeResumeSeq = + events.findIndex((event) => event.event.kind === "runtime.resume.updated") + 1; + const usageReceipt = await database + .prepare("SELECT created_at FROM session_event WHERE source_event_id = ?") + .bind("task-replay:usage") + .first<{ created_at: number }>(); + + expect(usageReceipt).not.toBeNull(); + + expect( + await database + .prepare("SELECT auto_title_event_seq, title FROM session WHERE id = ?") + .bind(SESSION_ID) + .first(), + ).toEqual({ auto_title_event_seq: titleSeq, title: "Atomic durable title" }); + expect( + await database + .prepare( + `SELECT call_key, created_at, source_event_seq, status + FROM session_model_call + WHERE session_id = ? AND session_run_id = ?`, + ) + .bind(SESSION_ID, RUN_ID) + .first(), + ).toEqual({ + call_key: "model_call:atomic-call", + created_at: usageReceipt?.created_at, + source_event_seq: usageSeq, + status: "started", + }); + expect( + await database + .prepare( + `SELECT created_at, input_tokens, output_tokens, source_event_seq + FROM usage_event + WHERE session_id = ? AND session_run_id = ?`, + ) + .bind(SESSION_ID, RUN_ID) + .first(), + ).toEqual({ + created_at: usageReceipt?.created_at, + input_tokens: 21, + output_tokens: 4, + source_event_seq: usageSeq, + }); + expect( + await database + .prepare( + `SELECT observed_event_seq, observed_session_run_id, value + FROM native_resume_ref + WHERE session_id = ?`, + ) + .bind(SESSION_ID) + .first(), + ).toEqual({ + observed_event_seq: nativeResumeSeq, + observed_session_run_id: RUN_ID, + value: "atomic-thread", + }); + + const viewerUsageSeq = events.length + 1; + await insertRuntimeEvent(database, { + kind: "usage.updated", + occurredAt: Date.now(), + payload: { source: "session_update", totalTokens: 42 }, + runId: RUN_ID, + seq: viewerUsageSeq, + sessionId: SESSION_ID, + }); + await database + .prepare("UPDATE session SET runtime_event_seq_cursor = ? WHERE id = ?") + .bind(viewerUsageSeq, SESSION_ID) + .run(); + + const replayDelivery: SessionDeliveryEvent[] = []; + const replay = await createController( + { + ...bindings, + DB: failPreparedProjection( + database, + /SET auto_title_event_seq =|INSERT INTO ["`]?session_model_call|INSERT INTO native_resume_ref/iu, + ), + } as ApiBindings, + (_sessionId, deliveryEvents) => replayDelivery.push(...deliveryEvents), + ).handlePushEvents({ driverInstanceId: DRIVER_ID, events }, activeContext); + + expect(replay.accepted.map((receipt) => receipt.eventId)).toEqual( + events.map((event) => event.eventId), + ); + expect(replayDelivery).toEqual([]); + expect(syncedSessionIds).toEqual([SESSION_ID]); + expect( + await database + .prepare( + `SELECT + (SELECT COUNT(*) FROM session_model_call WHERE session_id = ?) AS model_call_count, + (SELECT created_at FROM session_model_call WHERE session_id = ?) AS model_call_created_at, + (SELECT COUNT(*) FROM usage_event WHERE session_id = ?) AS usage_event_count, + (SELECT created_at FROM usage_event WHERE session_id = ?) AS usage_event_created_at, + (SELECT observed_event_seq FROM native_resume_ref WHERE session_id = ?) AS native_seq, + (SELECT auto_title_event_seq FROM session WHERE id = ?) AS title_seq`, + ) + .bind(SESSION_ID, SESSION_ID, SESSION_ID, SESSION_ID, SESSION_ID, SESSION_ID) + .first(), + ).toEqual({ + model_call_count: 1, + model_call_created_at: usageReceipt?.created_at, + native_seq: nativeResumeSeq, + title_seq: titleSeq, + usage_event_count: 1, + usage_event_created_at: usageReceipt?.created_at, + }); + const hydrated = await loadSessionViewerState(database, { + sessionId: SESSION_ID, + viewerId: PUBLIC_API_TEST_IDS.ownerAccount, + }); + expect(hydrated.taskSnapshot).toEqual({ + driverInstanceId: DRIVER_ID, + runId: RUN_ID, + tasks: [{ taskId: "canonical-task", title: "Inspect" }], + }); + expect(hydrated.commands).toEqual([{ description: "Inspect the workspace", name: "inspect" }]); + expect(hydrated.configOptions).toEqual([ + { + currentValue: "brief", + id: "tone", + name: "Tone", + type: "select", + values: [{ name: "Brief", value: "brief" }], + }, + ]); + expect(hydrated.currentModeId).toBe("plan"); + expect(hydrated.visibleModes).toEqual([{ id: "plan", name: "Plan" }]); + expect(hydrated.usage).toMatchObject({ source: "session_update", totalTokens: 42 }); + expect(hydrated.plan).toEqual([ + { content: "Verify durable state", priority: "high", status: "completed" }, + ]); + expect(hydrated.messages.find((message) => message.id === userMessageId)).toMatchObject({ + content: "canonical question", + role: "user", + }); + expect(hydrated.messages.find((message) => message.id === thoughtId)?.segments).toEqual([ + { kind: "reasoning", text: "canonical reasoning" }, + ]); + expect(hydrated.messages.find((message) => message.id === messageId)).toMatchObject({ + content: "canonical state", + role: "assistant", + segments: expect.arrayContaining([ + { + argsText: '{"command":"pwd"}', + kind: "tool_use", + path: null, + runId: RUN_ID, + tool: "Shell", + toolCallId: "stable-tool", + }, + { + kind: "tool_result", + output: "canonical output", + runId: RUN_ID, + tool: "Shell", + toolCallId: "stable-tool", + }, + ]), + }); + const orphanResultMessageId = createRuntimeToolResultMessageId({ + runId: RUN_ID, + toolCallId: "orphan-tool", + }); + expect( + hydrated.messages.find((message) => message.id === orphanResultMessageId)?.segments, + ).toEqual([ + { + kind: "tool_result", + output: "orphan output", + runId: RUN_ID, + tool: "Orphan tool", + toolCallId: "orphan-tool", + }, + ]); + + await expect( + createController(bindings).handlePushEvents( + { + driverInstanceId: DRIVER_ID, + events: [ + runtimeEvent({ + kind: "agent.tasks.replaced", + payload: { tasks: [{ taskId: "non-canonical-retry-payload" }] }, + sourceEventId: taskSourceEventId, + }), + ], + }, + activeContext, ), + ).rejects.toThrow("conflicts with its durable receipt"); + + await database + .prepare("UPDATE session_run SET completed_at = ?, status = 'completed' WHERE id = ?") + .bind(Date.now(), RUN_ID) + .run(); + await database + .prepare("UPDATE session SET status = 'IDLE' WHERE id = ?") + .bind(SESSION_ID) + .run(); + const terminalHydrated = await loadSessionViewerState(database, { + sessionId: SESSION_ID, + viewerId: PUBLIC_API_TEST_IDS.ownerAccount, + }); + expect({ + commands: terminalHydrated.commands, + configOptions: terminalHydrated.configOptions, + currentModeId: terminalHydrated.currentModeId, + plan: terminalHydrated.plan, + usage: terminalHydrated.usage, + visibleModes: terminalHydrated.visibleModes, + }).toEqual({ + commands: hydrated.commands, + configOptions: hydrated.configOptions, + currentModeId: hydrated.currentModeId, + plan: hydrated.plan, + usage: hydrated.usage, + visibleModes: hydrated.visibleModes, + }); + }); + + test("rolls back receipts and every side effect when a durable projection fails", async () => { + const projectionPatterns = [ + /SET auto_title_event_seq =/iu, + /INSERT INTO ["`]?session_model_call/iu, + /INSERT INTO native_resume_ref/iu, ]; - const finalBatches = splitIntoBatches(finalStreamEvents, 50); - for (const batch of finalBatches.slice(0, -1)) { - expect(await pushFreshController(bindings, batch)).toHaveLength(batch.length); + for (const pattern of projectionPatterns) { + const database = await createPublicHttpContractDatabase(); + await insertRuntimeFixture(database); + await database.prepare("UPDATE session SET title = NULL WHERE id = ?").bind(SESSION_ID).run(); + const events = [ + runtimeEvent({ + kind: "session.info.updated", + payload: { title: "Must roll back" }, + sourceEventId: "atomic-rollback:title", + }), + runtimeEvent({ + kind: "usage.updated", + payload: { + inputTokens: 8, + outputTokens: 2, + source: "prompt_response", + usageContract: "openai_total_with_cached_breakdown", + }, + sourceEventId: "atomic-rollback:usage", + }), + runtimeEvent({ + kind: "runtime.resume.updated", + payload: { resumePointer: "must-roll-back" }, + sourceEventId: "atomic-rollback:native-resume", + }), + ]; + + await expect( + pushFreshController( + { + ...createPublicHttpTestBindings(database), + DB: failPreparedProjection(database, pattern), + } as ApiBindings, + events, + ), + ).rejects.toThrow("injected durable side-effect projection failure"); + + expect( + await database + .prepare( + `SELECT runtime_event_seq_cursor, title, + (SELECT COUNT(*) FROM session_event) AS receipt_count, + (SELECT COUNT(*) FROM session_model_call) AS model_call_count, + (SELECT COUNT(*) FROM usage_event) AS usage_event_count, + (SELECT COUNT(*) FROM native_resume_ref) AS native_ref_count + FROM session WHERE id = ?`, + ) + .bind(SESSION_ID) + .first(), + ).toEqual({ + model_call_count: 0, + native_ref_count: 0, + receipt_count: 0, + runtime_event_seq_cursor: 0, + title: null, + usage_event_count: 0, + }); } + }); - const terminalBatch = [ - ...(finalBatches.at(-1) ?? []), + test("finalizes durable usage only with the terminal Run commit", async () => { + const database = await createPublicHttpContractDatabase(); + await insertRuntimeFixture(database); + const events = [ runtimeEvent({ - kind: "message.completed", - payload: { messageId: finalMessageId, role: "agent" }, - sourceEventId: "canary:final:completed", + kind: "usage.updated", + payload: { + inputTokens: 13, + outputTokens: 3, + source: "prompt_response", + usageContract: "openai_total_with_cached_breakdown", + }, + sourceEventId: "terminal-usage:usage", }), runtimeEvent({ kind: "run.completed", - payload: { - finalMessageId, - finalMessageText: FINAL_TEXT, - stopReason: "end_turn", - }, - sourceEventId: TERMINAL_SOURCE_EVENT_ID, + payload: { stopReason: "end_turn" }, + sourceEventId: "terminal-usage:completed", }), ]; - const failingBindings = { - ...bindings, - DB: failTerminalSessionEventInsert(database), - } as ApiBindings; - await expect(pushFreshController(failingBindings, terminalBatch)).rejects.toBeInstanceOf(Error); + await expect( + pushFreshController( + { + ...createPublicHttpTestBindings(database), + DB: failPreparedProjection(database, /completed-run:model-calls/iu), + } as ApiBindings, + events, + ), + ).rejects.toThrow("injected durable side-effect projection failure"); + expect( + await database + .prepare( + `SELECT r.status AS run_status, s.status AS session_status, + model_call.completed_at, model_call.status AS model_call_status, + (SELECT COUNT(*) FROM session_event + WHERE session_id = ? + AND event_type IN ('run.cancelled', 'run.completed', 'run.failed')) + AS terminal_count + FROM session_run AS r + JOIN session AS s ON s.id = r.session_id + JOIN session_model_call AS model_call ON model_call.session_run_id = r.id + WHERE r.id = ?`, + ) + .bind(SESSION_ID, RUN_ID) + .first(), + ).toEqual({ + completed_at: null, + model_call_status: "started", + run_status: "running", + session_status: "RUNNING", + terminal_count: 0, + }); - const completedRun = await database - .prepare("SELECT status FROM session_run WHERE id = ?") - .bind(RUN_ID) - .first<{ status: string }>(); - const projectedMessagesBeforeReplay = await database - .prepare("SELECT content_text, id FROM session_message WHERE session_run_id = ? ORDER BY seq") - .bind(RUN_ID) - .all<{ content_text: string; id: string }>(); - const finalOutputBeforeReplay = await readPublicThreadRunFinalOutput({ - database, - runId: RUN_ID, - sessionId: SESSION_ID, + await expect( + pushFreshController(createPublicHttpTestBindings(database) as ApiBindings, events), + ).resolves.toHaveLength(2); + await expect( + pushFreshController(createPublicHttpTestBindings(database) as ApiBindings, events), + ).resolves.toHaveLength(2); + expect( + await database + .prepare( + `SELECT r.status AS run_status, model_call.completed_at, + model_call.status AS model_call_status, + (SELECT COUNT(*) FROM session_event + WHERE session_id = ? AND event_type = 'run.completed') AS terminal_count + FROM session_run AS r + JOIN session_model_call AS model_call ON model_call.session_run_id = r.id + WHERE r.id = ?`, + ) + .bind(SESSION_ID, RUN_ID) + .first(), + ).toEqual({ + completed_at: expect.any(Number), + model_call_status: "completed", + run_status: "completed", + terminal_count: 1, }); - const terminalRowsBeforeReplay = await database - .prepare("SELECT source_event_id FROM session_event WHERE source_event_id = ?") - .bind(TERMINAL_SOURCE_EVENT_ID) - .all<{ source_event_id: string }>(); - expect(completedRun?.status).toBe("completed"); - expect(projectedMessagesBeforeReplay.results).toEqual([ - { content_text: FINAL_TEXT, id: finalMessageId }, - ]); - expect(finalOutputBeforeReplay?.text).toBe(FINAL_TEXT); - expect(new TextEncoder().encode(finalOutputBeforeReplay?.text)).toEqual( - new TextEncoder().encode(FINAL_TEXT), + await database + .prepare( + `UPDATE session_model_call + SET completed_at = NULL, status = 'started' + WHERE session_id = ? AND session_run_id = ?`, + ) + .bind(SESSION_ID, RUN_ID) + .run(); + const terminalEnvelope = events[1]; + + if (terminalEnvelope === undefined) { + throw new Error("Missing terminal usage test event."); + } + const canonicalTerminal = canonicalizeDriverEventEnvelope( + { event: terminalEnvelope.event, eventId: terminalEnvelope.eventId }, + { traceId: "trace-canary" }, ); - expect(terminalRowsBeforeReplay.results).toEqual([]); + await expect( + commitTerminalRunProjection(database, { + assistantMessage: null, + error: null, + runId: RUN_ID, + sessionId: SESSION_ID, + source: "driver", + targetStatus: "completed", + terminalEvent: { + event: canonicalTerminal.event, + occurredAt: Date.parse(canonicalTerminal.event.occurredAt), + sourceEventId: canonicalTerminal.event.sourceEventId ?? null, + }, + }), + ).resolves.toMatchObject({ kind: "duplicate" }); + expect( + await database + .prepare( + `SELECT completed_at, status + FROM session_model_call + WHERE session_id = ? AND session_run_id = ?`, + ) + .bind(SESSION_ID, RUN_ID) + .first(), + ).toEqual({ completed_at: expect.any(Number), status: "completed" }); + }); - const replayedFinalMessageId = createPlatformId(); - const crossBootTerminalBatch = [ - ...messageEvents({ - messageId: replayedFinalMessageId, - sourcePrefix: "canary:reconnected-final", - text: FINAL_TEXT, + test("fails closed when active hydration sees a private row in a public stream", async () => { + const database = await createPublicHttpContractDatabase(); + await insertRuntimeFixture(database); + const bindings = createPublicHttpTestBindings(database) as ApiBindings; + const messageId = createPlatformId(); + + await pushFreshController(bindings, [ + runtimeEvent({ + kind: "message.added", + payload: { content: "public", messageId, role: "agent" }, + sourceEventId: "active-mixed-visibility:public", + }), + runtimeEvent({ + kind: "message.delta", + payload: { contentDelta: "private", messageId, role: "agent" }, + sourceEventId: "active-mixed-visibility:private", + visibility: "owner_debug", }), + ]); + + await expect( + loadSessionViewerState(database, { + sessionId: SESSION_ID, + viewerId: PUBLIC_API_TEST_IDS.ownerAccount, + }), + ).rejects.toThrow("mixed-visibility"); + }); + + test("rejects an event batch after another Driver connection takes ownership", async () => { + const database = await createPublicHttpContractDatabase(); + await insertRuntimeFixture(database); + await database + .prepare("UPDATE session_run SET started_at = NULL, status = 'booting' WHERE id = ?") + .bind(RUN_ID) + .run(); + await database.prepare("UPDATE session SET title = NULL WHERE id = ?").bind(SESSION_ID).run(); + let replaced = false; + const replacedDatabase = new Proxy(database, { + get(target, property) { + if (property === "batch") { + return async (statements: D1PreparedStatement[]) => { + if (!replaced) { + replaced = true; + await target + .prepare("UPDATE driver_instance SET connection_id = ? WHERE id = ?") + .bind("replacement-connection", DRIVER_ID) + .run(); + } + return target.batch(statements); + }; + } + const value = Reflect.get(target, property); + return typeof value === "function" ? value.bind(target) : value; + }, + }); + const bindings = createPublicHttpTestBindings(replacedDatabase) as ApiBindings; + const events = [ + runtimeEvent({ + kind: "run.started", + payload: { startedAt: new Date(1).toISOString() }, + sourceEventId: "superseded-driver:run-started", + }), + runtimeEvent({ + kind: "message.added", + payload: { + content: "must not persist", + messageId: createPlatformId(), + role: "agent", + }, + sourceEventId: "superseded-driver:message", + }), + runtimeEvent({ + kind: "session.info.updated", + payload: { title: "Must not persist" }, + sourceEventId: "superseded-driver:title", + }), + ]; + + await expect(pushFreshController(bindings, events)).rejects.toThrow( + "lost its atomic session or active-run fence", + ); + expect(replaced).toBe(true); + expect( + await database + .prepare("SELECT runtime_event_seq_cursor FROM session WHERE id = ?") + .bind(SESSION_ID) + .first(), + ).toEqual({ runtime_event_seq_cursor: 0 }); + expect( + await database + .prepare( + `SELECT COUNT(*) AS count FROM session_event WHERE source_event_id IN (${events + .map(() => "?") + .join(", ")})`, + ) + .bind(...events.map((event) => event.eventId)) + .first(), + ).toEqual({ count: 0 }); + expect( + await database + .prepare("SELECT started_at, status FROM session_run WHERE id = ?") + .bind(RUN_ID) + .first(), + ).toEqual({ started_at: null, status: "booting" }); + expect( + await database.prepare("SELECT title FROM session WHERE id = ?").bind(SESSION_ID).first(), + ).toEqual({ title: null }); + }); + + test("commits run.started and its Run transition in one receipt batch", async () => { + const database = await createPublicHttpContractDatabase(); + await insertRuntimeFixture(database); + await database + .prepare( + "UPDATE session_run SET started_at = NULL, status = 'booting', status_seq = 0 WHERE id = ?", + ) + .bind(RUN_ID) + .run(); + const bindings = createPublicHttpTestBindings(database) as ApiBindings; + const event = runtimeEvent({ + kind: "run.started", + payload: { startedAt: new Date(1).toISOString() }, + sourceEventId: "atomic-run-started", + }); + + await pushFreshController(bindings, [event]); + await pushFreshController(bindings, [event]); + + expect( + await database + .prepare( + `SELECT r.started_at, r.status, r.status_seq, s.runtime_event_seq_cursor + FROM session_run AS r + INNER JOIN session AS s ON s.id = r.session_id + WHERE r.id = ?`, + ) + .bind(RUN_ID) + .first(), + ).toEqual({ + runtime_event_seq_cursor: 1, + started_at: 1, + status: "running", + status_seq: 1, + }); + }); + + test("fences a runless Session event to the exact Driver connection", async () => { + const database = await createPublicHttpContractDatabase(); + await insertRuntimeFixture(database); + await database + .prepare("UPDATE session_run SET completed_at = 2, status = 'completed' WHERE id = ?") + .bind(RUN_ID) + .run(); + await database + .prepare("UPDATE session SET status = 'IDLE', title = NULL WHERE id = ?") + .bind(SESSION_ID) + .run(); + let replaced = false; + const replacedDatabase = new Proxy(database, { + get(target, property) { + if (property === "batch") { + return async (statements: D1PreparedStatement[]) => { + if (!replaced) { + replaced = true; + await target + .prepare("UPDATE driver_instance SET connection_id = ? WHERE id = ?") + .bind("replacement-connection", DRIVER_ID) + .run(); + } + return target.batch(statements); + }; + } + const value = Reflect.get(target, property); + return typeof value === "function" ? value.bind(target) : value; + }, + }); + const bindings = createPublicHttpTestBindings(replacedDatabase) as ApiBindings; + const event = runtimeEvent({ + kind: "session.info.updated", + payload: { title: "Must not persist" }, + runId: null, + sourceEventId: "superseded-driver:runless-title", + }); + + await expect(pushFreshController(bindings, [event])).rejects.toThrow( + "lost its atomic session or active-run fence", + ); + expect( + await database + .prepare("SELECT runtime_event_seq_cursor, title FROM session WHERE id = ?") + .bind(SESSION_ID) + .first(), + ).toEqual({ runtime_event_seq_cursor: 0, title: null }); + }); + + test("does not complete a Run after another Driver connection takes ownership", async () => { + const database = await createPublicHttpContractDatabase(); + await insertRuntimeFixture(database); + const healthyBindings = createPublicHttpTestBindings(database) as ApiBindings; + const finalMessageId = createPlatformId(); + await pushFreshController( + healthyBindings, + messageEvents({ + messageId: finalMessageId, + sourcePrefix: "superseded-terminal:message", + text: "Sealed before reconnect.", + }), + ); + + let replaced = false; + const replacedDatabase = new Proxy(database, { + get(target, property) { + if (property === "batch") { + return async (statements: D1PreparedStatement[]) => { + if (!replaced) { + replaced = true; + await target + .prepare("UPDATE driver_instance SET connection_id = ? WHERE id = ?") + .bind("replacement-connection", DRIVER_ID) + .run(); + } + return target.batch(statements); + }; + } + const value = Reflect.get(target, property); + return typeof value === "function" ? value.bind(target) : value; + }, + }); + const bindings = createPublicHttpTestBindings(replacedDatabase) as ApiBindings; + + await expect( + pushFreshController(bindings, [ + runtimeEvent({ + kind: "run.completed", + payload: { finalMessageId, stopReason: "end_turn" }, + sourceEventId: TERMINAL_SOURCE_EVENT_ID, + }), + ]), + ).rejects.toThrow(); + expect(replaced).toBe(true); + expect( + await database.prepare("SELECT status FROM session_run WHERE id = ?").bind(RUN_ID).first(), + ).toEqual({ status: "running" }); + expect( + await database + .prepare("SELECT COUNT(*) AS count FROM session_event WHERE source_event_id = ?") + .bind(TERMINAL_SOURCE_EVENT_ID) + .first(), + ).toEqual({ count: 0 }); + }); + + test.each([ + { + artifactSandbox: { resolveError: new Error("injected missing sandbox") }, + name: "sandbox resolution", + }, + { + name: "conversation lookup", + removeConversation: true, + }, + { + artifactSandbox: { listError: new Error("injected output listing failure") }, + name: "output listing", + }, + { + artifactSandbox: { + files: new Map([["report.txt", "uncommitted"]]), + readError: new Error("injected output read failure"), + }, + name: "output read", + }, + { + artifactSandbox: { + files: new Map([ + ["valid.txt", "capturable"], + ["nested/tab\tline\n.txt", "unrepresentable"], + ]), + }, + name: "output filename", + }, + ] as const)("commits terminal authority when artifact $name is unavailable", async (failure) => { + const database = await createPublicHttpContractDatabase(); + await insertRuntimeFixture(database); + const bindings = createPublicHttpTestBindings( + database, + failure.artifactSandbox === undefined ? {} : { artifactSandbox: failure.artifactSandbox }, + ) as ApiBindings; + const finalMessageId = createPlatformId(); + await pushFreshController( + bindings, + messageEvents({ + messageId: finalMessageId, + sourcePrefix: `artifact-failure:${failure.name}:message`, + text: "This answer must remain canonical.", + }), + ); + if ("removeConversation" in failure) { + await database + .prepare("DELETE FROM sandbox_session WHERE session_id = ?") + .bind(SESSION_ID) + .run(); + } + const sourceEventId = `artifact-failure:${failure.name}:run-completed`; + + await expect( + pushFreshController(bindings, [ + runtimeEvent({ + kind: "run.completed", + payload: { finalMessageId, stopReason: "end_turn" }, + sourceEventId, + }), + ]), + ).resolves.toHaveLength(1); + expect(await readRuntimeArtifactManifest(database, TERMINAL_SOURCE_EVENT_ID)).toMatchObject({ + captureStatus: "omitted_runtime_unavailable", + files: [], + mode: "snapshot", + sourceEventId: TERMINAL_SOURCE_EVENT_ID, + }); + expect( + await database + .prepare("SELECT COUNT(*) AS count FROM session_artifact_head WHERE session_id = ?") + .bind(SESSION_ID) + .first(), + ).toEqual({ count: 0 }); + expect( + await database.prepare("SELECT status FROM session_run WHERE id = ?").bind(RUN_ID).first(), + ).toEqual({ status: "completed" }); + }); + + test("accepts a mixed message and unavailable file change before a later terminal event", async () => { + const database = await createPublicHttpContractDatabase(); + await insertRuntimeFixture(database); + const files = new Map([["report.txt", "existing artifact"]]); + const bucket = new PublicApiMemoryFileBucket(); + const healthyBindings = createPublicHttpTestBindings(database, { + artifactSandbox: { files }, + fileBucket: bucket, + }) as ApiBindings; + await pushFreshController(healthyBindings, [ + runtimeEvent({ + kind: "file.changed", + payload: { changes: [{ change: "upsert", path: "outputs/report.txt" }] }, + sourceEventId: "artifact-unavailable:existing", + }), + ]); + const headBefore = await database + .prepare( + `SELECT file_id, runtime_event_seq, source_event_id + FROM session_artifact_head + WHERE session_id = ? AND source_path = 'outputs/report.txt'`, + ) + .bind(SESSION_ID) + .first(); + const messageId = createPlatformId(); + const messageSourceEventId = "artifact-unavailable:message"; + const artifactSourceEventId = "artifact-unavailable:file"; + files.set("report.txt", "must not replace the existing head"); + + await expect( + pushFreshController( + createPublicHttpTestBindings(database, { + artifactSandbox: { + files, + readError: new Error("injected unavailable artifact content"), + }, + fileBucket: bucket, + }) as ApiBindings, + [ + runtimeEvent({ + kind: "message.added", + payload: { content: "Canonical despite artifact loss.", messageId, role: "agent" }, + sourceEventId: messageSourceEventId, + }), + runtimeEvent({ + kind: "file.changed", + payload: { changes: [{ change: "upsert", path: "outputs/report.txt" }] }, + sourceEventId: artifactSourceEventId, + }), + ], + ), + ).resolves.toHaveLength(2); + + expect(await readRuntimeArtifactManifest(database, artifactSourceEventId)).toMatchObject({ + captureStatus: "omitted_runtime_unavailable", + files: [], + mode: "delta", + }); + expect( + await database + .prepare("SELECT COUNT(*) AS count FROM session_event WHERE source_event_id = ?") + .bind(messageSourceEventId) + .first(), + ).toEqual({ count: 1 }); + expect( + await database + .prepare( + `SELECT file_id, runtime_event_seq, source_event_id + FROM session_artifact_head + WHERE session_id = ? AND source_path = 'outputs/report.txt'`, + ) + .bind(SESSION_ID) + .first(), + ).toEqual(headBefore); + + await expect( + pushFreshController(healthyBindings, [ + runtimeEvent({ + kind: "run.completed", + payload: { stopReason: "end_turn" }, + sourceEventId: "artifact-unavailable:terminal", + }), + ]), + ).resolves.toHaveLength(1); + expect( + await database.prepare("SELECT status FROM session_run WHERE id = ?").bind(RUN_ID).first(), + ).toEqual({ status: "completed" }); + }); + + test.each(["create", "claim", "put", "seal", "commit"] as const)( + "converges after artifact %s acknowledgement loss and collects losers", + async (phase) => { + const database = await createPublicHttpContractDatabase(); + await insertRuntimeFixture(database); + const sourceEventId = `artifact-ack-loss:${phase}`; + const files = new Map([["report.txt", "before-receipt"]]); + const bucket = new PublicApiMemoryFileBucket(); + const databaseFailure = + phase === "commit" + ? failAfterRuntimeArtifactCommit(database, sourceEventId) + : phase === "put" + ? { database, wasInjected: () => false } + : failAfterRuntimeArtifactDatabaseWrite(database, phase); + const putFailure = + phase === "put" + ? failAfterRuntimeArtifactPut(bucket) + : { bucket: bucket as unknown as R2Bucket, wasInjected: () => false }; + const event = runtimeEvent({ + kind: "file.changed", + payload: { changes: [{ change: "upsert", path: "outputs/report.txt" }] }, + sourceEventId, + }); + + const firstPush = pushFreshController( + createPublicHttpTestBindings(databaseFailure.database, { + artifactSandbox: { files }, + fileBucket: putFailure.bucket, + }) as ApiBindings, + [event], + ); + if (phase === "put") { + await expect(firstPush).resolves.toHaveLength(1); + } else { + await expect(firstPush).rejects.toThrow(`injected artifact ${phase} acknowledgement loss`); + } + expect(databaseFailure.wasInjected() || putFailure.wasInjected()).toBe(true); + + files.set("report.txt", "canonical-after-retry"); + const replayReads: string[] = []; + await expect( + pushFreshController( + createPublicHttpTestBindings(database, { + artifactSandbox: { files, onRead: (path) => replayReads.push(path) }, + fileBucket: bucket as unknown as R2Bucket, + }) as ApiBindings, + [event], + ), + ).resolves.toHaveLength(1); + + const attempts = await database + .prepare( + `SELECT accepted_event_id, id, owned_object_keys_json, status + FROM runtime_artifact_attempt + WHERE session_id = ? AND source_event_id = ? + ORDER BY id`, + ) + .bind(SESSION_ID, sourceEventId) + .all<{ + accepted_event_id: string | null; + id: string; + owned_object_keys_json: string; + status: string; + }>(); + const accepted = attempts.results.filter((attempt) => attempt.status === "accepted"); + const losers = attempts.results.filter((attempt) => attempt.status !== "accepted"); + expect(accepted).toHaveLength(1); + expect(losers).toHaveLength(phase === "commit" ? 0 : 1); + expect( + await database + .prepare("SELECT COUNT(*) AS count FROM session_event WHERE source_event_id = ?") + .bind(sourceEventId) + .first(), + ).toEqual({ count: 1 }); + expect( + await database + .prepare( + `SELECT COUNT(*) AS count + FROM session_artifact_head + WHERE session_id = ? AND source_path = 'outputs/report.txt' + AND source_event_id = ? AND file_id IS NOT NULL`, + ) + .bind(SESSION_ID, sourceEventId) + .first(), + ).toEqual({ count: phase === "put" ? 0 : 1 }); + + const acceptedManifest = await readRuntimeArtifactManifest(database, sourceEventId); + const winnerObjectKey = acceptedManifest.files[0]?.objectKey; + expect(acceptedManifest.captureStatus).toBe( + phase === "put" ? "omitted_runtime_unavailable" : "complete", + ); + if (winnerObjectKey !== undefined) { + const winner = await bucket.get(winnerObjectKey); + expect(await winner?.text()).toBe( + phase === "commit" ? "before-receipt" : "canonical-after-retry", + ); + } + expect(replayReads).toEqual( + phase === "commit" || phase === "put" ? [] : ["/workspace/outputs/report.txt"], + ); + + if (losers.length > 0) { + await database + .prepare( + `UPDATE runtime_artifact_attempt + SET created_at = 0, expires_at = 0, updated_at = 0 + WHERE id = ? AND status IN ('staging', 'staged')`, + ) + .bind(losers[0]!.id) + .run(); + await cleanupRuntimeArtifactAttempts( + createPublicHttpTestBindings(database, { + fileBucket: bucket as unknown as R2Bucket, + }) as ApiBindings, + ); + expect( + await database + .prepare("SELECT status FROM runtime_artifact_attempt WHERE id = ?") + .bind(losers[0]!.id) + .first(), + ).toEqual({ status: "deleting" }); + if (winnerObjectKey !== undefined) { + expect(await bucket.head(winnerObjectKey)).not.toBeNull(); + } + for (const objectKey of JSON.parse(losers[0]!.owned_object_keys_json) as string[]) { + expect(await bucket.head(objectKey)).toBeNull(); + } + + await database + .prepare( + `UPDATE runtime_artifact_attempt + SET delete_after = 0, updated_at = 0 + WHERE id = ? AND status = 'deleting'`, + ) + .bind(losers[0]!.id) + .run(); + await cleanupRuntimeArtifactAttempts( + createPublicHttpTestBindings(database, { + fileBucket: bucket as unknown as R2Bucket, + }) as ApiBindings, + ); + expect( + await database + .prepare("SELECT id FROM runtime_artifact_attempt WHERE id = ?") + .bind(losers[0]!.id) + .first(), + ).toBeNull(); + } + if (winnerObjectKey !== undefined) { + expect(await bucket.head(winnerObjectKey)).not.toBeNull(); + } + }, + ); + + test("accepts a 101-file completion omission without hiding prior artifacts", async () => { + const database = await createPublicHttpContractDatabase(); + await insertRuntimeFixture(database); + const files = new Map([["existing.txt", "previous artifact"]]); + const readPaths: string[] = []; + const bindings = createPublicHttpTestBindings(database, { + artifactSandbox: { files, onRead: (path) => readPaths.push(path) }, + fileBucket: new PublicApiMemoryFileBucket(), + }) as ApiBindings; + + await pushFreshController(bindings, [ + runtimeEvent({ + kind: "file.changed", + payload: { changes: [{ change: "upsert", path: "outputs/existing.txt" }] }, + sourceEventId: "artifact-quota:file-before-completion", + }), + ]); + files.clear(); + for (let index = 0; index < 101; index += 1) { + files.set(`generated-${String(index).padStart(3, "0")}.txt`, "x"); + } + const sourceEventId = "artifact-quota:101-files-completed"; + + await expect( + pushFreshController(bindings, [ + runtimeEvent({ + kind: "run.completed", + payload: { stopReason: "end_turn" }, + sourceEventId, + }), + ]), + ).resolves.toHaveLength(1); + + const row = await database + .prepare( + `SELECT head.file_id, head.runtime_event_seq, attempt.status AS attempt_status, + run.status AS run_status + FROM session_event AS event + INNER JOIN runtime_artifact_attempt AS attempt + ON attempt.id = event.artifact_attempt_id + INNER JOIN session_run AS run ON run.id = event.run_id + INNER JOIN session_artifact_head AS head + ON head.session_id = event.session_id + AND head.source_path = 'outputs/existing.txt' + WHERE event.source_event_id = ?`, + ) + .bind(TERMINAL_SOURCE_EVENT_ID) + .first<{ + attempt_status: string; + file_id: string | null; + run_status: string; + runtime_event_seq: number; + }>(); + expect(await readRuntimeArtifactManifest(database, TERMINAL_SOURCE_EVENT_ID)).toMatchObject({ + captureStatus: "omitted_file_limit", + files: [], + mode: "snapshot", + }); + expect(row).toMatchObject({ + attempt_status: "accepted", + file_id: expect.any(String), + run_status: "completed", + runtime_event_seq: 1, + }); + expect(readPaths).toEqual(["/workspace/outputs/existing.txt"]); + }); + + test.each([ + { + fileSizes: new Map([["large.bin", RUNTIME_SESSION_OUTPUT_MAX_FILE_BYTES + 1]]), + files: new Map([["large.bin", "must not be read"]]), + name: "single-file limit", + }, + { + fileSizes: new Map([ + ["one.bin", Math.floor(RUNTIME_SESSION_OUTPUT_MAX_TOTAL_BYTES / 2) + 1], + ["two.bin", Math.floor(RUNTIME_SESSION_OUTPUT_MAX_TOTAL_BYTES / 2) + 1], + ]), + files: new Map([ + ["one.bin", "must not be read"], + ["two.bin", "must not be read"], + ]), + name: "total-byte limit", + }, + ])("accepts a completion omitted by the artifact $name", async ({ fileSizes, files, name }) => { + const database = await createPublicHttpContractDatabase(); + await insertRuntimeFixture(database); + const bindings = createPublicHttpTestBindings(database, { + artifactSandbox: { + fileSizes, + files, + readError: new Error("quota omission must not read content"), + }, + }) as ApiBindings; + const sourceEventId = `artifact-quota:${name}`; + + await expect( + pushFreshController(bindings, [ + runtimeEvent({ + kind: "run.completed", + payload: { stopReason: "end_turn" }, + sourceEventId, + }), + ]), + ).resolves.toHaveLength(1); + + const row = await database + .prepare( + `SELECT attempt.status AS attempt_status, run.status AS run_status + FROM session_event AS event + INNER JOIN runtime_artifact_attempt AS attempt + ON attempt.id = event.artifact_attempt_id + INNER JOIN session_run AS run ON run.id = event.run_id + WHERE event.source_event_id = ?`, + ) + .bind(TERMINAL_SOURCE_EVENT_ID) + .first<{ + attempt_status: string; + run_status: string; + }>(); + expect(await readRuntimeArtifactManifest(database, TERMINAL_SOURCE_EVENT_ID)).toMatchObject({ + captureStatus: "omitted_size_limit", + files: [], + mode: "snapshot", + }); + expect(row).toMatchObject({ attempt_status: "accepted", run_status: "completed" }); + }); + + test("accepts an oversized file change before the terminal event", async () => { + const database = await createPublicHttpContractDatabase(); + await insertRuntimeFixture(database); + const files = new Map([["large.bin", "must not be read"]]); + const fileSizes = new Map([["large.bin", RUNTIME_SESSION_OUTPUT_MAX_FILE_BYTES + 1]]); + const bindings = createPublicHttpTestBindings(database, { + artifactSandbox: { + fileSizes, + files, + readError: new Error("quota omission must not read content"), + }, + }) as ApiBindings; + const deltaSourceEventId = "artifact-quota:oversized-delta"; + + await expect( + pushFreshController(bindings, [ + runtimeEvent({ + kind: "file.changed", + payload: { changes: [{ change: "upsert", path: "outputs/large.bin" }] }, + sourceEventId: deltaSourceEventId, + }), + ]), + ).resolves.toHaveLength(1); + files.clear(); + fileSizes.clear(); + await expect( + pushFreshController(bindings, [ + runtimeEvent({ + kind: "run.completed", + payload: { stopReason: "end_turn" }, + sourceEventId: "artifact-quota:terminal-after-oversized-delta", + }), + ]), + ).resolves.toHaveLength(1); + + expect(await readRuntimeArtifactManifest(database, deltaSourceEventId)).toMatchObject({ + captureStatus: "omitted_size_limit", + files: [], + mode: "delta", + }); + expect( + await database.prepare("SELECT status FROM session_run WHERE id = ?").bind(RUN_ID).first(), + ).toEqual({ status: "completed" }); + }); + + test("deduplicates a missing upsert before the later delete does sandbox I/O", async () => { + const database = await createPublicHttpContractDatabase(); + await insertRuntimeFixture(database); + const bindings = createPublicHttpTestBindings(database, { + artifactSandbox: { files: new Map() }, + }) as ApiBindings; + const events = [ + runtimeEvent({ + kind: "file.changed", + payload: { changes: [{ change: "upsert", path: "outputs/removed.txt" }] }, + sourceEventId: "artifact-missing:upsert", + }), + runtimeEvent({ + kind: "file.changed", + payload: { changes: [{ change: "delete", path: "outputs/removed.txt" }] }, + sourceEventId: "artifact-missing:delete", + }), + ]; + + await expect(pushFreshController(bindings, events)).resolves.toHaveLength(2); + expect( + await database + .prepare( + `SELECT file_id, source_event_id + FROM session_artifact_head + WHERE session_id = ? AND source_path = 'outputs/removed.txt'`, + ) + .bind(SESSION_ID) + .first(), + ).toEqual({ file_id: null, source_event_id: "artifact-missing:delete" }); + await expect( + pushFreshController(bindings, [ + runtimeEvent({ + kind: "run.completed", + payload: { stopReason: "end_turn" }, + sourceEventId: "artifact-missing:terminal-after-delete", + }), + ]), + ).resolves.toHaveLength(1); + + expect( + await database + .prepare( + `SELECT file_id, source_event_id + FROM session_artifact_head + WHERE session_id = ? AND source_path = 'outputs/removed.txt'`, + ) + .bind(SESSION_ID) + .first(), + ).toEqual({ file_id: null, source_event_id: TERMINAL_SOURCE_EVENT_ID }); + }); + + test("accepts a file upsert whose historical sandbox source is gone", async () => { + const database = await createPublicHttpContractDatabase(); + await insertRuntimeFixture(database); + const bindings = createPublicHttpTestBindings(database, { + artifactSandbox: { files: new Map() }, + }) as ApiBindings; + const sourceEventId = "artifact-missing:historical-upsert"; + + await expect( + pushFreshController(bindings, [ + runtimeEvent({ + kind: "file.changed", + payload: { changes: [{ change: "upsert", path: "outputs/gone.txt" }] }, + sourceEventId, + }), + ]), + ).resolves.toHaveLength(1); + expect(await readRuntimeArtifactManifest(database, sourceEventId)).toMatchObject({ + captureStatus: "omitted_source_missing", + files: [], + mode: "delta", + }); + + await expect( + pushFreshController(bindings, [ + runtimeEvent({ + kind: "run.completed", + payload: { stopReason: "end_turn" }, + sourceEventId: "artifact-missing:terminal-after-historical-upsert", + }), + ]), + ).resolves.toHaveLength(1); + expect( + await database.prepare("SELECT status FROM session_run WHERE id = ?").bind(RUN_ID).first(), + ).toEqual({ status: "completed" }); + }); + + test("bounds a file that grows beyond quota between stat and read", async () => { + const database = await createPublicHttpContractDatabase(); + await insertRuntimeFixture(database); + const files = new Map([["growing.bin", "x".repeat(RUNTIME_SESSION_OUTPUT_MAX_FILE_BYTES + 1)]]); + const bindings = createPublicHttpTestBindings(database, { + artifactSandbox: { + fileSizes: new Map([["growing.bin", 1]]), + files, + }, + }) as ApiBindings; + + await expect( + pushFreshController(bindings, [ + runtimeEvent({ + kind: "run.completed", + payload: { stopReason: "end_turn" }, + sourceEventId: "artifact-quota:growing-file", + }), + ]), + ).resolves.toHaveLength(1); + + expect(await readRuntimeArtifactManifest(database, TERMINAL_SOURCE_EVENT_ID)).toMatchObject({ + captureStatus: "omitted_size_limit", + files: [], + mode: "snapshot", + }); + }); + + test("omits a source that changes below quota between stat and read", async () => { + const database = await createPublicHttpContractDatabase(); + await insertRuntimeFixture(database); + const files = new Map([["report.txt", "old"]]); + const fileSizes = new Map(); + const bindings = createPublicHttpTestBindings(database, { + artifactSandbox: { fileSizes, files }, + fileBucket: new PublicApiMemoryFileBucket(), + }) as ApiBindings; + await pushFreshController(bindings, [ + runtimeEvent({ + kind: "file.changed", + payload: { changes: [{ change: "upsert", path: "outputs/report.txt" }] }, + sourceEventId: "artifact-changed:existing", + }), + ]); + const headBefore = await database + .prepare( + `SELECT file_id, runtime_event_seq, source_event_id + FROM session_artifact_head + WHERE session_id = ? AND source_path = 'outputs/report.txt'`, + ) + .bind(SESSION_ID) + .first(); + files.set("report.txt", "x"); + fileSizes.set("report.txt", 2); + const sourceEventId = "artifact-changed:delta"; + + await expect( + pushFreshController(bindings, [ + runtimeEvent({ + kind: "file.changed", + payload: { changes: [{ change: "upsert", path: "outputs/report.txt" }] }, + sourceEventId, + }), + ]), + ).resolves.toHaveLength(1); + expect(await readRuntimeArtifactManifest(database, sourceEventId)).toMatchObject({ + captureStatus: "omitted_source_changed", + files: [], + mode: "delta", + }); + expect( + await database + .prepare( + `SELECT file_id, runtime_event_seq, source_event_id + FROM session_artifact_head + WHERE session_id = ? AND source_path = 'outputs/report.txt'`, + ) + .bind(SESSION_ID) + .first(), + ).toEqual(headBefore); + + fileSizes.clear(); + await expect( + pushFreshController(bindings, [ + runtimeEvent({ + kind: "run.completed", + payload: { stopReason: "end_turn" }, + sourceEventId: "artifact-changed:terminal", + }), + ]), + ).resolves.toHaveLength(1); + expect( + await database.prepare("SELECT status FROM session_run WHERE id = ?").bind(RUN_ID).first(), + ).toEqual({ status: "completed" }); + }); + + test("omits an artifact delta batch when a later same-path capture is unavailable", async () => { + const database = await createPublicHttpContractDatabase(); + await insertRuntimeFixture(database); + const files = new Map([["report.txt", "existing"]]); + const bucket = new PublicApiMemoryFileBucket(); + const healthyBindings = createPublicHttpTestBindings(database, { + artifactSandbox: { files }, + fileBucket: bucket, + }) as ApiBindings; + await pushFreshController(healthyBindings, [ + runtimeEvent({ + kind: "file.changed", + payload: { changes: [{ change: "upsert", path: "outputs/report.txt" }] }, + sourceEventId: "artifact-batch-unavailable:existing", + }), + ]); + const headBefore = await database + .prepare( + `SELECT file_id, runtime_event_seq, source_event_id + FROM session_artifact_head + WHERE session_id = ? AND source_path = 'outputs/report.txt'`, + ) + .bind(SESSION_ID) + .first(); + const sourceEventIds = [ + "artifact-batch-unavailable:earlier", + "artifact-batch-unavailable:later", + ]; + + await expect( + pushFreshController( + createPublicHttpTestBindings(database, { + artifactSandbox: { + files, + resolveError: new Error("injected unavailable later capture"), + }, + fileBucket: bucket, + }) as ApiBindings, + sourceEventIds.map((sourceEventId) => + runtimeEvent({ + kind: "file.changed", + payload: { changes: [{ change: "upsert", path: "outputs/report.txt" }] }, + sourceEventId, + }), + ), + ), + ).resolves.toHaveLength(2); + expect( + await Promise.all(sourceEventIds.map((id) => readRuntimeArtifactManifest(database, id))), + ).toEqual([ + expect.objectContaining({ + captureStatus: "omitted_runtime_unavailable", + files: [], + mode: "delta", + }), + expect.objectContaining({ + captureStatus: "omitted_runtime_unavailable", + files: [], + mode: "delta", + }), + ]); + expect( + await database + .prepare( + `SELECT file_id, runtime_event_seq, source_event_id + FROM session_artifact_head + WHERE session_id = ? AND source_path = 'outputs/report.txt'`, + ) + .bind(SESSION_ID) + .first(), + ).toEqual(headBefore); + }); + + test("rejects fresh run events after terminal repair already won", async () => { + const database = await createPublicHttpContractDatabase(); + await insertRuntimeFixture(database); + const bindings = createPublicHttpTestBindings(database) as ApiBindings; + const failureSourceEventId = `session-run-terminal:${RUN_ID}:run.failed`; + await recordCanonicalSessionRunFailure(bindings, { + error: { + code: "runtime.driver_terminal", + details: {}, + message: "Driver disconnected.", + retryable: true, + }, + runId: RUN_ID, + sessionId: SESSION_ID, + source: "api", + }); + await expect( + loadSessionViewerState(database, { + sessionId: SESSION_ID, + viewerId: PUBLIC_API_TEST_IDS.ownerAccount, + }), + ).resolves.toMatchObject({ + run: { error: { retryable: true } }, + }); + const messageId = createPlatformId(); + const delta = runtimeEvent({ + kind: "message.delta", + payload: { contentDelta: "durable prefix", messageId, role: "agent" }, + sourceEventId: "recovered-message-delta", + }); + const failed = runtimeEvent({ + kind: "run.failed", + payload: { + error: { + code: "runtime.driver_terminal", + details: {}, + message: "Driver disconnected.", + retryable: true, + }, + recoverable: true, + }, + sourceEventId: "driver-run-failed", + }); + + await expect( + createController(bindings).handlePushEvents( + { driverInstanceId: DRIVER_ID, events: [delta, failed] }, + activeContext, + ), + ).rejects.toThrow("conflicts with its durable receipt"); + + const rows = await database + .prepare( + `SELECT event_type, source_event_id + FROM session_event + WHERE source_event_id IN (?, ?) + ORDER BY seq`, + ) + .bind(delta.eventId, failureSourceEventId) + .all<{ event_type: string; source_event_id: string }>(); + expect(rows.results).toEqual([ + { event_type: "run.failed", source_event_id: failureSourceEventId }, + ]); + }); + + test.each([ + ["in the terminal RPC", false], + ["before the terminal RPC", true], + ] as const)( + "persists one final assistant snapshot when the sealed stream arrives %s", + async (_arrival, persistBeforeTerminal) => { + const database = await createPublicHttpContractDatabase(); + await insertRuntimeFixture(database); + const capturedEvents: unknown[] = []; + setServerProductAnalyticsTransportForTests(async (_input, init) => { + capturedEvents.push(JSON.parse(init.body as string) as unknown); + return new Response(null, { status: 200 }); + }); + const bindings = { + ...createPublicHttpTestBindings(database), + POSTHOG_PROJECT_KEY: "phc_test", + } as ApiBindings; + const finalText = "The final answer."; + const finalMessageId = createPlatformId(); + const stream = messageEvents({ + messageId: finalMessageId, + sourcePrefix: "final-stream", + text: finalText, + }); + const terminal = runtimeEvent({ + kind: "run.completed", + payload: { finalMessageId, stopReason: "end_turn" }, + sourceEventId: "final-stream:run-completed", + }); + + if (persistBeforeTerminal) { + await pushFreshController(bindings, stream); + await pushFreshController(bindings, [terminal]); + } else { + await pushFreshController(bindings, [...stream, terminal]); + } + + const rows = await database + .prepare( + `SELECT content_text, ended_at, event_type, id, occurred_at, process_status, + process_type, run_id, seq, stream_id, tokens + FROM session_event + WHERE session_id = ? AND run_id = ? + ORDER BY seq`, + ) + .bind(SESSION_ID, RUN_ID) + .all(); + const assistantMessages = createSessionProcessEventsFromSessionEventRows(rows.results).filter( + (event) => event.type === "agent.message.delta", + ); + + expect( + rows.results + .filter((row) => row.event_type === "message.added") + .map((row) => row.content_text), + ).toEqual([finalText]); + expect(assistantMessages.map((event) => event.content)).toEqual([finalText]); + expect(capturedEvents).toEqual([ + expect.objectContaining({ + event: "task_succeeded", + properties: expect.objectContaining({ + run_duration_ms: expect.any(Number), + sandbox_id: PUBLIC_API_TEST_IDS.sandbox, + sandbox_kind: "pet", + sandbox_subject_kind: "agent", + session_type: "ui", + }), + }), + ]); + }, + ); + + test("keeps equal text from distinct progress and final message streams", async () => { + const database = await createPublicHttpContractDatabase(); + await insertRuntimeFixture(database); + const bindings = createPublicHttpTestBindings(database) as ApiBindings; + const progressMessageId = createPlatformId(); + const finalMessageId = createPlatformId(); + const text = "The same text belongs to two messages."; + + await pushFreshController(bindings, [ + ...messageEvents({ + messageId: progressMessageId, + sourcePrefix: "equal-text:progress", + text, + }), + ...messageEvents({ + messageId: finalMessageId, + sourcePrefix: "equal-text:final", + text, + }), + runtimeEvent({ + kind: "run.completed", + payload: { finalMessageId, stopReason: "end_turn" }, + sourceEventId: "equal-text:run-completed", + }), + ]); + + const rows = await database + .prepare( + `SELECT content_text, ended_at, event_type, id, occurred_at, process_status, + process_type, run_id, seq, stream_id, tokens + FROM session_event + WHERE session_id = ? AND run_id = ? + ORDER BY seq`, + ) + .bind(SESSION_ID, RUN_ID) + .all(); + const messageRows = rows.results.filter((row) => row.event_type === "message.added"); + const messages = createSessionProcessEventsFromSessionEventRows(rows.results).filter( + (event) => event.type === "agent.message.delta", + ); + + expect(messageRows.map((row) => [row.stream_id, row.content_text])).toEqual([ + [progressMessageId, text], + [finalMessageId, text], + ]); + expect(messages.map((message) => message.content)).toEqual([text, text]); + }); + + test("atomically seals a multi-megabyte final reference across hibernation and replay", async () => { + const database = await createPublicHttpContractDatabase(); + await insertRuntimeFixture(database); + const syncedSessionIds: string[] = []; + const bindings = createPublicHttpTestBindings(database, { + sessionNamespace: createSessionSyncNamespace(syncedSessionIds), + }) as ApiBindings; + const progressMessageIds = PROGRESS_TEXTS.map(() => createPlatformId()); + const finalMessageId = createPlatformId(); + const progressEvents = [ + runtimeEvent({ + kind: "run.started", + payload: { startedAt: new Date(1).toISOString() }, + sourceEventId: "canary:run-started", + }), + ...PROGRESS_TEXTS.flatMap((text, index) => + messageEvents({ + messageId: progressMessageIds[index], + sourcePrefix: `canary:progress:${index + 1}`, + text, + }), + ), + ]; + + expect( + (await pushFreshController(bindings, progressEvents)).map((receipt) => receipt.eventId), + ).toEqual(progressEvents.map((event) => event.eventId)); + + const toolEvents = [ + runtimeEvent({ + kind: "tool.call.updated", + payload: { + parentMessageId: finalMessageId, + rawInputDelta: '{"path":', + status: "running", + title: "Create artifact", + toolCallId: "tool-canary", + }, + sourceEventId: "canary:tool:started", + }), + runtimeEvent({ + kind: "tool.call.updated", + payload: { + rawInputDelta: '"report.md"}', + rawOutputDelta: "creating ", + status: "running", + toolCallId: "tool-canary", + }, + sourceEventId: "canary:tool:streamed", + }), + ]; + expect(await pushFreshController(bindings, toolEvents)).toHaveLength(toolEvents.length); + + const finalTextChunks = Array.from( + { length: Math.ceil(LARGE_FINAL_TEXT.length / LARGE_FINAL_TEXT_CHUNK_CHARACTERS) }, + (_, index) => + LARGE_FINAL_TEXT.slice( + index * LARGE_FINAL_TEXT_CHUNK_CHARACTERS, + (index + 1) * LARGE_FINAL_TEXT_CHUNK_CHARACTERS, + ), + ); + const finalStreamEvents = [ + runtimeEvent({ + kind: "message.started", + payload: { messageId: finalMessageId, role: "agent" }, + sourceEventId: "canary:final:started", + }), + runtimeEvent({ + kind: "message.added", + payload: { content: finalTextChunks[0], messageId: finalMessageId, role: "agent" }, + sourceEventId: "canary:final:snapshot", + }), + ...finalTextChunks.slice(1).map((contentDelta, index) => + runtimeEvent({ + kind: "message.delta", + payload: { contentDelta, messageId: finalMessageId, role: "agent" }, + sourceEventId: `canary:final:delta:${index + 2}`, + }), + ), + ]; + expect(finalStreamEvents.length).toBeGreaterThan(1_000); + const finalBatches = splitIntoBatches(finalStreamEvents, 64); + + for (const batch of finalBatches.slice(0, -1)) { + expect(await pushFreshController(bindings, batch)).toHaveLength(batch.length); + } + + const terminalBatch = [ + ...(finalBatches.at(-1) ?? []), + runtimeEvent({ + kind: "tool.call.updated", + payload: { + rawInput: '{"path":"report.md"}', + rawOutput: "artifact created", + status: "completed", + toolCallId: "tool-canary", + }, + sourceEventId: "canary:tool:completed", + }), + runtimeEvent({ + kind: "message.completed", + payload: { messageId: finalMessageId, role: "agent" }, + sourceEventId: "canary:final:completed", + }), + runtimeEvent({ + kind: "run.completed", + payload: { finalMessageId, stopReason: "end_turn" }, + sourceEventId: TERMINAL_SOURCE_EVENT_ID, + }), + ]; + const failingBindings = { + ...bindings, + DB: failTerminalSessionEventInsert(database), + } as ApiBindings; + + await expect(pushFreshController(failingBindings, terminalBatch)).rejects.toBeInstanceOf(Error); + + const completedRun = await database + .prepare("SELECT status FROM session_run WHERE id = ?") + .bind(RUN_ID) + .first<{ status: string }>(); + const projectedMessagesBeforeReplay = await database + .prepare("SELECT content_text, id FROM session_message WHERE session_run_id = ? ORDER BY seq") + .bind(RUN_ID) + .all<{ content_text: string; id: string }>(); + const finalOutputBeforeReplay = await readPublicThreadRunFinalOutput({ + database, + runId: RUN_ID, + sessionId: SESSION_ID, + }); + const terminalRowsBeforeReplay = await database + .prepare("SELECT source_event_id FROM session_event WHERE source_event_id = ?") + .bind(TERMINAL_SOURCE_EVENT_ID) + .all<{ source_event_id: string }>(); + const driverReleaseBeforeReplay = await database + .prepare("SELECT status_operation_id FROM driver_instance WHERE id = ?") + .bind(DRIVER_ID) + .first<{ status_operation_id: string | null }>(); + + expect(completedRun?.status).toBe("running"); + expect(projectedMessagesBeforeReplay.results).toEqual([]); + expect(new TextEncoder().encode(LARGE_FINAL_TEXT).byteLength).toBeGreaterThan(2_000_000); + expect(finalOutputBeforeReplay).toBeNull(); + expect(terminalRowsBeforeReplay.results).toEqual([]); + expect(driverReleaseBeforeReplay).toEqual({ status_operation_id: null }); + + const crossBootTerminalBatch = [ + runtimeEvent({ + kind: "run.completed", + payload: { finalMessageId, stopReason: "end_turn" }, + sourceEventId: TERMINAL_SOURCE_EVENT_ID, + }), + ]; + const expectedCrossBootReceiptIds = crossBootTerminalBatch.map((event) => event.eventId); + const crossBootDelivery: SessionDeliveryEvent[] = []; + const structuralTerminalBindings = { + ...bindings, + DB: rejectSessionEventContentReads(database), + } as ApiBindings; + + expect( + ( + await createController(structuralTerminalBindings, (_sessionId, events) => + crossBootDelivery.push(...events), + ).handlePushEvents( + { driverInstanceId: DRIVER_ID, events: crossBootTerminalBatch }, + activeContext, + ) + ).accepted.map((receipt) => receipt.eventId), + ).toEqual(expectedCrossBootReceiptIds); + expect(crossBootDelivery).toEqual([]); + expect(syncedSessionIds).toEqual([SESSION_ID]); + expect( + (await pushFreshController(bindings, terminalBatch)).map((receipt) => receipt.eventId), + ).toEqual(terminalBatch.map((event) => event.eventId)); + expect(syncedSessionIds).toEqual([SESSION_ID, SESSION_ID]); + await expect( + database + .prepare("SELECT status_operation_id FROM driver_instance WHERE id = ?") + .bind(DRIVER_ID) + .first(), + ).resolves.toEqual({ status_operation_id: RUN_ID }); + + const projectedMessagesAfterReplay = await database + .prepare( + "SELECT content_text, id, plan_json, projection_format, segments_json FROM session_message WHERE session_run_id = ? ORDER BY seq", + ) + .bind(RUN_ID) + .all<{ + content_text: string; + id: string; + plan_json: string | null; + projection_format: string; + segments_json: string | null; + }>(); + const terminalRowsAfterReplay = await database + .prepare("SELECT source_event_id FROM session_event WHERE source_event_id = ?") + .bind(TERMINAL_SOURCE_EVENT_ID) + .all<{ source_event_id: string }>(); + const transcript = await loadSessionViewerState(database, { + sessionId: SESSION_ID, + viewerId: PUBLIC_API_TEST_IDS.ownerAccount, + }); + const finalTranscriptMessage = transcript.messages.find( + (message) => message.id === finalMessageId, + ); + const canonicalTranscriptMessages = transcript.messages.filter( + (message) => message.role === "assistant" && message.content === LARGE_FINAL_TEXT, + ); + const measuredRecovery = measurePreparedQueries(database); + const recoveryMessages = await getSessionRuntimeRecoveryMessages(measuredRecovery.database, { + excludeRunId: null, + sessionId: SESSION_ID, + }); + + expect(projectedMessagesAfterReplay.results).toEqual([ + { + content_text: "", + id: finalMessageId, + plan_json: null, + projection_format: "event_stream_v3", + segments_json: null, + }, + ]); + expect(terminalRowsAfterReplay.results).toEqual([ + { source_event_id: TERMINAL_SOURCE_EVENT_ID }, + ]); + expect(finalTranscriptMessage?.content).toBe(LARGE_FINAL_TEXT); + expect(canonicalTranscriptMessages.map((message) => message.id)).toEqual([finalMessageId]); + expect(finalTranscriptMessage?.content).not.toContain(PROGRESS_TEXTS.join("")); + expect(finalTranscriptMessage?.segments.filter((segment) => segment.kind !== "text")).toEqual([ + { + argsText: '{"path":"report.md"}', + kind: "tool_use", + path: null, + runId: RUN_ID, + tool: "Create artifact", + toolCallId: "tool-canary", + }, + { + kind: "tool_result", + output: "artifact created", + runId: RUN_ID, + tool: "Create artifact", + toolCallId: "tool-canary", + }, + ]); + expect(LARGE_FINAL_TEXT.split("\n")).toContain("160|中文长文本校验-Aa9-表格字符|END160"); + expect(recoveryMessages).toEqual([ + { content: LARGE_FINAL_TEXT.slice(0, 32_000), role: "assistant" }, + ]); + expect(measuredRecovery.readCount()).toBeLessThanOrEqual(10); + }); + + test("removes provider-private citations at the public final-output boundary", async () => { + const database = await createPublicHttpContractDatabase(); + await insertRuntimeFixture(database); + const bindings = createPublicHttpTestBindings(database) as ApiBindings; + const finalMessageId = createPlatformId(); + const privateCitation = "\uE200cite\uE202turn2view0\uE202turn8view0\uE201"; + const providerText = `before${privateCitation}after`; + const events = [ + ...messageEvents({ + messageId: finalMessageId, + sourcePrefix: "private-citation:final", + text: providerText, + }), + runtimeEvent({ + kind: "run.completed", + payload: { + finalMessageId, + stopReason: "end_turn", + }, + sourceEventId: "private-citation:run-completed", + }), + ]; + + await pushFreshController(bindings, events); + + const persistedMessage = await database + .prepare("SELECT content_text FROM session_message WHERE id = ?") + .bind(finalMessageId) + .first<{ content_text: string }>(); + + expect(persistedMessage?.content_text).toBe(""); + await expect( + readPublicThreadRunFinalOutput({ database, runId: RUN_ID, sessionId: SESSION_ID }), + ).resolves.toEqual({ + text: "beforeafter", + warnings: [ + { + code: "unresolved_provider_citation", + count: 1, + }, + ], + }); + }); + + test("fails closed when a v3 terminal coexists with a legacy terminal", async () => { + const database = await createPublicHttpContractDatabase(); + await insertRuntimeFixture(database); + const bindings = createPublicHttpTestBindings(database) as ApiBindings; + const finalMessageId = createPlatformId(); + await pushFreshController(bindings, [ + ...messageEvents({ + messageId: finalMessageId, + sourcePrefix: "mixed-terminal:final", + text: FINAL_TEXT, + }), + runtimeEvent({ + kind: "run.completed", + payload: { finalMessageId, stopReason: "end_turn" }, + sourceEventId: TERMINAL_SOURCE_EVENT_ID, + }), + ]); + const session = await database + .prepare("SELECT runtime_event_seq_cursor FROM session WHERE id = ?") + .bind(SESSION_ID) + .first<{ runtime_event_seq_cursor: number }>(); + if (session === null) { + throw new Error("Missing Session cursor fixture."); + } + await database + .prepare( + `UPDATE session_event + SET artifact_attempt_id = NULL, + artifact_manifest_json = NULL, + artifact_manifest_sha256 = NULL, + semantic_hash = NULL, + terminal_event_json = NULL, + source_event_id = 'legacy-terminal' + WHERE source_event_id = ?`, + ) + .bind(TERMINAL_SOURCE_EVENT_ID) + .run(); + const v3TerminalSeq = session.runtime_event_seq_cursor + 1; + await insertRuntimeEvent(database, { + eventId: createPlatformId(), + kind: "run.completed", + occurredAt: Date.now(), + payload: { finalMessageId, stopReason: "end_turn" }, + runId: RUN_ID, + seq: v3TerminalSeq, + sessionId: SESSION_ID, + }); + await database + .prepare("UPDATE session SET runtime_event_seq_cursor = ? WHERE id = ?") + .bind(v3TerminalSeq, SESSION_ID) + .run(); + + await expect( + readPublicThreadRunFinalOutput({ database, runId: RUN_ID, sessionId: SESSION_ID }), + ).rejects.toThrow("not immutable"); + await expect( + loadSessionViewerState(database, { + sessionId: SESSION_ID, + viewerId: PUBLIC_API_TEST_IDS.ownerAccount, + }), + ).rejects.toThrow("terminal"); + }); + + test("budgets v3 recovery text after streaming private-citation removal", async () => { + const database = await createPublicHttpContractDatabase(); + await insertRuntimeFixture(database); + const bindings = createPublicHttpTestBindings(database) as ApiBindings; + const finalMessageId = createPlatformId(); + const visiblePrefix = "a".repeat(31_998); + const visibleTail = "VISIBLE"; + const events = [ + runtimeEvent({ + kind: "message.added", + payload: { + content: `${visiblePrefix}\uE200ci`, + messageId: finalMessageId, + role: "agent", + }, + sourceEventId: "recovery-citation:message-added", + }), + runtimeEvent({ + kind: "message.delta", + payload: { + contentDelta: `te\uE202${"private".repeat(800)}`, + messageId: finalMessageId, + role: "agent", + }, + sourceEventId: "recovery-citation:message-delta-private", + }), + runtimeEvent({ + kind: "message.delta", + payload: { + contentDelta: `\uE201${visibleTail}`, + messageId: finalMessageId, + role: "agent", + }, + sourceEventId: "recovery-citation:message-delta-tail", + }), + runtimeEvent({ + kind: "message.completed", + payload: { messageId: finalMessageId, role: "agent" }, + sourceEventId: "recovery-citation:message-completed", + }), + runtimeEvent({ + kind: "run.completed", + payload: { finalMessageId, stopReason: "end_turn" }, + sourceEventId: TERMINAL_SOURCE_EVENT_ID, + }), + ]; + + await pushFreshController(bindings, events); + + const stored = await database + .prepare("SELECT projection_format FROM session_message WHERE id = ?") + .bind(finalMessageId) + .first<{ projection_format: string }>(); + const recoveryMessages = await getSessionRuntimeRecoveryMessages(database, { + excludeRunId: null, + sessionId: SESSION_ID, + }); + + expect(stored?.projection_format).toBe("event_stream_v3"); + expect(recoveryMessages).toEqual([ + { content: `${visiblePrefix}${visibleTail.slice(0, 2)}`, role: "assistant" }, + ]); + expect(recoveryMessages[0]?.content).not.toContain("\uE200"); + }); + + test.each([1_000, 2_000])( + "seals bounded recovery at an exact %i-row page boundary", + async (streamRowCount) => { + const database = await createPublicHttpContractDatabase(); + await insertRuntimeFixture(database); + const bindings = createPublicHttpTestBindings(database) as ApiBindings; + const finalMessageId = createPlatformId(); + const events = [ + runtimeEvent({ + kind: "message.added", + payload: { content: "A", messageId: finalMessageId, role: "agent" }, + sourceEventId: `exact-page:${streamRowCount}:added`, + }), + ...Array.from({ length: streamRowCount - 2 }, (_, index) => + runtimeEvent({ + kind: "message.delta", + payload: { contentDelta: "b", messageId: finalMessageId, role: "agent" }, + sourceEventId: `exact-page:${streamRowCount}:delta:${index}`, + }), + ), + runtimeEvent({ + kind: "message.completed", + payload: { messageId: finalMessageId, role: "agent" }, + sourceEventId: `exact-page:${streamRowCount}:completed`, + }), + runtimeEvent({ + kind: "run.completed", + payload: { finalMessageId, stopReason: "end_turn" }, + sourceEventId: TERMINAL_SOURCE_EVENT_ID, + }), + ]; + + for (const batch of splitIntoBatches(events, 64)) { + await pushFreshController(bindings, batch); + } + + const measured = measurePreparedQueries(database); + const recoveryMessages = await getSessionRuntimeRecoveryMessages(measured.database, { + excludeRunId: null, + sessionId: SESSION_ID, + }); + + expect(recoveryMessages).toEqual([ + { content: `A${"b".repeat(streamRowCount - 2)}`, role: "assistant" }, + ]); + expect(measured.readCount()).toBeLessThanOrEqual(streamRowCount === 1_000 ? 8 : 10); + }, + ); + + test("omits live-only reasoning from stored final assistant segments", async () => { + const database = await createPublicHttpContractDatabase(); + await insertRuntimeFixture(database); + const bindings = createPublicHttpTestBindings(database) as ApiBindings; + const finalMessageId = createPlatformId(); + const privateReasoningText = "Private reasoning should stay out of stored history."; + const thoughtId = "private-reasoning"; + const events = [ + runtimeEvent({ + kind: "thought.started", + payload: { messageId: finalMessageId, thoughtId }, + sourceEventId: "reasoning:started", + }), + runtimeEvent({ + kind: "thought.delta", + payload: { contentDelta: privateReasoningText, messageId: finalMessageId, thoughtId }, + sourceEventId: "reasoning:delta", + }), + runtimeEvent({ + kind: "thought.completed", + payload: { messageId: finalMessageId, thoughtId }, + sourceEventId: "reasoning:completed", + }), + ...messageEvents({ + messageId: finalMessageId, + sourcePrefix: "reasoning:final", + text: FINAL_TEXT, + }), + runtimeEvent({ + kind: "run.completed", + payload: { + finalMessageId, + stopReason: "end_turn", + }, + sourceEventId: "reasoning:run-completed", + }), + ]; + + await pushFreshController(bindings, events); + + const persistedMessage = await database + .prepare( + "SELECT content_text, projection_format, segments_json FROM session_message WHERE id = ?", + ) + .bind(finalMessageId) + .first<{ content_text: string; projection_format: string; segments_json: string | null }>(); + + expect(persistedMessage).toEqual({ + content_text: "", + projection_format: "event_stream_v3", + segments_json: null, + }); + const transcript = await loadSessionViewerState(database, { + sessionId: SESSION_ID, + viewerId: PUBLIC_API_TEST_IDS.ownerAccount, + }); + const finalMessage = transcript.messages.find((message) => message.id === finalMessageId); + expect(finalMessage?.segments).toEqual([{ kind: "text", text: FINAL_TEXT }]); + expect(JSON.stringify(finalMessage?.segments)).not.toContain(privateReasoningText); + }); + + test("rehydrates the shared failed-tool fallback without explicit output", async () => { + const database = await createPublicHttpContractDatabase(); + await insertRuntimeFixture(database); + const bindings = createPublicHttpTestBindings(database) as ApiBindings; + const finalMessageId = createPlatformId(); + await pushFreshController(bindings, [ + runtimeEvent({ + kind: "message.added", + payload: { content: "Finished.", messageId: finalMessageId, role: "agent" }, + sourceEventId: "failed-tool:message", + }), + runtimeEvent({ + kind: "tool.call.updated", + payload: { + parentMessageId: finalMessageId, + status: "running", + title: "Shell", + toolCallId: "failed-tool", + }, + sourceEventId: "failed-tool:running", + }), + runtimeEvent({ + kind: "tool.call.updated", + payload: { status: "failed", toolCallId: "failed-tool" }, + sourceEventId: "failed-tool:failed", + }), + runtimeEvent({ + kind: "message.completed", + payload: { messageId: finalMessageId, role: "agent" }, + sourceEventId: "failed-tool:message-completed", + }), + runtimeEvent({ + kind: "run.completed", + payload: { finalMessageId, stopReason: "end_turn" }, + sourceEventId: TERMINAL_SOURCE_EVENT_ID, + }), + ]); + + const transcript = await loadSessionViewerState(database, { + sessionId: SESSION_ID, + viewerId: PUBLIC_API_TEST_IDS.ownerAccount, + }); + const finalMessage = transcript.messages.find((message) => message.id === finalMessageId); + expect(finalMessage?.segments.filter((segment) => segment.kind !== "text")).toEqual([ + { + argsText: "", + kind: "tool_use", + path: null, + runId: RUN_ID, + tool: "Shell", + toolCallId: "failed-tool", + }, + { + kind: "tool_result", + output: "Tool failed.", + runId: RUN_ID, + tool: "Shell", + toolCallId: "failed-tool", + }, + ]); + }); + + test("keeps a completed run's identity-free tool result after a newer run becomes latest", async () => { + const database = await createPublicHttpContractDatabase(); + await insertRuntimeFixture(database); + const syncedSessionIds: string[] = []; + const bindings = createPublicHttpTestBindings(database, { + sessionNamespace: createSessionSyncNamespace(syncedSessionIds), + }) as ApiBindings; + const finalMessageId = createPlatformId(); + const toolCallId = "terminal-orphan-tool"; + const resultMessageId = createRuntimeToolResultMessageId({ runId: RUN_ID, toolCallId }); + const toolOutputOccurredAt = Date.parse("2026-08-30T04:00:00.000Z"); + + await pushFreshController(bindings, [ + ...messageEvents({ + messageId: finalMessageId, + sourcePrefix: "terminal-orphan:message", + text: "Finished.", + }), + runtimeEvent({ + kind: "tool.call.updated", + payload: { status: "running", title: "Shell", toolCallId }, + sourceEventId: "terminal-orphan:running", + }), + runtimeEvent({ + kind: "tool.call.updated", + occurredAt: toolOutputOccurredAt, + payload: { rawOutputDelta: "A", status: "running", toolCallId }, + sourceEventId: "terminal-orphan:output-a", + }), + runtimeEvent({ + kind: "tool.call.updated", + occurredAt: toolOutputOccurredAt + 1, + payload: { rawOutputDelta: "B", status: "completed", toolCallId }, + sourceEventId: "terminal-orphan:output-b", + }), + ]); + + const active = await loadSessionViewerState(database, { + sessionId: SESSION_ID, + viewerId: PUBLIC_API_TEST_IDS.ownerAccount, + }); + const activeResult = active.messages.find((message) => message.id === resultMessageId); + + const terminalDelivery: SessionDeliveryEvent[] = []; + await createController(bindings, (_sessionId, events) => + terminalDelivery.push(...events), + ).handlePushEvents( + { + driverInstanceId: DRIVER_ID, + events: [ + runtimeEvent({ + kind: "run.completed", + payload: { finalMessageId, stopReason: "end_turn" }, + sourceEventId: TERMINAL_SOURCE_EVENT_ID, + }), + ], + }, + activeContext, + ); + expect(terminalDelivery).toEqual([]); + expect(syncedSessionIds).toEqual([SESSION_ID]); + + const terminal = await loadSessionViewerState(database, { + sessionId: SESSION_ID, + viewerId: PUBLIC_API_TEST_IDS.ownerAccount, + }); + expect(terminal.messages.find((message) => message.id === resultMessageId)?.segments).toEqual( + activeResult?.segments, + ); + expect(activeResult?.segments).toEqual([ + { + kind: "tool_result", + output: "AB", + runId: RUN_ID, + tool: "Tool", + toolCallId, + }, + ]); + + const nextRunId = createPlatformId(); + await database + .prepare( + `INSERT INTO session_run ( + id, session_id, agent_id, created_by_account_id, deployment_version_id, + deployment_version_number, driver_instance_id, trigger, status, provider, + model, runtime_id, trace_id, started_at, created_at, updated_at + ) VALUES (?, ?, ?, ?, ?, 1, ?, 'user_prompt', 'running', 'openai', + 'gpt-5.4', 'openai-runtime', 'trace-next', 2, 2, 2)`, + ) + .bind( + nextRunId, + SESSION_ID, + PUBLIC_API_TEST_IDS.agent, + PUBLIC_API_TEST_IDS.ownerAccount, + PUBLIC_API_TEST_IDS.deployment, + DRIVER_ID, + ) + .run(); + await database + .prepare("UPDATE session SET last_run_id = ?, status = 'RUNNING' WHERE id = ?") + .bind(nextRunId, SESSION_ID) + .run(); + + const freshNextRun = await loadSessionViewerState(database, { + sessionId: SESSION_ID, + viewerId: PUBLIC_API_TEST_IDS.ownerAccount, + }); + const freshResults = freshNextRun.messages.filter((message) => message.id === resultMessageId); + const carrierIndex = freshNextRun.messages.findIndex( + (message) => message.id === finalMessageId, + ); + expect(freshResults).toHaveLength(1); + expect(freshResults[0]?.segments).toEqual(activeResult?.segments); + expect(freshNextRun.messages.indexOf(freshResults[0])).toBe(carrierIndex + 1); + expect(freshResults[0]?.createdAt).toBe(new Date(toolOutputOccurredAt).toISOString()); + + const repeatedFreshNextRun = await loadSessionViewerState(database, { + sessionId: SESSION_ID, + viewerId: PUBLIC_API_TEST_IDS.ownerAccount, + }); + expect(repeatedFreshNextRun.messages).toEqual(freshNextRun.messages); + const publicFreshNextRun = await loadSessionViewerState(database, { + sessionId: SESSION_ID, + viewerId: PUBLIC_API_TEST_IDS.outsiderAccount, + }); + expect(publicFreshNextRun.messages).toEqual(freshNextRun.messages); + }); + + test("canonicalizes nonfinal and result-first tools into one terminal carrier", async () => { + const database = await createPublicHttpContractDatabase(); + await insertRuntimeFixture(database); + const syncedSessionIds: string[] = []; + const bindings = createPublicHttpTestBindings(database, { + sessionNamespace: createSessionSyncNamespace(syncedSessionIds), + }) as ApiBindings; + const progressMessageId = createPlatformId(); + const finalMessageId = createPlatformId(); + const finalOccurredAt = Date.parse("2026-08-30T07:00:00.000Z"); + const carrierOccurredAt = finalOccurredAt + 2; + + await pushFreshController(bindings, [ + runtimeEvent({ + kind: "message.added", + occurredAt: finalOccurredAt - 2, + payload: { content: "Progress", messageId: progressMessageId, role: "agent" }, + sourceEventId: "carrier-routes:progress", + }), + runtimeEvent({ + kind: "message.completed", + occurredAt: finalOccurredAt - 1, + payload: { messageId: progressMessageId, role: "agent" }, + sourceEventId: "carrier-routes:progress-completed", + }), + runtimeEvent({ + kind: "message.added", + occurredAt: finalOccurredAt, + payload: { content: "Final", messageId: finalMessageId, role: "agent" }, + sourceEventId: "carrier-routes:final", + }), + runtimeEvent({ + kind: "message.completed", + occurredAt: finalOccurredAt + 1, + payload: { messageId: finalMessageId, role: "agent" }, + sourceEventId: "carrier-routes:final-completed", + }), + runtimeEvent({ + kind: "tool.call.updated", + occurredAt: carrierOccurredAt, + payload: { + parentMessageId: progressMessageId, + rawInput: '{"a":1}', + status: "running", + title: "Progress tool", + toolCallId: "progress-tool", + }, + sourceEventId: "carrier-routes:progress-use", + }), + runtimeEvent({ + kind: "tool.call.updated", + occurredAt: carrierOccurredAt + 1, + payload: { rawOutput: "progress output", status: "completed", toolCallId: "progress-tool" }, + sourceEventId: "carrier-routes:progress-result", + }), + runtimeEvent({ + kind: "tool.call.updated", + occurredAt: carrierOccurredAt + 2, + payload: { + parentMessageId: progressMessageId, + rawInput: '{"b":2}', + status: "running", + title: "Use only", + toolCallId: "use-only-tool", + }, + sourceEventId: "carrier-routes:use-only", + }), + runtimeEvent({ + kind: "tool.call.updated", + occurredAt: carrierOccurredAt + 3, + payload: { + rawOutput: "result first", + status: "completed", + toolCallId: "result-first-tool", + }, + sourceEventId: "carrier-routes:result-first", + }), + runtimeEvent({ + kind: "tool.call.updated", + occurredAt: carrierOccurredAt + 4, + payload: { + parentMessageId: finalMessageId, + rawInput: '{"c":3}', + status: "completed", + title: "Result first", + toolCallId: "result-first-tool", + }, + sourceEventId: "carrier-routes:result-later-parent", + }), + runtimeEvent({ + kind: "tool.call.updated", + occurredAt: carrierOccurredAt + 5, + payload: { + parentMessageId: finalMessageId, + rawInput: '{"d":4}', + status: "running", + title: "Final tool", + toolCallId: "final-tool", + }, + sourceEventId: "carrier-routes:final-use", + }), + runtimeEvent({ + kind: "tool.call.updated", + occurredAt: carrierOccurredAt + 6, + payload: { rawOutput: "final output", status: "completed", toolCallId: "final-tool" }, + sourceEventId: "carrier-routes:final-result", + }), + ]); + + const active = await loadSessionViewerState(database, { + sessionId: SESSION_ID, + viewerId: PUBLIC_API_TEST_IDS.ownerAccount, + }); + expect( + active.messages.find((message) => message.id === progressMessageId)?.segments, + ).toHaveLength(4); + expect(active.messages.find((message) => message.id === RUN_ID)?.segments).toHaveLength(2); + expect(active.messages.find((message) => message.id === finalMessageId)?.segments).toHaveLength( + 3, + ); + + const terminalDelivery: SessionDeliveryEvent[] = []; + await createController(bindings, (_sessionId, events) => + terminalDelivery.push(...events), + ).handlePushEvents( + { + driverInstanceId: DRIVER_ID, + events: [ + runtimeEvent({ + kind: "run.completed", + payload: { finalMessageId, stopReason: "end_turn" }, + sourceEventId: "carrier-routes:terminal", + }), + ], + }, + activeContext, + ); + expect(terminalDelivery).toEqual([]); + expect(syncedSessionIds).toEqual([SESSION_ID]); + + const fresh = await loadSessionViewerState(database, { + sessionId: SESSION_ID, + viewerId: PUBLIC_API_TEST_IDS.ownerAccount, + }); + expect(fresh.messages.find((message) => message.id === progressMessageId)).toBeUndefined(); + expect( + fresh.messages + .filter((message) => message.id === finalMessageId || message.id === RUN_ID) + .map(({ createdAt, id }) => ({ createdAt, id })), + ).toEqual([ + { createdAt: new Date(finalOccurredAt).toISOString(), id: finalMessageId }, + { createdAt: new Date(carrierOccurredAt).toISOString(), id: RUN_ID }, + ]); + expect(fresh.messages.find((message) => message.id === finalMessageId)?.segments).toEqual([ + { kind: "text", text: "Final" }, + { + argsText: '{"d":4}', + kind: "tool_use", + path: null, + runId: RUN_ID, + tool: "Final tool", + toolCallId: "final-tool", + }, + { + kind: "tool_result", + output: "final output", + runId: RUN_ID, + tool: "Final tool", + toolCallId: "final-tool", + }, + ]); + expect(fresh.messages.find((message) => message.id === RUN_ID)?.segments).toEqual([ + { + argsText: '{"a":1}', + kind: "tool_use", + path: null, + runId: RUN_ID, + tool: "Progress tool", + toolCallId: "progress-tool", + }, + { + kind: "tool_result", + output: "progress output", + runId: RUN_ID, + tool: "Progress tool", + toolCallId: "progress-tool", + }, + { + argsText: '{"b":2}', + kind: "tool_use", + path: null, + runId: RUN_ID, + tool: "Use only", + toolCallId: "use-only-tool", + }, + { + argsText: '{"c":3}', + kind: "tool_use", + path: null, + runId: RUN_ID, + tool: "Result first", + toolCallId: "result-first-tool", + }, + { + kind: "tool_result", + output: "result first", + runId: RUN_ID, + tool: "Result first", + toolCallId: "result-first-tool", + }, + ]); + const publicFresh = await loadSessionViewerState(database, { + sessionId: SESSION_ID, + viewerId: PUBLIC_API_TEST_IDS.outsiderAccount, + }); + expect(publicFresh.messages).toEqual(fresh.messages); + }); + + for (const toolFirst of [false, true]) { + test(`merges a ${toolFirst ? "tool-before-final" : "final-before-tool"} carrier when the final id is the run id`, async () => { + const database = await createPublicHttpContractDatabase(); + await insertRuntimeFixture(database); + const bindings = createPublicHttpTestBindings(database) as ApiBindings; + const finalMessageId = RUN_ID as unknown as SessionMessageId; + const firstOccurredAt = Date.parse("2026-08-30T08:00:00.000Z"); + const message = runtimeEvent({ + kind: "message.added", + occurredAt: firstOccurredAt + (toolFirst ? 2 : 0), + payload: { content: "Same id final", messageId: finalMessageId, role: "agent" }, + sourceEventId: `same-id:${toolFirst}:message`, + }); + const completed = runtimeEvent({ + kind: "message.completed", + occurredAt: firstOccurredAt + (toolFirst ? 3 : 1), + payload: { messageId: finalMessageId, role: "agent" }, + sourceEventId: `same-id:${toolFirst}:message-completed`, + }); + const tool = runtimeEvent({ + kind: "tool.call.updated", + occurredAt: firstOccurredAt + (toolFirst ? 0 : 2), + payload: { + rawOutput: "same id output", + status: "completed", + toolCallId: "same-id-tool", + }, + sourceEventId: `same-id:${toolFirst}:tool`, + }); + + await pushFreshController(bindings, [ + ...(toolFirst ? [tool, message, completed] : [message, completed, tool]), + runtimeEvent({ + kind: "run.completed", + payload: { finalMessageId, stopReason: "end_turn" }, + sourceEventId: `same-id:${toolFirst}:terminal`, + }), + ]); + + const rows = await database + .prepare( + `SELECT created_at, id + FROM session_message + WHERE session_id = ? AND session_run_id = ? AND role = 'assistant' + ORDER BY seq`, + ) + .bind(SESSION_ID, RUN_ID) + .all<{ created_at: number; id: string }>(); + expect(rows.results).toEqual([{ created_at: firstOccurredAt, id: RUN_ID }]); + const fresh = await loadSessionViewerState(database, { + sessionId: SESSION_ID, + viewerId: PUBLIC_API_TEST_IDS.outsiderAccount, + }); + const merged = fresh.messages.filter((candidate) => candidate.id === RUN_ID); + expect(merged).toHaveLength(1); + expect(merged[0]?.createdAt).toBe(new Date(firstOccurredAt).toISOString()); + expect(merged[0]?.segments).toEqual( + toolFirst + ? [ + { + kind: "tool_result", + output: "same id output", + runId: RUN_ID, + tool: "Tool", + toolCallId: "same-id-tool", + }, + { kind: "text", text: "Same id final" }, + ] + : [ + { kind: "text", text: "Same id final" }, + { + kind: "tool_result", + output: "same id output", + runId: RUN_ID, + tool: "Tool", + toolCallId: "same-id-tool", + }, + ], + ); + }); + } + + test("fails closed when a cross-boot replay conflicts with the persisted final snapshot", async () => { + const database = await createPublicHttpContractDatabase(); + await insertRuntimeFixture(database); + const bindings = createPublicHttpTestBindings(database) as ApiBindings; + const finalMessageId = createPlatformId(); + const terminalBatch = [ + ...messageEvents({ + messageId: finalMessageId, + sourcePrefix: "conflict:original-final", + text: FINAL_TEXT, + }), + runtimeEvent({ + kind: "run.completed", + payload: { + finalMessageId, + stopReason: "end_turn", + }, + sourceEventId: TERMINAL_SOURCE_EVENT_ID, + }), + ]; + await expect(pushFreshController(bindings, terminalBatch)).resolves.toHaveLength( + terminalBatch.length, + ); + + const replayedFinalMessageId = createPlatformId(); + const conflictingText = `${FINAL_TEXT}\nCONFLICTING-REPLAY`; + const conflictingReplay = [ + ...messageEvents({ + messageId: replayedFinalMessageId, + sourcePrefix: "conflict:reconnected-final", + text: conflictingText, + }), + runtimeEvent({ + kind: "run.completed", + payload: { + finalMessageId: replayedFinalMessageId, + stopReason: "end_turn", + }, + sourceEventId: TERMINAL_SOURCE_EVENT_ID, + }), + ]; + + await expect(pushFreshController(bindings, conflictingReplay)).rejects.toBeInstanceOf(Error); + + const messages = await database + .prepare("SELECT content_text, id FROM session_message WHERE session_run_id = ? ORDER BY seq") + .bind(RUN_ID) + .all<{ content_text: string; id: string }>(); + const terminalRows = await database + .prepare("SELECT source_event_id FROM session_event WHERE source_event_id = ?") + .bind(TERMINAL_SOURCE_EVENT_ID) + .all<{ source_event_id: string }>(); + + expect(messages.results).toEqual([{ content_text: "", id: finalMessageId }]); + expect(terminalRows.results).toEqual([{ source_event_id: TERMINAL_SOURCE_EVENT_ID }]); + await expect( + readPublicThreadRunFinalOutput({ database, runId: RUN_ID, sessionId: SESSION_ID }), + ).resolves.toEqual({ text: FINAL_TEXT }); + }); + + test("does not guess a progress message when the terminal RPC has no final identity", async () => { + const database = await createPublicHttpContractDatabase(); + await insertRuntimeFixture(database); + const bindings = createPublicHttpTestBindings(database) as ApiBindings; + const progressMessageId = createPlatformId(); + const progressEvents = messageEvents({ + messageId: progressMessageId, + sourcePrefix: "fallback:progress", + text: PROGRESS_TEXTS[0], + }); + + await pushFreshController(bindings, progressEvents); + await expect( + recordDriverInstanceCompletion(bindings, { + driverInstanceId: DRIVER_ID, + sessionRunId: RUN_ID, + }), + ).rejects.toThrow("Completed Session Run is missing its canonical terminal event."); + + await expect( + readPublicThreadRunFinalOutput({ + database, + runId: RUN_ID, + sessionId: SESSION_ID, + }), + ).resolves.toBeNull(); + const messages = await database + .prepare("SELECT id FROM session_message WHERE session_run_id = ?") + .bind(RUN_ID) + .all<{ id: string }>(); + + expect(messages.results).toEqual([]); + }); + + test("rejects a failure RPC after another Driver connection takes ownership", async () => { + const database = await createPublicHttpContractDatabase(); + await insertRuntimeFixture(database); + const bindings = createPublicHttpTestBindings(database) as ApiBindings; + + await database + .prepare("UPDATE driver_instance SET connection_id = ?, generation = 1 WHERE id = ?") + .bind("replacement-connection", DRIVER_ID) + .run(); + + await expect( + recordDriverInstanceFailure(bindings, { + driverConnectionId: "canary-connection", + driverGeneration: 0, + driverInstanceId: DRIVER_ID, + sessionRunId: RUN_ID, + error: { + code: "driver.disconnected", + details: {}, + message: "The superseded Driver disconnected.", + retryable: true, + }, + }), + ).rejects.toThrow("lost a concurrent running race"); + + const run = await database + .prepare("SELECT status FROM session_run WHERE id = ?") + .bind(RUN_ID) + .first<{ status: string }>(); + const terminalEvents = await database + .prepare( + "SELECT id FROM session_event WHERE run_id = ? AND event_type IN ('run.cancelled', 'run.completed', 'run.failed')", + ) + .bind(RUN_ID) + .all(); + + expect(run?.status).toBe("running"); + expect(terminalEvents.results).toEqual([]); + }); + + test("a late run-one terminal RPC cannot touch the active run-two lease", async () => { + const database = await createPublicHttpContractDatabase(); + await insertRuntimeFixture(database); + const bindings = createPublicHttpTestBindings(database) as ApiBindings; + const nextRunId = createPlatformId(); + + await recordCanonicalSessionRunTerminal(bindings, { + assistantMessage: null, + error: null, + runId: RUN_ID, + sessionId: SESSION_ID, + source: "api", + status: "completed", + }); + await database + .prepare( + `INSERT INTO session_run ( + id, session_id, agent_id, created_by_account_id, deployment_version_id, + deployment_version_number, driver_instance_id, trigger, status, provider, + model, runtime_id, trace_id, started_at, created_at, updated_at + ) VALUES (?, ?, ?, ?, ?, 1, ?, 'user_prompt', 'running', 'openai', + 'gpt-5.4', 'openai-runtime', 'trace-next', 2, 2, 2)`, + ) + .bind( + nextRunId, + SESSION_ID, + PUBLIC_API_TEST_IDS.agent, + PUBLIC_API_TEST_IDS.ownerAccount, + PUBLIC_API_TEST_IDS.deployment, + DRIVER_ID, + ) + .run(); + await database + .prepare("UPDATE session SET last_run_id = ?, status = 'RUNNING' WHERE id = ?") + .bind(nextRunId, SESSION_ID) + .run(); + + await expect( + recordDriverInstanceCompletion(bindings, { + driverConnectionId: "canary-connection", + driverGeneration: 0, + driverInstanceId: DRIVER_ID, + sessionRunId: RUN_ID, + }), + ).rejects.toThrow("lost a concurrent completed race"); + await expect( + database.prepare("SELECT status FROM session_run WHERE id = ?").bind(nextRunId).first(), + ).resolves.toEqual({ status: "running" }); + await expect( + database + .prepare("SELECT last_run_id, status, status_operation_id FROM session WHERE id = ?") + .bind(SESSION_ID) + .first(), + ).resolves.toEqual({ + last_run_id: nextRunId, + status: "RUNNING", + status_operation_id: null, + }); + await expect( + database + .prepare("SELECT status, status_operation_id FROM driver_instance WHERE id = ?") + .bind(DRIVER_ID) + .first(), + ).resolves.toEqual({ status: "ready", status_operation_id: null }); + }); + + test("returns no final output for a canonical completion without a final message", async () => { + const database = await createPublicHttpContractDatabase(); + await insertRuntimeFixture(database); + const bindings = createPublicHttpTestBindings(database) as ApiBindings; + + await pushFreshController(bindings, [ + runtimeEvent({ + kind: "run.completed", + payload: { stopReason: "end_turn" }, + sourceEventId: TERMINAL_SOURCE_EVENT_ID, + }), + ]); + + await expect( + readPublicThreadRunFinalOutput({ database, runId: RUN_ID, sessionId: SESSION_ID }), + ).resolves.toBeNull(); + }); + + for (const terminalCase of [ + { + kind: "run.completed" as const, + payload: { stopReason: "end_turn" }, + status: "completed", + }, + { + kind: "run.failed" as const, + payload: { + error: { + code: "driver.provider_failed", + details: {}, + message: "Provider failed after tool output.", + retryable: false, + }, + recoverable: false, + }, + status: "failed", + }, + { + kind: "run.cancelled" as const, + payload: { reason: "session.stop" }, + status: "cancelled", + }, + ]) { + test(`persists a ${terminalCase.status} run's parentless tool deltas without a final message`, async () => { + const database = await createPublicHttpContractDatabase(); + await insertRuntimeFixture(database); + const bindings = createPublicHttpTestBindings(database) as ApiBindings; + const toolOccurredAt = Date.parse("2026-08-30T06:00:00.000Z"); + + await pushFreshController(bindings, [ + runtimeEvent({ + kind: "tool.call.updated", + occurredAt: toolOccurredAt, + payload: { + rawOutputDelta: "A", + status: "running", + toolCallId: "terminal-no-final-tool", + }, + sourceEventId: `${terminalCase.status}:tool-output-a`, + }), + runtimeEvent({ + kind: "tool.call.updated", + occurredAt: toolOccurredAt + 1, + payload: { + rawOutputDelta: "B", + status: "completed", + toolCallId: "terminal-no-final-tool", + }, + sourceEventId: `${terminalCase.status}:tool-output-b`, + }), + runtimeEvent({ + kind: terminalCase.kind, + payload: terminalCase.payload, + sourceEventId: `${terminalCase.status}:terminal`, + }), + ]); + + const rows = await database + .prepare( + `SELECT created_at, id, projection_format + FROM session_message + WHERE session_id = ? AND session_run_id = ? AND role = 'assistant' + ORDER BY seq`, + ) + .bind(SESSION_ID, RUN_ID) + .all<{ created_at: number; id: string; projection_format: string }>(); + expect(rows.results).toEqual([ + { + created_at: toolOccurredAt, + id: RUN_ID, + projection_format: "event_stream_v3", + }, + ]); + const fresh = await loadSessionViewerState(database, { + sessionId: SESSION_ID, + viewerId: PUBLIC_API_TEST_IDS.outsiderAccount, + }); + expect(fresh.messages.find((message) => message.id === RUN_ID)).toEqual( + expect.objectContaining({ + content: "", + createdAt: new Date(toolOccurredAt).toISOString(), + segments: [ + { + kind: "tool_result", + output: "AB", + runId: RUN_ID, + tool: "Tool", + toolCallId: "terminal-no-final-tool", + }, + ], + }), + ); + }); + } + + test("adopts a canonical completion RPC replay without creating another terminal event", async () => { + const database = await createPublicHttpContractDatabase(); + await insertRuntimeFixture(database); + const bindings = createPublicHttpTestBindings(database) as ApiBindings; + const finalMessageId = createPlatformId(); + + await pushFreshController(bindings, [ + ...messageEvents({ + messageId: finalMessageId, + sourcePrefix: "completion-rpc-replay:final", + text: FINAL_TEXT, + }), + runtimeEvent({ + kind: "run.completed", + payload: { finalMessageId, stopReason: "end_turn" }, + sourceEventId: "provider-completion-id", + }), + ]); + + const operationId = createPlatformId(); + await database + .prepare( + `UPDATE session + SET status = 'RESCHEDULING', status_operation_id = ?, status_seq = status_seq + 1 + WHERE id = ?`, + ) + .bind(operationId, SESSION_ID) + .run(); + const sessionBeforeReplay = await database + .prepare( + `SELECT last_run_id, message_seq_cursor, runtime_event_seq_cursor, + status, status_operation_id, status_seq + FROM session WHERE id = ?`, + ) + .bind(SESSION_ID) + .first(); + + for (let attempt = 0; attempt < 2; attempt += 1) { + await recordDriverInstanceCompletion(bindings, { + driverInstanceId: DRIVER_ID, + sessionRunId: RUN_ID, + }); + } + const sessionAfterReplay = await database + .prepare( + `SELECT last_run_id, message_seq_cursor, runtime_event_seq_cursor, + status, status_operation_id, status_seq + FROM session WHERE id = ?`, + ) + .bind(SESSION_ID) + .first(); + + const terminalEvents = await database + .prepare( + "SELECT semantic_hash, source_event_id FROM session_event WHERE run_id = ? AND event_type = 'run.completed'", + ) + .bind(RUN_ID) + .all<{ semantic_hash: string; source_event_id: string }>(); + + expect(terminalEvents.results).toEqual([ + { + semantic_hash: expect.stringMatching(/^[0-9a-f]{64}$/), + source_event_id: TERMINAL_SOURCE_EVENT_ID, + }, + ]); + expect(sessionAfterReplay).toEqual(sessionBeforeReplay); + await expect( + readPublicThreadRunFinalOutput({ database, runId: RUN_ID, sessionId: SESSION_ID }), + ).resolves.toEqual({ text: FINAL_TEXT }); + }); + + for (const rpcKind of ["completion", "failure"] as const) { + test(`rejects a stale ${rpcKind} RPC when a same-generation Driver connection replaced its terminal winner`, async () => { + const database = await createPublicHttpContractDatabase(); + await insertRuntimeFixture(database); + const bindings = createPublicHttpTestBindings(database) as ApiBindings; + const terminalError = { + code: "driver.api_winner", + details: {}, + message: "The API terminal winner is durable.", + retryable: false, + } as const; + + if (rpcKind === "completion") { + const finalMessageId = createPlatformId(); + await pushFreshController( + bindings, + messageEvents({ + messageId: finalMessageId, + sourcePrefix: "same-generation-winner:final", + text: FINAL_TEXT, + }), + ); + await recordCanonicalSessionRunTerminal(bindings, { + assistantMessage: prepareAssistantMessageProjection({ + createdByAccountId: PUBLIC_API_TEST_IDS.ownerAccount, + messageId: finalMessageId, + sessionId: SESSION_ID, + sessionRunId: RUN_ID, + }), + error: null, + runId: RUN_ID, + sessionId: SESSION_ID, + source: "api", + status: "completed", + }); + } else { + await recordCanonicalSessionRunFailure(bindings, { + error: terminalError, + runId: RUN_ID, + sessionId: SESSION_ID, + source: "api", + }); + } + await database + .prepare("UPDATE driver_instance SET connection_id = ? WHERE id = ?") + .bind("replacement-connection", DRIVER_ID) + .run(); + const driverBefore = await database + .prepare( + "SELECT connection_id, generation, status, status_operation_id FROM driver_instance WHERE id = ?", + ) + .bind(DRIVER_ID) + .first(); + const sessionBefore = await database + .prepare( + `SELECT message_seq_cursor, runtime_event_seq_cursor, status, + status_operation_id, status_seq, updated_at + FROM session WHERE id = ?`, + ) + .bind(SESSION_ID) + .first(); + + await expect( + rpcKind === "completion" + ? recordDriverInstanceCompletion(bindings, { + driverConnectionId: "canary-connection", + driverGeneration: 0, + driverInstanceId: DRIVER_ID, + sessionRunId: RUN_ID, + }) + : recordDriverInstanceFailure(bindings, { + driverConnectionId: "canary-connection", + driverGeneration: 0, + driverInstanceId: DRIVER_ID, + sessionRunId: RUN_ID, + error: { + code: "driver.stale_rpc", + details: {}, + message: "The old connection reported terminal.", + retryable: true, + }, + }), + ).rejects.toThrow("lost a concurrent"); + + await expect( + database + .prepare( + "SELECT connection_id, generation, status, status_operation_id FROM driver_instance WHERE id = ?", + ) + .bind(DRIVER_ID) + .first(), + ).resolves.toEqual(driverBefore); + await expect( + database + .prepare( + `SELECT message_seq_cursor, runtime_event_seq_cursor, status, + status_operation_id, status_seq, updated_at + FROM session WHERE id = ?`, + ) + .bind(SESSION_ID) + .first(), + ).resolves.toEqual(sessionBefore); + await expect( + database + .prepare( + `SELECT COUNT(*) AS count + FROM session_event + WHERE run_id = ? AND event_type IN ('run.cancelled', 'run.completed', 'run.failed')`, + ) + .bind(RUN_ID) + .first(), + ).resolves.toEqual({ count: 1 }); + }); + } + + test("atomically claims an exact terminal Driver without rewriting a newer Session lifecycle", async () => { + const database = await createPublicHttpContractDatabase(); + await insertRuntimeFixture(database); + const bindings = createPublicHttpTestBindings(database) as ApiBindings; + const finalMessageId = createPlatformId(); + await pushFreshController( + bindings, + messageEvents({ + messageId: finalMessageId, + sourcePrefix: "atomic-adoption-claim:final", + text: FINAL_TEXT, + }), + ); + await recordCanonicalSessionRunTerminal(bindings, { + assistantMessage: prepareAssistantMessageProjection({ + createdByAccountId: PUBLIC_API_TEST_IDS.ownerAccount, + messageId: finalMessageId, + sessionId: SESSION_ID, + sessionRunId: RUN_ID, + }), + error: null, + runId: RUN_ID, + sessionId: SESSION_ID, + source: "api", + status: "completed", + }); + const operationId = createPlatformId(); + await database + .prepare( + `UPDATE session + SET status = 'RESCHEDULING', status_operation_id = ?, status_seq = status_seq + 1 + WHERE id = ?`, + ) + .bind(operationId, SESSION_ID) + .run(); + const sessionBefore = await database + .prepare( + `SELECT last_run_id, message_seq_cursor, runtime_event_seq_cursor, + status, status_operation_id, status_seq, updated_at + FROM session WHERE id = ?`, + ) + .bind(SESSION_ID) + .first(); + + await recordDriverInstanceCompletion(bindings, { + driverConnectionId: "canary-connection", + driverGeneration: 0, + driverInstanceId: DRIVER_ID, + sessionRunId: RUN_ID, + }); + + await expect( + database + .prepare( + `SELECT last_run_id, message_seq_cursor, runtime_event_seq_cursor, + status, status_operation_id, status_seq, updated_at + FROM session WHERE id = ?`, + ) + .bind(SESSION_ID) + .first(), + ).resolves.toEqual(sessionBefore); + }); + + test("atomically claims the persisted receipt while repairing an exact failure replay", async () => { + const database = await createPublicHttpContractDatabase(); + await insertRuntimeFixture(database); + const bindings = createPublicHttpTestBindings(database) as ApiBindings; + const durableError = { + code: "driver.api_failure", + details: {}, + message: "The API failure receipt is durable.", + retryable: false, + } as const; + await recordCanonicalSessionRunFailure(bindings, { + error: durableError, + runId: RUN_ID, + sessionId: SESSION_ID, + source: "api", + }); + await database + .prepare("UPDATE session SET status = 'RUNNING', updated_at = 1 WHERE id = ?") + .bind(SESSION_ID) + .run(); + + await recordDriverInstanceFailure(bindings, { + driverConnectionId: "canary-connection", + driverGeneration: 0, + driverInstanceId: DRIVER_ID, + sessionRunId: RUN_ID, + error: { + code: "driver.late_failure", + details: {}, + message: "The exact Driver replay arrived later.", + retryable: true, + }, + }); + + await expect( + database.prepare("SELECT status FROM session WHERE id = ?").bind(SESSION_ID).first(), + ).resolves.toEqual({ status: "IDLE" }); + await expect( + database + .prepare("SELECT error_code, status FROM session_run WHERE id = ?") + .bind(RUN_ID) + .first(), + ).resolves.toEqual({ error_code: durableError.code, status: "failed" }); + await expect( + database + .prepare( + "SELECT COUNT(*) AS count FROM session_event WHERE run_id = ? AND event_type = 'run.failed'", + ) + .bind(RUN_ID) + .first(), + ).resolves.toEqual({ count: 1 }); + }); + + test("records a fresh Driver failure while the exact connection is still connecting", async () => { + const database = await createPublicHttpContractDatabase(); + await insertRuntimeFixture(database); + const bindings = createPublicHttpTestBindings(database) as ApiBindings; + await database + .prepare("UPDATE driver_instance SET status = 'connecting' WHERE id = ?") + .bind(DRIVER_ID) + .run(); + + await recordDriverInstanceFailure(bindings, { + driverConnectionId: "canary-connection", + driverGeneration: 0, + driverInstanceId: DRIVER_ID, + sessionRunId: RUN_ID, + error: { + code: "driver.connect_failed", + details: {}, + message: "The Driver failed before ready.", + retryable: true, + }, + }); + + await expect( + database + .prepare("SELECT error_code, status FROM session_run WHERE id = ?") + .bind(RUN_ID) + .first(), + ).resolves.toEqual({ error_code: "driver.connect_failed", status: "failed" }); + await expect( + database + .prepare( + "SELECT COUNT(*) AS count FROM session_event WHERE run_id = ? AND event_type = 'run.failed'", + ) + .bind(RUN_ID) + .first(), + ).resolves.toEqual({ count: 1 }); + }); + + test("repairs a missing final reference behind the exact terminal adoption barrier", async () => { + const database = await createPublicHttpContractDatabase(); + await insertRuntimeFixture(database); + const finalMessageId = await insertRepairableCompletedAuthority( + database, + "adoption-barrier:exact", + ); + const bindings = createPublicHttpTestBindings(database) as ApiBindings; + + await recordDriverInstanceCompletion(bindings, { + driverInstanceId: DRIVER_ID, + sessionRunId: RUN_ID, + }); + + await expect( + database + .prepare( + "SELECT id, projection_format, seq, session_id FROM session_message WHERE session_run_id = ?", + ) + .bind(RUN_ID) + .first(), + ).resolves.toEqual({ + id: finalMessageId, + projection_format: "event_stream_v3", + seq: 1, + session_id: SESSION_ID, + }); + }); + + for (const corruptedStream of ["null", "progress"] as const) { + test(`rejects a pre-corrupted ${corruptedStream} terminal stream before missing-final adoption`, async () => { + const database = await createPublicHttpContractDatabase(); + await insertRuntimeFixture(database); + const bindings = createPublicHttpTestBindings(database) as ApiBindings; + const progressMessageId = createPlatformId(); + await pushFreshController( + bindings, + messageEvents({ + messageId: progressMessageId, + sourcePrefix: `adoption-corrupt-${corruptedStream}:progress`, + text: PROGRESS_TEXTS[0], + }), + ); + await insertRepairableCompletedAuthority( + database, + `adoption-corrupt-${corruptedStream}:final`, + ); + await database + .prepare( + "UPDATE session_event SET stream_id = ? WHERE run_id = ? AND event_type = 'run.completed'", + ) + .bind(corruptedStream === "null" ? null : progressMessageId, RUN_ID) + .run(); + const messagesBefore = await database + .prepare( + "SELECT content_text, id, projection_format FROM session_message WHERE session_run_id = ? AND role = 'assistant' ORDER BY seq", + ) + .bind(RUN_ID) + .all<{ content_text: string; id: string; projection_format: string }>(); + + let rejected = false; + try { + await recordDriverInstanceCompletion(bindings, { + driverInstanceId: DRIVER_ID, + sessionRunId: RUN_ID, + }); + } catch { + rejected = true; + } + const messages = await database + .prepare( + "SELECT content_text, id, projection_format FROM session_message WHERE session_run_id = ? AND role = 'assistant' ORDER BY seq", + ) + .bind(RUN_ID) + .all<{ content_text: string; id: string; projection_format: string }>(); + expect({ messages: messages.results, rejected }).toEqual({ + messages: messagesBefore.results, + rejected: true, + }); + }); + } + + test("preserves the durable TERMINATED lifecycle while adopting a partial RUNNING Session", async () => { + const database = await createPublicHttpContractDatabase(); + await insertRuntimeFixture(database); + const bindings = createPublicHttpTestBindings(database) as ApiBindings; + const finalMessageId = createPlatformId(); + await pushFreshController( + bindings, + messageEvents({ + messageId: finalMessageId, + sourcePrefix: "adoption-terminated:final", + text: FINAL_TEXT, + }), + ); + await recordCanonicalSessionRunTerminal(bindings, { + assistantMessage: prepareAssistantMessageProjection({ + createdByAccountId: PUBLIC_API_TEST_IDS.ownerAccount, + messageId: finalMessageId, + sessionId: SESSION_ID, + sessionRunId: RUN_ID, + }), + error: null, + lifecycle: "TERMINATED", + runId: RUN_ID, + sessionId: SESSION_ID, + source: "api", + status: "completed", + }); + await database.prepare("DELETE FROM session_message WHERE id = ?").bind(finalMessageId).run(); + await database + .prepare("UPDATE session SET message_seq_cursor = 0, status = 'RUNNING' WHERE id = ?") + .bind(SESSION_ID) + .run(); + + await recordDriverInstanceCompletion(bindings, { + driverInstanceId: DRIVER_ID, + sessionRunId: RUN_ID, + }); + + await expect( + database.prepare("SELECT status FROM session WHERE id = ?").bind(SESSION_ID).first(), + ).resolves.toEqual({ status: "TERMINATED" }); + }); + + for (const receiptMutation of [ + { + field: "source", + sql: "UPDATE session_event SET source = 'system' WHERE run_id = ? AND event_type = 'run.completed'", + }, + { + field: "stream", + sql: "UPDATE session_event SET stream_id = '01J000000000000000000000ZZ' WHERE run_id = ? AND event_type = 'run.completed'", + }, + { + field: "sequence", + sql: "UPDATE session_event SET seq = seq + 1000 WHERE run_id = ? AND event_type = 'run.completed'", + }, + { + field: "visibility", + sql: "UPDATE session_event SET visibility = 'owner_debug' WHERE run_id = ? AND event_type = 'run.completed'", + }, + { + field: "artifact", + sql: `UPDATE session_event + SET artifact_manifest_sha256 = '${"0".repeat(64)}' + WHERE run_id = ? AND event_type = 'run.completed'`, + }, + ] as const) { + test(`rolls back terminal adoption when its ${receiptMutation.field} receipt field changes before the batch`, async () => { + const database = await createPublicHttpContractDatabase(); + await insertRuntimeFixture(database); + await insertRepairableCompletedAuthority( + database, + `adoption-barrier:${receiptMutation.field}`, + ); + await expect( + database + .prepare( + "SELECT artifact_manifest_sha256 FROM session_event WHERE run_id = ? AND event_type = 'run.completed'", + ) + .bind(RUN_ID) + .first<{ artifact_manifest_sha256: string | null }>(), + ).resolves.toEqual({ artifact_manifest_sha256: expect.any(String) }); + const sessionBefore = await database + .prepare( + `SELECT last_message_at, message_seq_cursor, runtime_event_seq_cursor, + status, status_operation_id, status_seq, updated_at + FROM session WHERE id = ?`, + ) + .bind(SESSION_ID) + .first(); + const injected = mutateBeforeFirstDatabaseBatch(database, async (target) => { + await target.prepare(receiptMutation.sql).bind(RUN_ID).run(); + }); + const bindings = createPublicHttpTestBindings(injected.database) as ApiBindings; + + await expect( + recordDriverInstanceCompletion(bindings, { + driverInstanceId: DRIVER_ID, + sessionRunId: RUN_ID, + }), + ).rejects.toThrow(); + + expect(injected.wasInjected()).toBe(true); + await expect( + database + .prepare( + `SELECT last_message_at, message_seq_cursor, runtime_event_seq_cursor, + status, status_operation_id, status_seq, updated_at + FROM session WHERE id = ?`, + ) + .bind(SESSION_ID) + .first(), + ).resolves.toEqual(sessionBefore); + await expect( + database + .prepare( + "SELECT COUNT(*) AS count FROM session_message WHERE session_run_id = ? AND role = 'assistant'", + ) + .bind(RUN_ID) + .first(), + ).resolves.toEqual({ count: 0 }); + await expect( + database + .prepare( + `SELECT COUNT(*) AS count + FROM session_event + WHERE run_id = ? AND event_type IN ('run.cancelled', 'run.completed', 'run.failed')`, + ) + .bind(RUN_ID) + .first(), + ).resolves.toEqual({ count: 1 }); + }); + } + + for (const corruptedAuthority of ["final", "carrier"] as const) { + test(`rejects a canonical replay whose ${corruptedAuthority} row belongs to another session`, async () => { + const database = await createPublicHttpContractDatabase(); + await insertRuntimeFixture(database); + await insertNonOwnerSession(database); + const bindings = createPublicHttpTestBindings(database) as ApiBindings; + const finalMessageId = createPlatformId(); + const terminal = runtimeEvent({ + kind: "run.completed", + payload: + corruptedAuthority === "final" + ? { finalMessageId, stopReason: "end_turn" } + : { stopReason: "end_turn" }, + sourceEventId: `cross-session-authority:${corruptedAuthority}:terminal`, + }); + + await pushFreshController(bindings, [ + ...(corruptedAuthority === "final" + ? messageEvents({ + messageId: finalMessageId, + sourcePrefix: "cross-session-authority:final", + text: "Final authority", + }) + : [ + runtimeEvent({ + kind: "tool.call.updated", + payload: { + rawOutput: "carrier authority", + status: "completed", + toolCallId: "cross-session-carrier", + }, + sourceEventId: "cross-session-authority:carrier", + }), + ]), + terminal, + ]); + + await database + .prepare("UPDATE session_message SET session_id = ? WHERE id = ?") + .bind( + PUBLIC_API_TEST_IDS.nonOwnerSession, + corruptedAuthority === "final" ? finalMessageId : RUN_ID, + ) + .run(); + await database + .prepare( + `UPDATE session + SET status = 'RESCHEDULING', status_operation_id = ?, status_seq = status_seq + 1 + WHERE id = ?`, + ) + .bind(createPlatformId(), SESSION_ID) + .run(); + + await expect( + recordDriverInstanceCompletion(bindings, { + driverInstanceId: DRIVER_ID, + sessionRunId: RUN_ID, + }), + ).rejects.toThrow("Canonical assistant messages"); + }); + } + + test("rejects a legacy materialized final authority from another session on RPC replay", async () => { + const database = await createPublicHttpContractDatabase(); + await insertRuntimeFixture(database); + await insertNonOwnerSession(database); + const bindings = createPublicHttpTestBindings(database) as ApiBindings; + const finalMessageId = createPlatformId(); + await pushFreshController(bindings, [ + ...messageEvents({ + messageId: finalMessageId, + sourcePrefix: "legacy-cross-session:final", + text: "Legacy final authority", + }), + runtimeEvent({ + kind: "run.completed", + payload: { finalMessageId, stopReason: "end_turn" }, + sourceEventId: "legacy-cross-session:terminal", + }), + ]); + await database + .prepare( + `UPDATE session_message + SET content_text = 'Legacy final authority', projection_format = 'materialized', + session_id = ? + WHERE id = ?`, + ) + .bind(PUBLIC_API_TEST_IDS.nonOwnerSession, finalMessageId) + .run(); + await database + .prepare( + `UPDATE session_event + SET artifact_attempt_id = NULL, artifact_manifest_json = NULL, + artifact_manifest_sha256 = NULL, semantic_hash = NULL, + terminal_event_json = NULL + WHERE run_id = ? AND event_type = 'run.completed'`, + ) + .bind(RUN_ID) + .run(); + + await expect( + recordDriverInstanceCompletion(bindings, { + driverInstanceId: DRIVER_ID, + sessionRunId: RUN_ID, + }), + ).rejects.toThrow("Legacy terminal assistant messages conflict"); + }); + + test("does not repair a missing terminal event across a newer Session operation", async () => { + const database = await createPublicHttpContractDatabase(); + await insertRuntimeFixture(database); + const bindings = createPublicHttpTestBindings(database) as ApiBindings; + const finalMessageId = createPlatformId(); + const terminal = runtimeEvent({ + kind: "run.completed", + payload: { finalMessageId, stopReason: "end_turn" }, + sourceEventId: TERMINAL_SOURCE_EVENT_ID, + }); + + await pushFreshController(bindings, [ + ...messageEvents({ + messageId: finalMessageId, + sourcePrefix: "partial-repair:final", + text: FINAL_TEXT, + }), + terminal, + ]); + await database + .prepare("DELETE FROM session_event WHERE source_event_id = ?") + .bind(TERMINAL_SOURCE_EVENT_ID) + .run(); + const completedRun = await getSessionRunSummary(database, RUN_ID); + if (completedRun === null) { + throw new Error("Missing completed Session Run fixture."); + } + const runEvent = createSessionRunUpdatedEvent( + completedRun, + SESSION_ID, + "IDLE", + TERMINAL_SOURCE_EVENT_ID, + ); + const repairEvent = { + ...runEvent, + payload: { ...runEvent.payload, finalMessageId, stopReason: "end_turn" }, + }; + const run = await database + .prepare("SELECT updated_at FROM session_run WHERE id = ?") + .bind(RUN_ID) + .first<{ updated_at: number }>(); + if (run === null) { + throw new Error("Missing completed Session Run fixture."); + } + const operationId = createPlatformId(); + await database + .prepare( + `UPDATE session + SET status = 'RESCHEDULING', status_operation_id = ?, + status_seq = status_seq + 1, updated_at = ? + WHERE id = ?`, + ) + .bind(operationId, run.updated_at + 1, SESSION_ID) + .run(); + const before = await database + .prepare( + `SELECT last_run_id, message_seq_cursor, runtime_event_seq_cursor, + status, status_operation_id, status_seq, updated_at + FROM session WHERE id = ?`, + ) + .bind(SESSION_ID) + .first(); + + await expect( + commitTerminalRunProjection(database, { + assistantMessage: prepareAssistantMessageProjection({ + createdByAccountId: PUBLIC_API_TEST_IDS.ownerAccount, + messageId: finalMessageId, + sessionId: SESSION_ID, + sessionRunId: RUN_ID, + }), + error: null, + runId: RUN_ID, + sessionId: SESSION_ID, + source: "driver", + targetStatus: "completed", + terminalEvent: { + event: repairEvent, + occurredAt: Date.parse(repairEvent.occurredAt), + sourceEventId: TERMINAL_SOURCE_EVENT_ID, + }, + }), + ).rejects.toThrow("not safely repairable"); + + const after = await database + .prepare( + `SELECT last_run_id, message_seq_cursor, runtime_event_seq_cursor, + status, status_operation_id, status_seq, updated_at + FROM session WHERE id = ?`, + ) + .bind(SESSION_ID) + .first(); + const terminalRows = await database + .prepare("SELECT id FROM session_event WHERE source_event_id = ?") + .bind(TERMINAL_SOURCE_EVENT_ID) + .all(); + expect(after).toEqual(before); + expect(terminalRows.results).toEqual([]); + }); + + test("repairs a missing terminal event from an unchanged RUNNING Session", async () => { + const database = await createPublicHttpContractDatabase(); + await insertRuntimeFixture(database); + const bindings = createPublicHttpTestBindings(database) as ApiBindings; + const finalMessageId = createPlatformId(); + + await pushFreshController(bindings, [ + ...messageEvents({ + messageId: finalMessageId, + sourcePrefix: "partial-repair-running:final", + text: FINAL_TEXT, + }), + runtimeEvent({ + kind: "run.completed", + payload: { finalMessageId, stopReason: "end_turn" }, + sourceEventId: TERMINAL_SOURCE_EVENT_ID, + }), + ]); + await database + .prepare("DELETE FROM session_event WHERE source_event_id = ?") + .bind(TERMINAL_SOURCE_EVENT_ID) + .run(); + const completedRun = await getSessionRunSummary(database, RUN_ID); + if (completedRun === null) { + throw new Error("Missing completed Session Run fixture."); + } + const repairEvent = createSessionRunUpdatedEvent( + completedRun, + SESSION_ID, + "IDLE", + TERMINAL_SOURCE_EVENT_ID, + ); + await database + .prepare( + `UPDATE session + SET status = 'RUNNING', status_operation_id = NULL, updated_at = ? + WHERE id = ?`, + ) + .bind(completedRun.updatedAt, SESSION_ID) + .run(); + + await expect( + commitTerminalRunProjection(database, { + assistantMessage: prepareAssistantMessageProjection({ + createdByAccountId: PUBLIC_API_TEST_IDS.ownerAccount, + messageId: finalMessageId, + sessionId: SESSION_ID, + sessionRunId: RUN_ID, + }), + error: null, + runId: RUN_ID, + sessionId: SESSION_ID, + source: "driver", + targetStatus: "completed", + terminalEvent: { + event: { + ...repairEvent, + payload: { ...repairEvent.payload, finalMessageId, stopReason: "end_turn" }, + }, + occurredAt: Date.parse(repairEvent.occurredAt), + sourceEventId: TERMINAL_SOURCE_EVENT_ID, + }, + }), + ).resolves.toMatchObject({ kind: "applied" }); + + await expect( + database + .prepare( + `SELECT status, status_operation_id + FROM session WHERE id = ?`, + ) + .bind(SESSION_ID) + .first(), + ).resolves.toEqual({ status: "IDLE", status_operation_id: null }); + await expect( + readPublicThreadRunFinalOutput({ database, runId: RUN_ID, sessionId: SESSION_ID }), + ).resolves.toEqual({ text: FINAL_TEXT }); + }); + + test("rejects an explicit terminal Run view after its observed Run advances", async () => { + const database = await createPublicHttpContractDatabase(); + await insertRuntimeFixture(database); + await database + .prepare( + `UPDATE session_run + SET started_at = NULL, status = 'queued', status_seq = status_seq + 1 + WHERE id = ?`, + ) + .bind(RUN_ID) + .run(); + const observed = await getSessionRunSummary(database, RUN_ID); + if (observed === null) { + throw new Error("Missing Session Run race fixture."); + } + const error = { + code: "runtime.stale_projection", + details: {}, + message: "Stale terminal projection.", + retryable: true, + }; + const terminalTimestampMs = 2_000; + const staleTerminalRun = { + ...observed, + completedAt: new Date(terminalTimestampMs).toISOString(), + error, + startedAt: new Date(terminalTimestampMs).toISOString(), + status: "failed" as const, + updatedAt: new Date(terminalTimestampMs).toISOString(), + }; + const terminalEvent = createFailedSessionRunRuntimeEvent({ + lifecycle: "IDLE", + run: staleTerminalRun, + runError: error, + sessionId: SESSION_ID, + sourceEventId: `session-run-terminal:${RUN_ID}:run.failed`, + }); + + await database + .prepare( + `UPDATE session_run + SET started_at = 1500, status = 'booting', status_seq = status_seq + 1, + updated_at = 1500 + WHERE id = ?`, + ) + .bind(RUN_ID) + .run(); + + await expect( + commitTerminalRunProjection(database, { + assistantMessage: null, + error, + runId: RUN_ID, + sessionId: SESSION_ID, + source: "api", + targetStatus: "failed", + terminalEvent: { + event: terminalEvent, + occurredAt: terminalTimestampMs, + sourceEventId: `session-run-terminal:${RUN_ID}:run.failed`, + }, + timestampMs: terminalTimestampMs, + }), + ).resolves.toMatchObject({ currentStatus: "booting", kind: "stale" }); + await expect(getSessionRunSummary(database, RUN_ID)).resolves.toMatchObject({ + error: null, + startedAt: new Date(1_500).toISOString(), + status: "booting", + }); + const terminalRows = await database + .prepare("SELECT id FROM session_event WHERE run_id = ? AND event_type = 'run.failed'") + .bind(RUN_ID) + .all(); + expect(terminalRows.results).toEqual([]); + }); + + test("adopts a canonical cancellation when a later completion RPC loses the race", async () => { + const database = await createPublicHttpContractDatabase(); + await insertRuntimeFixture(database); + const bindings = createPublicHttpTestBindings(database) as ApiBindings; + + await pushFreshController(bindings, [ + runtimeEvent({ + kind: "run.cancelled", + payload: { reason: "session.stop" }, + sourceEventId: "provider-cancellation-id", + }), + ]); + await recordDriverInstanceCompletion(bindings, { + driverInstanceId: DRIVER_ID, + sessionRunId: RUN_ID, + }); + + const run = await database + .prepare("SELECT status FROM session_run WHERE id = ?") + .bind(RUN_ID) + .first<{ status: string }>(); + const terminalEvents = await database + .prepare( + "SELECT event_type, source_event_id FROM session_event WHERE run_id = ? AND event_type IN ('run.cancelled', 'run.completed', 'run.failed')", + ) + .bind(RUN_ID) + .all<{ event_type: string; source_event_id: string }>(); + + expect(run).toEqual({ status: "cancelled" }); + expect(terminalEvents.results).toEqual([ + { + event_type: "run.cancelled", + source_event_id: `session-run-terminal:${RUN_ID}:run.cancelled`, + }, + ]); + }); + + test("adopts the canonical provider failure when its terminal RPC acknowledgement is lost", async () => { + const database = await createPublicHttpContractDatabase(); + await insertRuntimeFixture(database); + const bindings = createPublicHttpTestBindings(database) as ApiBindings; + const providerError = { + code: "driver.provider_failed", + details: { phase: "stream" }, + message: "The provider stream failed.", + retryable: false, + } as const; + + await pushFreshController(bindings, [ runtimeEvent({ - kind: "run.completed", - payload: { - finalMessageId: replayedFinalMessageId, - finalMessageText: FINAL_TEXT, - stopReason: "end_turn", - }, - sourceEventId: TERMINAL_SOURCE_EVENT_ID, + kind: "run.failed", + payload: { error: providerError, recoverable: false }, + sourceEventId: "provider-failure-id", }), - ]; - - expect(await pushFreshController(bindings, crossBootTerminalBatch)).toHaveLength( - crossBootTerminalBatch.length, - ); - expect(await pushFreshController(bindings, crossBootTerminalBatch)).toHaveLength( - crossBootTerminalBatch.length, - ); + ]); + await recordDriverInstanceFailure(bindings, { + driverInstanceId: DRIVER_ID, + sessionRunId: RUN_ID, + error: { + code: "driver.rpc_failed", + details: {}, + message: "The later terminal RPC failed.", + retryable: false, + }, + }); - const projectedMessagesAfterReplay = await database - .prepare("SELECT content_text, id FROM session_message WHERE session_run_id = ? ORDER BY seq") + const run = await database + .prepare("SELECT error_code, error_message, status FROM session_run WHERE id = ?") .bind(RUN_ID) - .all<{ content_text: string; id: string }>(); - const terminalRowsAfterReplay = await database - .prepare("SELECT source_event_id FROM session_event WHERE source_event_id = ?") - .bind(TERMINAL_SOURCE_EVENT_ID) - .all<{ source_event_id: string }>(); - const transcript = await loadSessionViewerState(database, { - sessionId: SESSION_ID, - viewerId: PUBLIC_API_TEST_IDS.ownerAccount, - }); - const finalTranscriptMessage = transcript.messages.find( - (message) => message.id === finalMessageId, - ); - const canonicalTranscriptMessages = transcript.messages.filter( - (message) => message.role === "assistant" && message.content === FINAL_TEXT, - ); + .first<{ error_code: string; error_message: string; status: string }>(); + const terminalEvents = await database + .prepare( + "SELECT content_text, source_event_id FROM session_event WHERE run_id = ? AND event_type = 'run.failed'", + ) + .bind(RUN_ID) + .all<{ content_text: string; source_event_id: string }>(); - expect(projectedMessagesAfterReplay.results).toEqual(projectedMessagesBeforeReplay.results); - expect(terminalRowsAfterReplay.results).toEqual([ - { source_event_id: TERMINAL_SOURCE_EVENT_ID }, + expect(run).toEqual({ + error_code: providerError.code, + error_message: providerError.message, + status: "failed", + }); + expect(terminalEvents.results).toEqual([ + { + content_text: providerError.message, + source_event_id: `session-run-terminal:${RUN_ID}:run.failed`, + }, ]); - expect(finalTranscriptMessage?.content).toBe(FINAL_TEXT); - expect(canonicalTranscriptMessages.map((message) => message.id)).toEqual([finalMessageId]); - expect(finalTranscriptMessage?.content).not.toContain(PROGRESS_TEXTS.join("")); - expect(FINAL_TEXT.split("\n")).toContain("160|中文长文本校验-Aa9-表格字符|END160"); }); - test("removes provider-private citations at the public final-output boundary", async () => { + for (const corruption of ["legacy hash", "noncanonical source"] as const) { + test(`rejects ${corruption} when the completion RPC adopts a terminal event`, async () => { + const database = await createPublicHttpContractDatabase(); + await insertRuntimeFixture(database); + const bindings = createPublicHttpTestBindings(database) as ApiBindings; + const finalMessageId = createPlatformId(); + + await pushFreshController(bindings, [ + ...messageEvents({ + messageId: finalMessageId, + sourcePrefix: `corrupt-completion:${corruption}`, + text: FINAL_TEXT, + }), + runtimeEvent({ + kind: "run.completed", + payload: { finalMessageId, stopReason: "end_turn" }, + sourceEventId: "provider-completion-id", + }), + ]); + await database + .prepare( + corruption === "legacy hash" + ? `UPDATE session_event + SET artifact_attempt_id = NULL, + artifact_manifest_json = NULL, + artifact_manifest_sha256 = NULL, + semantic_hash = NULL, + terminal_event_json = NULL + WHERE run_id = ? AND event_type = 'run.completed'` + : `UPDATE session_event + SET artifact_attempt_id = NULL, + artifact_manifest_json = NULL, + artifact_manifest_sha256 = NULL, + source_event_id = 'provider-completion-id' + WHERE run_id = ? AND event_type = 'run.completed'`, + ) + .bind(RUN_ID) + .run(); + + await expect( + recordDriverInstanceCompletion(bindings, { + driverInstanceId: DRIVER_ID, + sessionRunId: RUN_ID, + }), + ).rejects.toThrow( + corruption === "legacy hash" + ? "Legacy terminal assistant messages conflict" + : "terminal semantic authority is invalid", + ); + }); + } + + test("requires an authoritative message snapshot to be sealed before run completion", async () => { const database = await createPublicHttpContractDatabase(); await insertRuntimeFixture(database); const bindings = createPublicHttpTestBindings(database) as ApiBindings; const finalMessageId = createPlatformId(); - const privateCitation = "\uE200cite\uE202turn2view0\uE202turn8view0\uE201"; - const providerText = `before${privateCitation}after`; - const events = [ - ...messageEvents({ - messageId: finalMessageId, - sourcePrefix: "private-citation:final", - text: providerText, + const terminal = runtimeEvent({ + kind: "run.completed", + payload: { finalMessageId, stopReason: "end_turn" }, + sourceEventId: "unsealed:run-completed", + }); + const unsealed = [ + runtimeEvent({ + kind: "message.added", + payload: { content: "Authoritative prefix", messageId: finalMessageId, role: "agent" }, + sourceEventId: "unsealed:snapshot", }), runtimeEvent({ - kind: "run.completed", - payload: { - finalMessageId, - finalMessageText: providerText, - stopReason: "end_turn", - }, - sourceEventId: "private-citation:run-completed", + kind: "message.delta", + payload: { contentDelta: " plus delta", messageId: finalMessageId, role: "agent" }, + sourceEventId: "unsealed:delta", }), ]; - await pushFreshController(bindings, events); + await expect(pushFreshController(bindings, [...unsealed, terminal])).rejects.toThrow( + "has no sealed authoritative snapshot", + ); - const persistedMessage = await database - .prepare("SELECT content_text FROM session_message WHERE id = ?") - .bind(finalMessageId) - .first<{ content_text: string }>(); + const runBeforeSeal = await database + .prepare("SELECT status FROM session_run WHERE id = ?") + .bind(RUN_ID) + .first<{ status: string }>(); + expect(runBeforeSeal?.status).toBe("running"); + + const completed = runtimeEvent({ + kind: "message.completed", + payload: { messageId: finalMessageId, role: "agent" }, + sourceEventId: "unsealed:completed", + }); + await pushFreshController(bindings, [completed, terminal]); - expect(persistedMessage?.content_text).toBe(providerText); await expect( readPublicThreadRunFinalOutput({ database, runId: RUN_ID, sessionId: SESSION_ID }), - ).resolves.toEqual({ - text: "beforeafter", - warnings: [ - { - code: "unresolved_provider_citation", - count: 1, - }, - ], - }); + ).resolves.toEqual({ text: "Authoritative prefix plus delta" }); }); - test("omits live-only reasoning from stored final assistant segments", async () => { + test("requires an authoritative replacement snapshot to be sealed again", async () => { const database = await createPublicHttpContractDatabase(); await insertRuntimeFixture(database); const bindings = createPublicHttpTestBindings(database) as ApiBindings; const finalMessageId = createPlatformId(); - const privateReasoningText = "Private reasoning should stay out of stored history."; - const events = [ - runtimeEvent({ - kind: "thought.started", - payload: { messageId: finalMessageId }, - sourceEventId: "reasoning:started", - }), + const originalOccurredAt = Date.parse("2026-08-30T05:00:00.000Z"); + const carrierOccurredAt = originalOccurredAt + 2; + const replacementOccurredAt = originalOccurredAt + 3; + + await pushFreshController(bindings, [ runtimeEvent({ - kind: "thought.delta", - payload: { contentDelta: privateReasoningText, messageId: finalMessageId }, - sourceEventId: "reasoning:delta", + kind: "message.added", + occurredAt: originalOccurredAt, + payload: { content: "Original answer", messageId: finalMessageId, role: "agent" }, + sourceEventId: "replacement:original:snapshot", }), runtimeEvent({ - kind: "thought.completed", - payload: { messageId: finalMessageId }, - sourceEventId: "reasoning:completed", - }), - ...messageEvents({ - messageId: finalMessageId, - sourcePrefix: "reasoning:final", - text: FINAL_TEXT, + kind: "message.completed", + occurredAt: originalOccurredAt + 1, + payload: { messageId: finalMessageId, role: "agent" }, + sourceEventId: "replacement:original:completed", }), runtimeEvent({ - kind: "run.completed", + kind: "tool.call.updated", + occurredAt: carrierOccurredAt, payload: { - finalMessageId, - finalMessageText: FINAL_TEXT, - stopReason: "end_turn", + rawOutputDelta: "carrier output", + status: "completed", + toolCallId: "replacement-orphan-tool", }, - sourceEventId: "reasoning:run-completed", + sourceEventId: "replacement:carrier-output", }), - ]; + ]); - await pushFreshController(bindings, events); + const replacement = runtimeEvent({ + kind: "message.added", + occurredAt: replacementOccurredAt, + payload: { content: "Replacement answer", messageId: finalMessageId, role: "agent" }, + sourceEventId: "replacement:snapshot", + }); + const terminal = runtimeEvent({ + kind: "run.completed", + payload: { finalMessageId, stopReason: "end_turn" }, + sourceEventId: "replacement:run-completed", + }); - const persistedMessage = await database - .prepare("SELECT content_text, segments_json FROM session_message WHERE id = ?") - .bind(finalMessageId) - .first<{ content_text: string; segments_json: string }>(); + await expect(pushFreshController(bindings, [replacement, terminal])).rejects.toThrow( + "has no sealed authoritative snapshot", + ); - expect(persistedMessage?.content_text).toBe(FINAL_TEXT); - expect(JSON.parse(persistedMessage?.segments_json ?? "[]")).toEqual([ - { kind: "text", text: FINAL_TEXT }, + await pushFreshController(bindings, [ + runtimeEvent({ + kind: "message.completed", + occurredAt: replacementOccurredAt + 1, + payload: { messageId: finalMessageId, role: "agent" }, + sourceEventId: "replacement:completed", + }), + terminal, + ]); + + await expect( + readPublicThreadRunFinalOutput({ database, runId: RUN_ID, sessionId: SESSION_ID }), + ).resolves.toEqual({ text: "Replacement answer" }); + const assistantRows = await database + .prepare( + `SELECT created_at, id, seq + FROM session_message + WHERE session_id = ? AND session_run_id = ? AND role = 'assistant' + ORDER BY seq`, + ) + .bind(SESSION_ID, RUN_ID) + .all<{ created_at: number; id: string; seq: number }>(); + expect(assistantRows.results.map(({ created_at, id }) => ({ created_at, id }))).toEqual([ + { created_at: carrierOccurredAt, id: RUN_ID }, + { created_at: replacementOccurredAt, id: finalMessageId }, + ]); + expect(assistantRows.results.map(({ seq }) => seq)).toEqual([ + assistantRows.results[0]?.seq, + (assistantRows.results[0]?.seq ?? 0) + 1, + ]); + const fresh = await loadSessionViewerState(database, { + sessionId: SESSION_ID, + viewerId: PUBLIC_API_TEST_IDS.outsiderAccount, + }); + expect( + fresh.messages + .filter((message) => message.id === RUN_ID || message.id === finalMessageId) + .map(({ createdAt, id }) => ({ createdAt, id })), + ).toEqual([ + { createdAt: new Date(carrierOccurredAt).toISOString(), id: RUN_ID }, + { createdAt: new Date(replacementOccurredAt).toISOString(), id: finalMessageId }, ]); - expect(persistedMessage?.segments_json).not.toContain(privateReasoningText); }); - test("fails closed when a cross-boot replay conflicts with the persisted final snapshot", async () => { + test("rejects a final stream identity shared with owner-only message rows", async () => { const database = await createPublicHttpContractDatabase(); await insertRuntimeFixture(database); const bindings = createPublicHttpTestBindings(database) as ApiBindings; const finalMessageId = createPlatformId(); - const terminalBatch = [ + + await pushFreshController(bindings, [ ...messageEvents({ messageId: finalMessageId, - sourcePrefix: "conflict:original-final", - text: FINAL_TEXT, + sourcePrefix: "mixed-visibility:public", + text: "Public answer", }), runtimeEvent({ - kind: "run.completed", - payload: { - finalMessageId, - finalMessageText: FINAL_TEXT, - stopReason: "end_turn", - }, - sourceEventId: TERMINAL_SOURCE_EVENT_ID, - }), - ]; - const failingBindings = { - ...bindings, - DB: failTerminalSessionEventInsert(database), - } as ApiBindings; - - await expect(pushFreshController(failingBindings, terminalBatch)).rejects.toBeInstanceOf(Error); - - const replayedFinalMessageId = createPlatformId(); - const conflictingText = `${FINAL_TEXT}\nCONFLICTING-REPLAY`; - const conflictingReplay = [ - ...messageEvents({ - messageId: replayedFinalMessageId, - sourcePrefix: "conflict:reconnected-final", - text: conflictingText, + kind: "message.added", + payload: { content: "Owner-only replacement", messageId: finalMessageId, role: "agent" }, + sourceEventId: "mixed-visibility:owner:snapshot", + visibility: "owner_debug", }), runtimeEvent({ - kind: "run.completed", - payload: { - finalMessageId: replayedFinalMessageId, - finalMessageText: conflictingText, - stopReason: "end_turn", - }, - sourceEventId: TERMINAL_SOURCE_EVENT_ID, + kind: "message.completed", + payload: { messageId: finalMessageId, role: "agent" }, + sourceEventId: "mixed-visibility:owner:completed", + visibility: "owner_debug", }), - ]; - - await expect(pushFreshController(bindings, conflictingReplay)).rejects.toThrow( - "Canonical final assistant message conflicts with the persisted projection", - ); - - const messages = await database - .prepare("SELECT content_text, id FROM session_message WHERE session_run_id = ? ORDER BY seq") - .bind(RUN_ID) - .all<{ content_text: string; id: string }>(); - const terminalRows = await database - .prepare("SELECT source_event_id FROM session_event WHERE source_event_id = ?") - .bind(TERMINAL_SOURCE_EVENT_ID) - .all<{ source_event_id: string }>(); + ]); - expect(messages.results).toEqual([{ content_text: FINAL_TEXT, id: finalMessageId }]); - expect(terminalRows.results).toEqual([]); await expect( - readPublicThreadRunFinalOutput({ database, runId: RUN_ID, sessionId: SESSION_ID }), - ).resolves.toEqual({ text: FINAL_TEXT }); - }); - - test("does not guess a progress message when the terminal RPC has no final identity", async () => { - const database = await createPublicHttpContractDatabase(); - await insertRuntimeFixture(database); - const bindings = createPublicHttpTestBindings(database) as ApiBindings; - const progressMessageId = createPlatformId(); - const progressEvents = messageEvents({ - messageId: progressMessageId, - sourcePrefix: "fallback:progress", - text: PROGRESS_TEXTS[0], - }); - - await pushFreshController(bindings, progressEvents); - await recordDriverInstanceCompletion(bindings, { - driverInstanceId: DRIVER_ID, - driverReady: true, - }); + pushFreshController(bindings, [ + runtimeEvent({ + kind: "run.completed", + payload: { finalMessageId, stopReason: "end_turn" }, + sourceEventId: "mixed-visibility:run-completed", + }), + ]), + ).rejects.toThrow("mixed visibility"); - const finalOutput = await readPublicThreadRunFinalOutput({ - database, - runId: RUN_ID, - sessionId: SESSION_ID, - }); + const run = await database + .prepare("SELECT status FROM session_run WHERE id = ?") + .bind(RUN_ID) + .first<{ status: string }>(); const messages = await database .prepare("SELECT id FROM session_message WHERE session_run_id = ?") .bind(RUN_ID) .all<{ id: string }>(); - expect(finalOutput).toBeNull(); + expect(run?.status).toBe("running"); expect(messages.results).toEqual([]); }); - test("fails closed when run completion omits the final text snapshot", async () => { + test.each(["unknown", "user"] as const)( + "rejects a final message ID that references an %s stream", + async (boundary) => { + const database = await createPublicHttpContractDatabase(); + await insertRuntimeFixture(database); + const bindings = createPublicHttpTestBindings(database) as ApiBindings; + const finalMessageId = createPlatformId(); + const otherMessageId = createPlatformId(); + const stream = + boundary === "unknown" + ? messageEvents({ + messageId: otherMessageId, + sourcePrefix: "invalid-final:other", + text: "Another assistant message", + }) + : [ + runtimeEvent({ + kind: "message.added", + payload: { content: "User input", messageId: finalMessageId, role: "user" }, + sourceEventId: "invalid-final:user-snapshot", + }), + runtimeEvent({ + kind: "message.completed", + payload: { messageId: finalMessageId, role: "user" }, + sourceEventId: "invalid-final:user-completed", + }), + ]; + + await expect( + pushFreshController(bindings, [ + ...stream, + runtimeEvent({ + kind: "run.completed", + payload: { finalMessageId, stopReason: "end_turn" }, + sourceEventId: `invalid-final:${boundary}:run-completed`, + }), + ]), + ).rejects.toThrow( + boundary === "user" ? "conflicting identity rows" : "has no sealed authoritative snapshot", + ); + + const run = await database + .prepare("SELECT status FROM session_run WHERE id = ?") + .bind(RUN_ID) + .first<{ status: string }>(); + expect(run?.status).toBe("running"); + }, + ); + + test("rejects a sealed message stream from another run", async () => { const database = await createPublicHttpContractDatabase(); await insertRuntimeFixture(database); const bindings = createPublicHttpTestBindings(database) as ApiBindings; - const progressMessageId = createPlatformId(); - const events = [ - ...messageEvents({ - messageId: progressMessageId, - sourcePrefix: "missing-snapshot:progress", - text: PROGRESS_TEXTS[0], - }), - runtimeEvent({ - kind: "run.completed", - payload: { finalMessageId: progressMessageId, stopReason: "end_turn" }, - sourceEventId: "missing-snapshot:run-completed", - }), - ]; + const finalMessageId = createPlatformId(); + const otherRunId = createPlatformId(); - await pushFreshController(bindings, events); + await pushFreshController( + bindings, + messageEvents({ + messageId: finalMessageId, + sourcePrefix: "cross-run:message", + text: "Another run's answer", + }), + ); + await database + .prepare( + `INSERT INTO session_run ( + id, session_id, agent_id, created_by_account_id, trigger, status, + trace_id, created_at, updated_at + ) VALUES (?, ?, ?, ?, 'user_prompt', 'completed', ?, 1, 1)`, + ) + .bind( + otherRunId, + SESSION_ID, + PUBLIC_API_TEST_IDS.agent, + PUBLIC_API_TEST_IDS.ownerAccount, + "trace-other-run", + ) + .run(); + await database + .prepare("UPDATE session_event SET run_id = ? WHERE session_id = ? AND stream_id = ?") + .bind(otherRunId, SESSION_ID, finalMessageId) + .run(); await expect( - readPublicThreadRunFinalOutput({ database, runId: RUN_ID, sessionId: SESSION_ID }), - ).resolves.toBeNull(); + pushFreshController(bindings, [ + runtimeEvent({ + kind: "run.completed", + payload: { finalMessageId, stopReason: "end_turn" }, + sourceEventId: "cross-run:run-completed", + }), + ]), + ).rejects.toThrow("conflicting identity rows"); }); test("does not persist canonical output after another terminal status wins", async () => { @@ -804,14 +5251,15 @@ describe("runtime final output ingestion", () => { kind: "run.completed", payload: { finalMessageId, - finalMessageText: FINAL_TEXT, stopReason: "end_turn", }, sourceEventId: "stale-completion:run-completed", }), ]; - await pushFreshController(bindings, events); + await expect(pushFreshController(bindings, events)).rejects.toThrow( + "requires its exact active Session Run", + ); const run = await database .prepare("SELECT status FROM session_run WHERE id = ?") diff --git a/apps/api/tests/runtime-operation-ready-authority-migration.test.ts b/apps/api/tests/runtime-operation-ready-authority-migration.test.ts new file mode 100644 index 00000000..d7c90fee --- /dev/null +++ b/apps/api/tests/runtime-operation-ready-authority-migration.test.ts @@ -0,0 +1,188 @@ +import { describe, expect, test } from "bun:test"; + +import type { RuntimeOperationId, SessionId } from "@mosoo/id"; +import { + createRuntimeEventSemanticHash, + stringifyRuntimeEventSemanticValue, +} from "@mosoo/runtime-events"; +import type { RuntimeEventEnvelope } from "@mosoo/runtime-events"; + +import { createRuntimeOperationSessionEvent } from "../src/modules/runtime/application/runtime-state-operation-events"; +import type { RuntimeOperationEvent } from "../src/modules/runtime/application/runtime-state-operation-events"; +import { createSessionRuntimeEventProjection } from "../src/modules/sessions/domain/session-runtime-event-projection"; +import { applyDrizzleMigration, applyDrizzleMigrationsBefore } from "./helpers/drizzle-migrations"; +import { SqliteD1Database } from "./helpers/sqlite-d1"; + +const MIGRATION_TAG = "0018_runtime-operation-ready-authority"; + +const ACCOUNT_ID = "01J0000000000000000000001K"; +const AGENT_ID = "01J0000000000000000000001J"; +const APP_ID = "01J0000000000000000000001M"; +const SESSION_ID = "01J0000000000000000000001H" as SessionId; +const OPERATION_ID = "01J0000000000000000000001R" as RuntimeOperationId; +const LEGACY_OPERATION_ID = "01J0000000000000000000001S" as RuntimeOperationId; + +async function createPre0018Database(): Promise { + const database = new SqliteD1Database(); + applyDrizzleMigrationsBefore(database, MIGRATION_TAG); + return database; +} + +async function insertSession(database: SqliteD1Database): Promise { + await database + .prepare( + `INSERT INTO session ( + agent_id, created_at, creator_account_id, id, kind, model, app_id, + provider, renamed, runtime_id, status, updated_at + ) VALUES (?, 1, ?, ?, 'agent', 'gpt-5.4', ?, 'openai', 0, 'codex', 'IDLE', 1)`, + ) + .bind(AGENT_ID, ACCOUNT_ID, SESSION_ID, APP_ID) + .run(); +} + +async function insertLegacyReadyReceipt(database: SqliteD1Database): Promise { + await database + .prepare( + `INSERT INTO session_event ( + agent_id, content_text, created_at, ended_at, event_type, family, id, + occurred_at, process_status, process_type, semantic_hash, seq, session_id, + source_event_id, source, visibility + ) VALUES (?, '', 1, 1, 'agent.task.updated', 'agent', ?, 1, 'available', + 'agent_task', ?, 1, ?, ?, 'api', 'all_consumers')`, + ) + .bind( + AGENT_ID, + "01J0000000000000000000001T", + "0".repeat(64), + SESSION_ID, + `runtime-operation:${LEGACY_OPERATION_ID}:${SESSION_ID}:ready`, + ) + .run(); +} + +function operationEvent(status: RuntimeOperationEvent["status"]): RuntimeOperationEvent { + return { + agentId: AGENT_ID, + observedAt: status === "updating" ? "2026-08-30T00:00:00.000Z" : "2026-08-30T00:00:01.000Z", + operation: "restartDriver", + status, + }; +} + +async function insertCanonicalOperationEvent( + database: SqliteD1Database, + status: RuntimeOperationEvent["status"], + seq: number, +): Promise { + const event = createRuntimeOperationSessionEvent({ + event: operationEvent(status), + operationId: OPERATION_ID, + sessionId: SESSION_ID, + }); + const projection = createSessionRuntimeEventProjection(event); + const occurredAt = Date.parse(event.occurredAt); + await database + .prepare( + `INSERT INTO session_event ( + agent_id, content_text, created_at, ended_at, event_type, family, id, + occurred_at, process_status, process_type, run_id, + runtime_operation_event_json, semantic_hash, seq, session_id, + source_event_id, source, stream_id, trace_id, visibility + ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, + ) + .bind( + AGENT_ID, + projection.contentText, + occurredAt, + occurredAt, + projection.eventType, + projection.family, + event.id, + occurredAt, + projection.processStatus, + projection.processType, + projection.runId, + stringifyRuntimeEventSemanticValue(event), + await createRuntimeEventSemanticHash(event), + seq, + SESSION_ID, + event.sourceEventId, + projection.source, + projection.streamId, + projection.traceId, + projection.visibility, + ) + .run(); + return event; +} + +describe("runtime operation authority migration", () => { + test("preserves legacy NULL while admitting both canonical operation phases", async () => { + const database = await createPre0018Database(); + await insertSession(database); + await insertLegacyReadyReceipt(database); + + applyDrizzleMigration(database, MIGRATION_TAG); + + expect( + ( + await database.prepare("PRAGMA table_info(session_event)").all<{ name: string }>() + ).results.map(({ name }) => name), + ).toContain("runtime_operation_event_json"); + await expect( + database + .prepare( + `SELECT runtime_operation_event_json + FROM session_event + WHERE source_event_id LIKE 'runtime-operation:%:ready'`, + ) + .first(), + ).resolves.toEqual({ runtime_operation_event_json: null }); + + await insertCanonicalOperationEvent(database, "updating", 2); + await insertCanonicalOperationEvent(database, "ready", 3); + const rows = await database + .prepare( + `SELECT json_extract(runtime_operation_event_json, '$.payload.status') AS status + FROM session_event + WHERE runtime_operation_event_json IS NOT NULL + ORDER BY seq`, + ) + .all<{ status: string }>(); + expect(rows.results).toEqual([{ status: "updating" }, { status: "ready" }]); + }); + + test("rejects a non-operation phase authority carrier", async () => { + const database = await createPre0018Database(); + await insertSession(database); + applyDrizzleMigration(database, MIGRATION_TAG); + const event = await insertCanonicalOperationEvent(database, "updating", 1); + const forged = { + ...event, + id: "01J0000000000000000000001V", + payload: { ...(event.payload as Record), status: "forged" }, + sourceEventId: "forged-runtime-operation-event", + } satisfies RuntimeEventEnvelope; + + await expect( + database + .prepare( + `INSERT INTO session_event ( + agent_id, content_text, created_at, ended_at, event_type, family, id, + occurred_at, process_status, process_type, runtime_operation_event_json, + semantic_hash, seq, session_id, source_event_id, source, visibility + ) VALUES (?, '', 2, 2, 'agent.task.updated', 'agent', ?, 2, 'available', + 'agent_task', ?, ?, 2, ?, ?, 'api', 'all_consumers')`, + ) + .bind( + AGENT_ID, + forged.id, + stringifyRuntimeEventSemanticValue(forged), + await createRuntimeEventSemanticHash(forged), + SESSION_ID, + forged.sourceEventId, + ) + .run(), + ).rejects.toThrow("CHECK constraint failed"); + }); +}); diff --git a/apps/api/tests/runtime-provisioning-lease.test.ts b/apps/api/tests/runtime-provisioning-lease.test.ts new file mode 100644 index 00000000..f9672bd3 --- /dev/null +++ b/apps/api/tests/runtime-provisioning-lease.test.ts @@ -0,0 +1,799 @@ +import { describe, expect, test } from "bun:test"; + +import { createPlatformId } from "@mosoo/id"; +import type { DriverInstanceId } from "@mosoo/id"; +import { PLATFORM_ID_FIXTURES } from "@mosoo/id/testing"; + +import { + createDriverInstanceRecord, + runtimeProvisioningDriverLaunchIsOwned, +} from "../src/modules/runtime/infrastructure/driver-instance/driver-instance-record.repository"; +import { stopDriverSession } from "../src/modules/runtime/infrastructure/driver-session-stop.service"; +import { startProvisionProcessWithOwnershipFence } from "../src/modules/runtime/infrastructure/runtime-sandbox-provisioning/runtime-driver-process-cleanup"; +import { + adoptReadyRuntimeRunProvisioningLease, + claimRuntimeProvisioningDriverCleanup, + claimRuntimeRunProvisioningLease, + claimStaleRuntimeProvisioningLeases, + heartbeatRuntimeRunProvisioningLease, + readRuntimeProvisioningCleanupTargets, + recordRuntimeProvisioningConversationTarget, + releaseAbortedRuntimeProvisioningLease, + releaseReadyRuntimeRunProvisioningLease, + renewRuntimeProvisioningLeaseOwnership, +} from "../src/modules/runtime/infrastructure/runtime-subject-lifecycle/runtime-provisioning-lease-store"; +import type { RuntimeRunProvisioningLease } from "../src/modules/runtime/infrastructure/runtime-subject-lifecycle/runtime-provisioning-lease-store"; +import { repairStaleRuntimeProvisioningLeases } from "../src/modules/runtime/infrastructure/runtime-subject-lifecycle/runtime-subject-maintenance.service"; +import { + ensureRuntimeSubjectId, + recordRuntimeConversationSessionClosed, +} from "../src/modules/runtime/infrastructure/runtime-subject-lifecycle/runtime-subject-store"; +import type { RuntimeProcessHandle } from "../src/modules/runtime/infrastructure/sandbox-handles"; +import type { ApiBindings } from "../src/platform/cloudflare/worker-types"; +import { SqliteD1Database } from "./helpers/sqlite-d1"; + +const DRIVER_ID = PLATFORM_ID_FIXTURES.driverInstance; +const NEW_CONVERSATION_ID = "01J00000000000000000000003"; +const OLD_CONVERSATION_ID = "01J00000000000000000000002"; +const RUN_ID = PLATFORM_ID_FIXTURES.sessionRun; +const SANDBOX_ID = PLATFORM_ID_FIXTURES.sandbox; +const SESSION_ID = PLATFORM_ID_FIXTURES.session; + +function createDatabase(options: { readonly insertSandbox?: boolean } = {}): SqliteD1Database { + const database = new SqliteD1Database({ foreignKeys: false }); + database.execute(` + CREATE TABLE session ( + id text PRIMARY KEY NOT NULL, + archived_at integer, + cleanup_operation_kind text, + last_run_id text, + runtime_provisioning_heartbeat_at integer, + runtime_provisioning_operation_id text, + runtime_provisioning_run_id text, + runtime_provisioning_sandbox_id text, + runtime_provisioning_sandbox_incarnation integer, + runtime_provisioning_sandbox_session_id text, + status text NOT NULL, + status_operation_id text, + CHECK ( + ( + runtime_provisioning_operation_id IS NULL + AND runtime_provisioning_run_id IS NULL + AND runtime_provisioning_sandbox_id IS NULL + AND runtime_provisioning_sandbox_incarnation IS NULL + AND runtime_provisioning_sandbox_session_id IS NULL + AND runtime_provisioning_heartbeat_at IS NULL + ) OR ( + runtime_provisioning_operation_id IS NOT NULL + AND runtime_provisioning_sandbox_id IS NOT NULL + AND runtime_provisioning_heartbeat_at IS NOT NULL + AND typeof(runtime_provisioning_heartbeat_at) = 'integer' + AND archived_at IS NULL + AND cleanup_operation_kind IS NULL + AND status_operation_id IS NULL + ) + ) + ); + CREATE TABLE session_run ( + driver_instance_id text, + id text PRIMARY KEY NOT NULL, + session_id text NOT NULL, + status text NOT NULL + ); + CREATE TABLE sandbox_session ( + cloudflare_session_id text NOT NULL DEFAULT '${OLD_CONVERSATION_ID}', + cleanup_operation_id text, + sandbox_id text NOT NULL, + sandbox_incarnation integer NOT NULL DEFAULT 1, + session_id text PRIMARY KEY NOT NULL, + status text NOT NULL, + updated_at integer NOT NULL DEFAULT 0 + ); + CREATE TABLE driver_instance ( + boot_token_expires_at integer, + boot_token_hash blob, + created_at integer NOT NULL DEFAULT 0, + expires_at integer, + generation integer NOT NULL DEFAULT 0, + heartbeat_count integer NOT NULL DEFAULT 0, + id text PRIMARY KEY NOT NULL, + protocol text NOT NULL DEFAULT 'orpc-ws', + protocol_version integer NOT NULL DEFAULT 3, + restart_count integer NOT NULL DEFAULT 0, + runtime text NOT NULL DEFAULT 'openai-runtime', + sandbox_id text NOT NULL, + sandbox_incarnation integer NOT NULL DEFAULT 1, + sandbox_session_id text NOT NULL, + status text NOT NULL, + status_changed_at integer DEFAULT 0 NOT NULL, + status_event text DEFAULT 'driver.provision' NOT NULL, + status_operation_id text, + status_seq integer DEFAULT 0 NOT NULL, + status_source text DEFAULT 'system' NOT NULL, + updated_at integer DEFAULT 0 NOT NULL + ); + CREATE TABLE sandbox ( + agent_id text, + app_id text, + bind_mount_ready integer NOT NULL DEFAULT 0, + claim_expires_at integer, + claim_owner text, + created_at integer NOT NULL DEFAULT 0, + global_mounts_json text NOT NULL DEFAULT '[]', + id text PRIMARY KEY NOT NULL, + inactive_deadline_at integer, + incarnation integer NOT NULL DEFAULT 1, + kind text, + last_backup_id text, + last_error text, + last_error_code text, + last_restore_backup_id text, + network_constraints_hash text, + operation_kind text, + owner_account_id text, + status text NOT NULL DEFAULT 'active', + status_changed_at integer NOT NULL DEFAULT 0, + status_event text NOT NULL DEFAULT 'runtime_subject.active', + status_operation_id text, + status_seq integer NOT NULL DEFAULT 0, + status_source text NOT NULL DEFAULT 'test', + subject_id text, + subject_kind text, + updated_at integer NOT NULL + ); + INSERT INTO session (id, last_run_id, status) + VALUES ('${SESSION_ID}', '${RUN_ID}', 'RUNNING'); + INSERT INTO session_run (id, session_id, status) + VALUES ('${RUN_ID}', '${SESSION_ID}', 'running'); + `); + if (options.insertSandbox !== false) { + database.execute(` + INSERT INTO sandbox ( + agent_id, app_id, id, kind, owner_account_id, subject_id, subject_kind, updated_at + ) VALUES ( + '${PLATFORM_ID_FIXTURES.agent}', '${PLATFORM_ID_FIXTURES.app}', '${SANDBOX_ID}', + 'cattle', '${PLATFORM_ID_FIXTURES.account}', '${SESSION_ID}', 'session', 0 + ); + `); + } + return database; +} + +async function insertDurableRunHandoff( + database: D1Database, + lease: RuntimeRunProvisioningLease, +): Promise { + await database + .prepare( + `INSERT INTO sandbox_session (sandbox_id, session_id, status) + VALUES (?, ?, 'active')`, + ) + .bind(SANDBOX_ID, SESSION_ID) + .run(); + await database + .prepare( + `INSERT INTO driver_instance (id, sandbox_id, sandbox_session_id, status) + VALUES (?, ?, ?, 'ready')`, + ) + .bind(DRIVER_ID, SANDBOX_ID, SESSION_ID) + .run(); + await database + .prepare("UPDATE session_run SET driver_instance_id = ? WHERE id = ?") + .bind(DRIVER_ID, RUN_ID) + .run(); + const targeted = await recordRuntimeProvisioningConversationTarget(database, { + lease, + sandboxIncarnation: 1, + sandboxSessionId: OLD_CONVERSATION_ID, + }); + if (targeted === null) { + throw new Error("Runtime provisioning fixture lost its target lease."); + } + return targeted; +} + +describe("runtime provisioning lease", () => { + test("allocates the lifecycle row before claiming initial provisioning", async () => { + const database = createDatabase({ insertSandbox: false }); + const sandboxId = await ensureRuntimeSubjectId(database, { + agentId: PLATFORM_ID_FIXTURES.agent, + appId: PLATFORM_ID_FIXTURES.app, + executionOwnerUserId: PLATFORM_ID_FIXTURES.account, + kind: "cattle", + runtimeSubjectId: SANDBOX_ID, + subjectId: SESSION_ID, + subjectKind: "session", + }); + + expect(sandboxId).toBe(SANDBOX_ID); + await expect( + ensureRuntimeSubjectId(database, { + agentId: PLATFORM_ID_FIXTURES.agent, + appId: PLATFORM_ID_FIXTURES.app, + executionOwnerUserId: "01J0000000000000000000000F", + kind: "cattle", + runtimeSubjectId: SANDBOX_ID, + subjectId: SESSION_ID, + subjectKind: "session", + }), + ).rejects.toThrow("identity does not match"); + expect( + await claimRuntimeRunProvisioningLease(database, { + runId: RUN_ID, + sandboxId, + sessionId: SESSION_ID, + }), + ).not.toBeNull(); + }); + + test("rejects a half-written lease without a heartbeat", async () => { + const database = createDatabase(); + + await expect( + database + .prepare( + `UPDATE session + SET runtime_provisioning_operation_id = ?, + runtime_provisioning_sandbox_id = ? + WHERE id = ?`, + ) + .bind(PLATFORM_ID_FIXTURES.runtimeOperation, SANDBOX_ID, SESSION_ID) + .run(), + ).rejects.toThrow(); + }); + + test("uses stable provisioning identity across heartbeats and rejects a rotated owner", async () => { + const database = createDatabase(); + const claimed = await claimRuntimeRunProvisioningLease(database, { + runId: RUN_ID, + sandboxId: SANDBOX_ID, + sessionId: SESSION_ID, + }); + expect(claimed).not.toBeNull(); + if (claimed === null) { + return; + } + await database + .prepare( + `INSERT INTO sandbox_session ( + cloudflare_session_id, sandbox_id, sandbox_incarnation, session_id, status + ) VALUES (?, ?, 1, ?, 'active')`, + ) + .bind(OLD_CONVERSATION_ID, SANDBOX_ID, SESSION_ID) + .run(); + const targeted = await recordRuntimeProvisioningConversationTarget(database, { + lease: claimed, + sandboxIncarnation: 1, + sandboxSessionId: OLD_CONVERSATION_ID, + }); + expect(targeted).not.toBeNull(); + if (targeted === null) { + return; + } + + expect(await heartbeatRuntimeRunProvisioningLease(database, targeted)).toBe(true); + expect( + await createDriverInstanceRecord({ DB: database } as ApiBindings, { + bootTokenHash: new Uint8Array([1]), + conflictStrategy: "insert-only", + driverInstanceId: DRIVER_ID, + runtime: "openai-runtime", + runtimeProvisioningLease: targeted, + sandboxId: SANDBOX_ID, + sandboxIncarnation: 1, + sandboxSessionId: SESSION_ID, + }), + ).toMatchObject({ generation: 0, status: "created" }); + + expect( + await runtimeProvisioningDriverLaunchIsOwned(database, { + bootTokenHash: new Uint8Array([1]), + driverGeneration: 0, + driverInstanceId: DRIVER_ID, + lease: targeted, + }), + ).toBe(true); + + await database + .prepare("UPDATE session SET runtime_provisioning_heartbeat_at = 1 WHERE id = ?") + .bind(SESSION_ID) + .run(); + const maintenance = await claimStaleRuntimeProvisioningLeases(database, { + heartbeatAtLte: 1, + limit: 1, + }); + expect(maintenance).toHaveLength(1); + expect( + await runtimeProvisioningDriverLaunchIsOwned(database, { + bootTokenHash: new Uint8Array([1]), + driverGeneration: 0, + driverInstanceId: DRIVER_ID, + lease: targeted, + }), + ).toBe(false); + expect( + await createDriverInstanceRecord({ DB: database } as ApiBindings, { + bootTokenHash: new Uint8Array([2]), + conflictStrategy: "insert-only", + driverInstanceId: createPlatformId(), + runtime: "openai-runtime", + runtimeProvisioningLease: targeted, + sandboxId: SANDBOX_ID, + sandboxIncarnation: 1, + sandboxSessionId: SESSION_ID, + }), + ).toEqual({ + bootTokenExpiresAt: null, + generation: null, + reason: "existing-driver", + status: "skipped", + }); + }); + + test("stops a late process after activation moves to the next incarnation", async () => { + let activeIncarnation = 1; + let disposeCalls = 0; + let killCalls = 0; + let startCalls = 0; + const process = { + [Symbol.dispose]: () => { + disposeCalls++; + }, + getStatus: async () => "running" as const, + kill: async () => { + killCalls++; + }, + } as unknown as RuntimeProcessHandle; + const assertOwned = async () => { + if (activeIncarnation !== 1) { + throw new Error("Runtime Driver provisioning lost launch ownership."); + } + }; + + await expect( + startProvisionProcessWithOwnershipFence({ + assertOwned, + context: { sandboxId: SANDBOX_ID }, + message: "test late process cleanup", + startProcess: async () => { + startCalls++; + activeIncarnation = 2; + return process; + }, + }), + ).rejects.toThrow("lost launch ownership"); + expect({ disposeCalls, killCalls, startCalls }).toEqual({ + disposeCalls: 1, + killCalls: 1, + startCalls: 1, + }); + + await expect( + startProvisionProcessWithOwnershipFence({ + assertOwned, + context: { sandboxId: SANDBOX_ID }, + message: "test pre-launch fence", + startProcess: async () => { + startCalls++; + return process; + }, + }), + ).rejects.toThrow("lost launch ownership"); + expect(startCalls).toBe(1); + }); + + test("fences cleanup until the conversation, Driver, and Run handoff are durable", async () => { + const database = createDatabase(); + let lease = await claimRuntimeRunProvisioningLease(database, { + runId: RUN_ID, + sandboxId: SANDBOX_ID, + sessionId: SESSION_ID, + }); + expect(lease).not.toBeNull(); + if (lease === null) { + return; + } + + await expect( + database + .prepare( + "UPDATE session SET archived_at = 1, cleanup_operation_kind = 'archive' WHERE id = ?", + ) + .bind(SESSION_ID) + .run(), + ).rejects.toThrow(); + expect( + await releaseReadyRuntimeRunProvisioningLease(database, { + driverGeneration: 0, + driverInstanceId: DRIVER_ID, + lease, + }), + ).toBe(false); + + lease = await insertDurableRunHandoff(database, lease); + expect( + await releaseReadyRuntimeRunProvisioningLease(database, { + driverGeneration: 0, + driverInstanceId: DRIVER_ID, + lease, + }), + ).toBe(true); + expect( + await database + .prepare("SELECT runtime_provisioning_operation_id FROM session WHERE id = ?") + .bind(SESSION_ID) + .first(), + ).toEqual({ runtime_provisioning_operation_id: null }); + }); + + test("does not release a stale Driver generation handoff", async () => { + const database = createDatabase(); + let lease = await claimRuntimeRunProvisioningLease(database, { + runId: RUN_ID, + sandboxId: SANDBOX_ID, + sessionId: SESSION_ID, + }); + expect(lease).not.toBeNull(); + if (lease === null) { + return; + } + lease = await insertDurableRunHandoff(database, lease); + await database + .prepare("UPDATE driver_instance SET generation = 1 WHERE id = ?") + .bind(DRIVER_ID) + .run(); + + expect( + await releaseReadyRuntimeRunProvisioningLease(database, { + driverGeneration: 0, + driverInstanceId: DRIVER_ID, + lease, + }), + ).toBe(false); + expect(await heartbeatRuntimeRunProvisioningLease(database, lease)).toBe(true); + expect( + await releaseReadyRuntimeRunProvisioningLease(database, { + driverGeneration: 1, + driverInstanceId: DRIVER_ID, + lease, + }), + ).toBe(true); + }); + + test("cleanup ownership wins before provisioning without an external-work window", async () => { + const database = createDatabase(); + await database + .prepare( + `UPDATE session + SET archived_at = 1, + cleanup_operation_kind = 'archive', + status = 'RESCHEDULING', + status_operation_id = ? + WHERE id = ?`, + ) + .bind(PLATFORM_ID_FIXTURES.runtimeOperation, SESSION_ID) + .run(); + + expect( + await claimRuntimeRunProvisioningLease(database, { + runId: RUN_ID, + sandboxId: SANDBOX_ID, + sessionId: SESSION_ID, + }), + ).toBeNull(); + }); + + test("maintenance adopts a durable handoff instead of destroying a healthy Run", async () => { + const database = createDatabase(); + let lease = await claimRuntimeRunProvisioningLease(database, { + runId: RUN_ID, + sandboxId: SANDBOX_ID, + sessionId: SESSION_ID, + }); + expect(lease).not.toBeNull(); + if (lease === null) { + return; + } + lease = await insertDurableRunHandoff(database, lease); + await database + .prepare("UPDATE session SET runtime_provisioning_heartbeat_at = 1 WHERE id = ?") + .bind(SESSION_ID) + .run(); + + const [maintenance] = await claimStaleRuntimeProvisioningLeases(database, { + heartbeatAtLte: 1, + limit: 1, + }); + expect(maintenance).toBeDefined(); + if (maintenance === undefined) { + return; + } + expect(await adoptReadyRuntimeRunProvisioningLease(database, maintenance)).toBe(true); + expect(await heartbeatRuntimeRunProvisioningLease(database, lease)).toBe(false); + expect(await releaseAbortedRuntimeProvisioningLease(database, lease)).toBe(false); + expect( + await database + .prepare("SELECT driver_instance_id, status FROM session_run WHERE id = ?") + .bind(RUN_ID) + .first(), + ).toEqual({ driver_instance_id: DRIVER_ID, status: "running" }); + }); + + test("a cleanup claim makes the exact Driver generation non-assignable before remote I/O", async () => { + const database = createDatabase(); + let lease = await claimRuntimeRunProvisioningLease(database, { + runId: RUN_ID, + sandboxId: SANDBOX_ID, + sessionId: SESSION_ID, + }); + expect(lease).not.toBeNull(); + if (lease === null) { + return; + } + lease = await insertDurableRunHandoff(database, lease); + await database + .prepare("UPDATE session SET runtime_provisioning_heartbeat_at = 1 WHERE id = ?") + .bind(SESSION_ID) + .run(); + const requestStarted = Promise.withResolvers(); + const response = Promise.withResolvers(); + const bindings = { + DB: database, + DriverConnection: { + get: () => ({ + fetch: async () => { + requestStarted.resolve(); + return response.promise; + }, + }), + idFromName: () => "driver-do-id", + }, + } as unknown as ApiBindings; + expect( + await claimRuntimeProvisioningDriverCleanup(database, { + driverGeneration: 0, + driverInstanceId: DRIVER_ID, + lease, + source: "maintenance", + }), + ).toBe(true); + const stopping = stopDriverSession(bindings, { + driverInstanceId: DRIVER_ID, + expectedDriverGeneration: 0, + expectedSessionRunId: RUN_ID, + reason: "runtime.provisioning_stale", + }); + await requestStarted.promise; + + await expect( + database + .prepare( + "SELECT status, status_operation_id, status_source FROM driver_instance WHERE id = ?", + ) + .bind(DRIVER_ID) + .first(), + ).resolves.toEqual({ + status: "stopping", + status_operation_id: expect.any(String), + status_source: "maintenance", + }); + const [maintenance] = await claimStaleRuntimeProvisioningLeases(database, { + heartbeatAtLte: 1, + limit: 1, + }); + expect(maintenance).toBeDefined(); + if (maintenance !== undefined) { + expect(await adoptReadyRuntimeRunProvisioningLease(database, maintenance)).toBe(false); + } + + response.resolve(Response.json({ error: "injected stop failure" }, { status: 500 })); + await expect(stopping).rejects.toThrow("injected stop failure"); + }); + + test("a stale cleanup cannot claim a Driver after maintenance adopts its handoff", async () => { + const database = createDatabase(); + let staleLease = await claimRuntimeRunProvisioningLease(database, { + runId: RUN_ID, + sandboxId: SANDBOX_ID, + sessionId: SESSION_ID, + }); + expect(staleLease).not.toBeNull(); + if (staleLease === null) { + return; + } + staleLease = await insertDurableRunHandoff(database, staleLease); + const targets = await readRuntimeProvisioningCleanupTargets(database, staleLease); + const [driver] = targets?.driverInstances ?? []; + expect(driver).toBeDefined(); + if (driver === undefined) { + return; + } + await database + .prepare("UPDATE session SET runtime_provisioning_heartbeat_at = 1 WHERE id = ?") + .bind(SESSION_ID) + .run(); + const [maintenance] = await claimStaleRuntimeProvisioningLeases(database, { + heartbeatAtLte: 1, + limit: 1, + }); + expect(maintenance).toBeDefined(); + if (maintenance === undefined) { + return; + } + expect(await adoptReadyRuntimeRunProvisioningLease(database, maintenance)).toBe(true); + + expect( + await claimRuntimeProvisioningDriverCleanup(database, { + driverGeneration: driver.generation, + driverInstanceId: driver.id, + lease: staleLease, + source: "maintenance", + }), + ).toBe(false); + await expect( + database + .prepare("SELECT status, status_operation_id FROM driver_instance WHERE id = ?") + .bind(DRIVER_ID) + .first(), + ).resolves.toEqual({ status: "ready", status_operation_id: null }); + await expect( + database + .prepare("SELECT driver_instance_id, status FROM session_run WHERE id = ?") + .bind(RUN_ID) + .first(), + ).resolves.toEqual({ driver_instance_id: DRIVER_ID, status: "running" }); + }); + + test("the maintenance entry point releases a crashed post-handoff lease", async () => { + const database = createDatabase(); + let lease = await claimRuntimeRunProvisioningLease(database, { + runId: RUN_ID, + sandboxId: SANDBOX_ID, + sessionId: SESSION_ID, + }); + expect(lease).not.toBeNull(); + if (lease === null) { + return; + } + lease = await insertDurableRunHandoff(database, lease); + await database + .prepare("UPDATE session SET runtime_provisioning_heartbeat_at = 1 WHERE id = ?") + .bind(SESSION_ID) + .run(); + + expect( + await repairStaleRuntimeProvisioningLeases({ DB: database } as ApiBindings, { + heartbeatAtLte: 1, + limit: 1, + }), + ).toBe(1); + expect( + await database + .prepare("SELECT runtime_provisioning_operation_id FROM session WHERE id = ?") + .bind(SESSION_ID) + .first(), + ).toEqual({ runtime_provisioning_operation_id: null }); + }); + + test("maintenance takeover revokes the old holder before clearing an incomplete attempt", async () => { + const database = createDatabase(); + const lease = await claimRuntimeRunProvisioningLease(database, { + runId: RUN_ID, + sandboxId: SANDBOX_ID, + sessionId: SESSION_ID, + }); + expect(lease).not.toBeNull(); + if (lease === null) { + return; + } + await database + .prepare("UPDATE session SET runtime_provisioning_heartbeat_at = 1 WHERE id = ?") + .bind(SESSION_ID) + .run(); + + const [maintenance] = await claimStaleRuntimeProvisioningLeases(database, { + heartbeatAtLte: 1, + limit: 1, + }); + expect(maintenance).toBeDefined(); + if (maintenance === undefined) { + return; + } + expect(maintenance.operationId).not.toBe(lease.operationId); + expect(await heartbeatRuntimeRunProvisioningLease(database, lease)).toBe(false); + expect(await releaseAbortedRuntimeProvisioningLease(database, lease)).toBe(false); + expect(await releaseAbortedRuntimeProvisioningLease(database, maintenance)).toBe(true); + + const nextLease = await claimRuntimeRunProvisioningLease(database, { + runId: RUN_ID, + sandboxId: SANDBOX_ID, + sessionId: SESSION_ID, + }); + expect(nextLease).not.toBeNull(); + expect(await renewRuntimeProvisioningLeaseOwnership(database, lease)).toBe(false); + if (nextLease !== null) { + expect(await heartbeatRuntimeRunProvisioningLease(database, nextLease)).toBe(true); + } + }); + + test("a late old cleanup cannot close the next owner's conversation", async () => { + const database = createDatabase(); + let lease = await claimRuntimeRunProvisioningLease(database, { + runId: RUN_ID, + sandboxId: SANDBOX_ID, + sessionId: SESSION_ID, + }); + expect(lease).not.toBeNull(); + if (lease === null) { + return; + } + await database + .prepare( + `INSERT INTO sandbox_session + (cloudflare_session_id, sandbox_id, session_id, status) + VALUES (?, ?, ?, 'active')`, + ) + .bind(OLD_CONVERSATION_ID, SANDBOX_ID, SESSION_ID) + .run(); + const targetedLease = await recordRuntimeProvisioningConversationTarget(database, { + lease, + sandboxIncarnation: 1, + sandboxSessionId: OLD_CONVERSATION_ID, + }); + expect(targetedLease).not.toBeNull(); + if (targetedLease === null) { + return; + } + lease = targetedLease; + expect(await readRuntimeProvisioningCleanupTargets(database, lease)).toMatchObject({ + conversationSessionId: OLD_CONVERSATION_ID, + }); + await database + .prepare("UPDATE session SET runtime_provisioning_heartbeat_at = 1 WHERE id = ?") + .bind(SESSION_ID) + .run(); + + const [maintenance] = await claimStaleRuntimeProvisioningLeases(database, { + heartbeatAtLte: 1, + limit: 1, + }); + expect(maintenance).toBeDefined(); + if (maintenance === undefined) { + return; + } + expect(await releaseAbortedRuntimeProvisioningLease(database, maintenance)).toBe(true); + const nextLease = await claimRuntimeRunProvisioningLease(database, { + runId: RUN_ID, + sandboxId: SANDBOX_ID, + sessionId: SESSION_ID, + }); + expect(nextLease).not.toBeNull(); + await database + .prepare( + `UPDATE sandbox_session + SET cloudflare_session_id = ?, status = 'active' + WHERE session_id = ?`, + ) + .bind(NEW_CONVERSATION_ID, SESSION_ID) + .run(); + + await recordRuntimeConversationSessionClosed(database, { + expectedProvisioningOperationId: lease.operationId, + inactiveDeadlineAt: 100, + now: 10, + runtimeSubjectId: SANDBOX_ID, + sandboxSessionId: OLD_CONVERSATION_ID, + sessionId: SESSION_ID, + }); + + expect( + await database + .prepare("SELECT cloudflare_session_id, status FROM sandbox_session WHERE session_id = ?") + .bind(SESSION_ID) + .first(), + ).toEqual({ cloudflare_session_id: NEW_CONVERSATION_ID, status: "active" }); + if (nextLease !== null) { + expect(await heartbeatRuntimeRunProvisioningLease(database, nextLease)).toBe(true); + } + }); +}); diff --git a/apps/api/tests/runtime-session-link.test.ts b/apps/api/tests/runtime-session-link.test.ts index 8fc36fa3..7b11ec1d 100644 --- a/apps/api/tests/runtime-session-link.test.ts +++ b/apps/api/tests/runtime-session-link.test.ts @@ -18,8 +18,11 @@ function createRuntimeSessionLinkDatabase(): SqliteD1Database { id text PRIMARY KEY NOT NULL, session_id text NOT NULL, created_by_account_id text NOT NULL, + created_at integer NOT NULL, + runtime_id text, trace_id text, - status text NOT NULL + status text NOT NULL, + updated_at integer NOT NULL ); CREATE TABLE session ( @@ -27,6 +30,7 @@ function createRuntimeSessionLinkDatabase(): SqliteD1Database { agent_id text NOT NULL, app_id text, creator_account_id text NOT NULL, + runtime_id text NOT NULL, type text DEFAULT 'preview' NOT NULL ); @@ -49,11 +53,20 @@ function createRuntimeSessionLinkDatabase(): SqliteD1Database { INSERT INTO agent (id, owner_account_id) VALUES ('01J00000000000000000000009', '01J00000000000000000000001'); - INSERT INTO session (id, agent_id, creator_account_id) - VALUES ('session-1', '01J00000000000000000000009', 'creator-1'); - - INSERT INTO session_run (driver_instance_id, id, session_id, created_by_account_id, trace_id, status) - VALUES ('driver-1', 'run-1', 'session-1', 'caller-1', 'trace-1', 'running'); + INSERT INTO session (id, agent_id, creator_account_id, runtime_id) + VALUES ('session-1', '01J00000000000000000000009', 'creator-1', 'runtime-1'); + + INSERT INTO session_run ( + driver_instance_id, + id, + session_id, + created_by_account_id, + created_at, + trace_id, + status, + updated_at + ) + VALUES ('driver-1', 'run-1', 'session-1', 'caller-1', 1, 'trace-1', 'running', 1); INSERT INTO sandbox (id, kind, subject_kind) VALUES ('01J0000000000000000000000D', 'pet', 'agent'); diff --git a/apps/api/tests/runtime-session-outputs.test.ts b/apps/api/tests/runtime-session-outputs.test.ts index 1e71ac03..32868da2 100644 --- a/apps/api/tests/runtime-session-outputs.test.ts +++ b/apps/api/tests/runtime-session-outputs.test.ts @@ -1,213 +1,138 @@ import { describe, expect, test } from "bun:test"; +import { createPlatformId } from "@mosoo/id"; +import type { RuntimeEventId } from "@mosoo/id"; import { createRuntimeEvent } from "@mosoo/runtime-events"; -import { createBaseLiveState } from "../src/modules/runtime/infrastructure/driver-instance/event-projection"; +import { fileStore } from "../src/modules/files/application/file-store"; import type { RuntimeSessionLink } from "../src/modules/runtime/infrastructure/driver-instance/event-types"; -import { appRuntimeDriverEvents } from "../src/modules/runtime/infrastructure/driver-instance/events"; +import { DriverInstanceRpcEventIngestionController } from "../src/modules/runtime/infrastructure/driver-instance/rpc-event-ingestion-controller"; import { getRuntimeSessionOutputDirectory, normalizeRuntimeSessionOutputRelativePath, - readRuntimeSessionOutputListing, + readRuntimeSessionOutputInventory, toRuntimeSessionOutputFile, } from "../src/modules/runtime/infrastructure/driver-instance/runtime-session-outputs"; -import type { SandboxHandle } from "../src/modules/runtime/infrastructure/sandbox-handles"; +import { RuntimeSessionViewCache } from "../src/modules/runtime/infrastructure/driver-instance/runtime-session-view-cache"; import type { ApiBindings } from "../src/platform/cloudflare/worker-types"; -import { API_DRIVER_BOUNDARY_IDS } from "./api-driver-boundary-fixtures"; import { PUBLIC_API_TEST_IDS, PublicApiMemoryFileBucket, createPublicHttpContractDatabase, createPublicHttpTestBindings, + insertActiveSandboxSessionFixture, insertOwnerSession, nowMsForTest, } from "./helpers/public-api-http-test-fixture"; +import { createRuntimeOutputSandbox } from "./helpers/runtime-output-sandbox"; -const encoder = new TextEncoder(); - -function encodeBase64(value: string): string { - const bytes = encoder.encode(value); - let binary = ""; - - for (const byte of bytes) { - binary += String.fromCodePoint(byte); - } - - return btoa(binary); -} - -function createSandboxHandle(files: ReadonlyMap): SandboxHandle { - const unavailable = async () => { - throw new Error("Unexpected sandbox test method call."); - }; - const successfulCommand: SandboxHandle["exec"] = async (command) => { - if (command.includes("find . -type f")) { - const outputDir = "/workspace/session/outputs/"; - const output = [...files.keys()] - .filter((path) => path.startsWith(outputDir)) - .map((path) => path.slice(outputDir.length)) - .toSorted() - .join("\n"); - - return { - exitCode: 0, - stderr: "", - stdout: output.length === 0 ? "" : `${output}\n`, - success: true, - }; - } - - return { - exitCode: 0, - stderr: "", - stdout: "", - success: true, - }; - }; - const readFile: SandboxHandle["readFile"] = async (path) => { - const content = files.get(path); - - if (content === undefined) { - throw new Error(`Missing sandbox test file: ${path}`); - } - - return { - content: encodeBase64(content), - encoding: "base64", - }; - }; - - return { - configureNetworkConstraints: unavailable, - createBackup: unavailable, - createSession: unavailable, - deleteSession: unavailable, - destroy: unavailable, - exec: successfulCommand, - getSession: async () => ({ - exec: successfulCommand, - mkdir: unavailable, - readFile, - startProcess: unavailable, - watch: unavailable, - writeFile: unavailable, - }), - mkdir: unavailable, - mountBucket: unavailable, - readFile, - restoreBackup: unavailable, - setKeepAlive: unavailable, - startProcess: unavailable, - terminal: unavailable, - unmountBucket: unavailable, - watch: unavailable, - writeFile: unavailable, - wsConnect: unavailable, - }; -} - -async function insertActiveSandboxSession(database: D1Database): Promise { +async function insertActiveRuntime(database: D1Database): Promise { + const now = nowMsForTest(); await database .prepare( - ` - INSERT INTO sandbox_session ( - cloudflare_session_id, - created_at, - cwd, - origin_json, - sandbox_id, - session_id, - status, - updated_at - ) - VALUES (?, ?, ?, ?, ?, ?, ?, ?) - `, + `INSERT INTO driver_instance ( + id, sandbox_id, sandbox_incarnation, sandbox_session_id, runtime, protocol, protocol_version, + status, status_changed_at, status_event, status_seq, status_source, + connection_id, command_seq_cursor, boot_token_hash, boot_token_expires_at, + generation, heartbeat_count, restart_count, expires_at, created_at, updated_at + ) VALUES (?, ?, 1, ?, 'openai-runtime', 'acp', 3, 'ready', ?, 'driver.ready', 1, + 'driver', 'connection-1', 0, X'00', ?, 1, 1, 0, ?, ?, ?)`, ) .bind( - API_DRIVER_BOUNDARY_IDS.sandboxSession, - nowMsForTest(), - "/workspace/session", - "{}", + PUBLIC_API_TEST_IDS.driverOwner, PUBLIC_API_TEST_IDS.sandbox, PUBLIC_API_TEST_IDS.ownerSession, - "active", - nowMsForTest(), + now, + now + 60_000, + now + 60_000, + now, + now, ) .run(); -} - -function createRuntimeLink(): RuntimeSessionLink { - return { - agentId: PUBLIC_API_TEST_IDS.agent, - appId: PUBLIC_API_TEST_IDS.app, - callerId: PUBLIC_API_TEST_IDS.ownerAccount, - creatorId: PUBLIC_API_TEST_IDS.ownerAccount, - executionOwnerId: PUBLIC_API_TEST_IDS.ownerAccount, - sandboxId: PUBLIC_API_TEST_IDS.sandbox, - sandboxKind: "cattle", - sandboxSubjectKind: "session", - sessionId: PUBLIC_API_TEST_IDS.ownerSession, - sessionRunId: PUBLIC_API_TEST_IDS.run, - sessionRunStatus: "running", - sessionType: "ui", - traceId: "trace-session-outputs", - }; + await database + .prepare( + `INSERT INTO session_run ( + id, session_id, agent_id, created_by_account_id, driver_instance_id, + trigger, status, provider, model, runtime_id, trace_id, created_at, + status_changed_at, status_event, status_seq, status_source, updated_at + ) VALUES (?, ?, ?, ?, ?, 'user_prompt', 'running', 'openai', 'gpt-5.4', + 'openai-runtime', 'trace-session-outputs', ?, ?, 'run.start', 1, 'driver', ?)`, + ) + .bind( + PUBLIC_API_TEST_IDS.run, + PUBLIC_API_TEST_IDS.ownerSession, + PUBLIC_API_TEST_IDS.agent, + PUBLIC_API_TEST_IDS.ownerAccount, + PUBLIC_API_TEST_IDS.driverOwner, + now, + now, + now, + ) + .run(); + await database + .prepare( + "UPDATE session SET last_run_id = ?, status = 'RUNNING', status_operation_id = NULL WHERE id = ?", + ) + .bind(PUBLIC_API_TEST_IDS.run, PUBLIC_API_TEST_IDS.ownerSession) + .run(); } function createCompletedRunEvent() { return createRuntimeEvent({ driverInstanceId: PUBLIC_API_TEST_IDS.driverOwner, - id: API_DRIVER_BOUNDARY_IDS.runtimeEvent, + id: createPlatformId(), kind: "run.completed", - occurredAt: "2026-06-22T00:00:00.000Z", - payload: { - run: { - completedAt: "2026-06-22T00:00:00.000Z", - error: null, - startedAt: null, - status: "completed", - }, - }, + occurredAt: "2026-06-22T00:00:01.000Z", + payload: { stopReason: "end_turn" }, runId: PUBLIC_API_TEST_IDS.run, + runtimeId: "openai-runtime", sessionId: PUBLIC_API_TEST_IDS.ownerSession, }); } -function createFileChangedEvent(path = "outputs/live.txt") { +interface TestFileChange { + readonly change: "delete" | "upsert"; + readonly metadata?: { readonly contentType?: string }; + readonly path: string; +} + +function createFileChangedEvent(...changes: readonly TestFileChange[]) { return createRuntimeEvent({ driverInstanceId: PUBLIC_API_TEST_IDS.driverOwner, - id: API_DRIVER_BOUNDARY_IDS.runtimeEvent, + id: createPlatformId(), kind: "file.changed", occurredAt: "2026-06-22T00:00:00.000Z", - payload: { - changes: [ - { - change: "upsert", - metadata: { contentType: "text/plain" }, - path, - }, - { - change: "upsert", - path: "src/temp.txt", - }, - ], - }, + payload: { changes }, runId: PUBLIC_API_TEST_IDS.run, + runtimeId: "openai-runtime", sessionId: PUBLIC_API_TEST_IDS.ownerSession, }); } -async function createBindings(input: { - database: D1Database; - files?: ReadonlyMap; -}): Promise<{ bindings: ApiBindings; bucket: PublicApiMemoryFileBucket }> { +function fileUpsert(path: string, contentType = "text/plain"): TestFileChange { + return { change: "upsert", metadata: { contentType }, path }; +} + +function createBindings(input: { database: D1Database; files?: ReadonlyMap }): { + bindings: ApiBindings; + bucket: PublicApiMemoryFileBucket; +} { const bucket = new PublicApiMemoryFileBucket(); - const sandbox = createSandboxHandle(input.files ?? new Map()); + const sandbox = createRuntimeOutputSandbox({ + files: input.files, + onExec: (command) => { + if (command.includes("find . -type f")) { + expect(command.startsWith("bash -lc ")).toBe(true); + expect(command.indexOf("head -z")).toBeLessThan(command.indexOf("sort -z")); + } + }, + root: "/workspace/session/outputs", + }); return { bindings: { ...createPublicHttpTestBindings(input.database, { - fileBucket: bucket as unknown as R2Bucket, + fileBucket: bucket, }), runtimeSubjectHandleFactory: () => sandbox, } as ApiBindings, @@ -215,41 +140,125 @@ async function createBindings(input: { }; } +const activeContext = { + assertActiveConnection: () => undefined, + connectionId: "connection-1", +} as never; + +function createController(bindings: ApiBindings): DriverInstanceRpcEventIngestionController { + const state = { + hello: { pid: 1 }, + requireDriverGeneration: () => 1, + requireDriverInstanceId: () => PUBLIC_API_TEST_IDS.driverOwner, + runtimeSessionLink: null as RuntimeSessionLink | null, + setRuntimeSessionLink(link: RuntimeSessionLink) { + this.runtimeSessionLink = link; + }, + }; + + return new DriverInstanceRpcEventIngestionController({ + env: bindings, + state, + viewCache: new RuntimeSessionViewCache(), + viewerEventDelivery: { + enqueue: () => undefined, + flush: async () => undefined, + flushSafely: async () => undefined, + requestStateSync: () => undefined, + resetAfterFlush: () => undefined, + }, + } as never); +} + +async function createRuntimeSessionOutputFixture( + files: ReadonlyMap = new Map(), +): Promise<{ + readonly bindings: ApiBindings; + readonly bucket: PublicApiMemoryFileBucket; + readonly database: D1Database; +}> { + const database = await createPublicHttpContractDatabase(); + await insertOwnerSession(database); + await insertActiveSandboxSessionFixture(database, { + cwd: "/workspace/session", + ownerAccountId: PUBLIC_API_TEST_IDS.ownerAccount, + sandboxId: PUBLIC_API_TEST_IDS.sandbox, + sessionId: PUBLIC_API_TEST_IDS.ownerSession, + }); + await insertActiveRuntime(database); + + return { database, ...createBindings({ database, files }) }; +} + async function dispatchRuntimeEvent(input: { bindings: ApiBindings; event: ReturnType; - link: RuntimeSessionLink; }): Promise { - await appRuntimeDriverEvents(input.bindings, { - currentLiveState: createBaseLiveState({ - callerId: input.link.callerId, - creatorId: input.link.creatorId, + const eventId = input.event.sourceEventId ?? input.event.id; + + await createController(input.bindings).handlePushEvents( + { driverInstanceId: PUBLIC_API_TEST_IDS.driverOwner, - sessionId: input.link.sessionId, - }), - driverInstanceId: PUBLIC_API_TEST_IDS.driverOwner, - events: [ - { - event: input.event, - eventId: "source-runtime-session-outputs", - occurredAt: 1, - }, - ], - link: input.link, - }); + events: [{ event: input.event, eventId, occurredAt: input.event.occurredAt }], + }, + activeContext, + ); } describe("runtime session outputs", () => { + test("preserves exact binary bytes in the memory R2 fixture", async () => { + const bucket = new PublicApiMemoryFileBucket(); + const source = Uint8Array.of(0xaa, 0xff, 0, 0x80, 0x41, 0xbb); + const expected = Uint8Array.of(0xff, 0, 0x80, 0x41); + const customMetadata = { contentSha256: "binary-hash", sourcePath: "outputs/data.bin" }; + + const stored = await bucket.put("binary", source.subarray(1, -1), { + customMetadata, + httpMetadata: { contentType: "application/octet-stream" }, + }); + source.fill(0); + const body = await bucket.get("binary"); + + expect(stored).toMatchObject({ customMetadata, size: expected.byteLength }); + expect(body).toMatchObject({ customMetadata, size: expected.byteLength }); + expect(new Uint8Array(await body!.arrayBuffer())).toEqual(expected); + }); + + test.each([ + ["If-None-Match", false], + ["If-Match", true], + ] as const)("serializes concurrent %s writes in the memory R2 fixture", async (name, seeded) => { + const bucket = new PublicApiMemoryFileBucket(); + const key = "conditional"; + const previous = seeded ? await bucket.put(key, "previous") : null; + const onlyIf = new Headers({ [name]: seeded ? previous!.httpEtag : "*" }); + const writes = await Promise.all([ + bucket.put(key, "first", { onlyIf }), + bucket.put(key, "second", { onlyIf }), + ]); + const winner = writes.find((object) => object !== null); + const stored = await bucket.get(key); + + expect(writes.filter((object) => object !== null)).toHaveLength(1); + expect(stored?.etag).toBe(winner?.etag); + expect(["first", "second"]).toContain(await stored?.text()); + }); + test("normalizes the session output directory contract", () => { expect(getRuntimeSessionOutputDirectory("/workspace/session")).toBe( "/workspace/session/outputs", ); - expect(normalizeRuntimeSessionOutputRelativePath("a/./b.txt")).toBe("a/b.txt"); + expect(normalizeRuntimeSessionOutputRelativePath("a/b.txt")).toBe("a/b.txt"); + expect(normalizeRuntimeSessionOutputRelativePath("a/./b.txt")).toBeNull(); expect(normalizeRuntimeSessionOutputRelativePath("../b.txt")).toBeNull(); expect(normalizeRuntimeSessionOutputRelativePath("/tmp/b.txt")).toBeNull(); - expect(readRuntimeSessionOutputListing("b.txt\n../secret.txt\nnested/a.pdf\n")).toEqual([ - "b.txt", - "nested/a.pdf", + expect( + readRuntimeSessionOutputInventory( + ["./b.txt", "12", "./nested/report.pdf", "7", ""].join("\0"), + ), + ).toEqual([ + { relativePath: "b.txt", size: 12 }, + { relativePath: "nested/report.pdf", size: 7 }, ]); expect( toRuntimeSessionOutputFile({ @@ -276,24 +285,39 @@ describe("runtime session outputs", () => { ).toBeNull(); }); - test("records files under outputs as session artifacts on run completion", async () => { - const database = await createPublicHttpContractDatabase(); - await insertOwnerSession(database); - await insertActiveSandboxSession(database); + test.each([ + ["control character", "nested/tab\tline\n.txt"], + ["trailing control character", "nested/report.txt\n"], + ["backslash", "nested/back\\slash.txt"], + ["non-canonical whitespace", "nested/ report.txt"], + ["encoded traversal", "nested/%2e%2e/report.txt"], + ["dot alias", "nested/./report.txt"], + ["empty segment", "nested//report.txt"], + ] as const)("rejects a %s that file records cannot represent", (_name, path) => { + expect(normalizeRuntimeSessionOutputRelativePath(path)).toBeNull(); + expect(() => readRuntimeSessionOutputInventory([`./${path}`, "1", ""].join("\0"))).toThrow( + "Runtime output inventory is invalid.", + ); + expect( + toRuntimeSessionOutputFile({ cwd: "/workspace/session", path: `outputs/${path}` }), + ).toBeNull(); + }); - const { bindings, bucket } = await createBindings({ - database, - files: new Map([ + test("records files under outputs as session artifacts from file changes", async () => { + const { bindings, bucket, database } = await createRuntimeSessionOutputFixture( + new Map([ ["/workspace/session/outputs/resume.txt", "Improved resume"], ["/workspace/session/outputs/nested/summary.md", "# Summary"], ]), - }); - const link = createRuntimeLink(); + ); await dispatchRuntimeEvent({ bindings, - event: createCompletedRunEvent(), - link, + event: createFileChangedEvent(fileUpsert("outputs/resume.txt")), + }); + await dispatchRuntimeEvent({ + bindings, + event: createFileChangedEvent(fileUpsert("outputs/nested/summary.md", "text/markdown")), }); const rows = await database @@ -341,23 +365,144 @@ describe("runtime session outputs", () => { size: 9, }, ]); - expect([...bucket.objects.values()]).toHaveLength(2); + expect( + [...bucket.objects.values()].filter((object) => !object.key.endsWith("/manifest.json")), + ).toHaveLength(2); }); - test("deduplicates runtime outputs by source path and content", async () => { - const database = await createPublicHttpContractDatabase(); - await insertOwnerSession(database); - await insertActiveSandboxSession(database); + test("atomically replaces delta artifact heads with the completed Run snapshot", async () => { + const files = new Map([ + ["/workspace/session/outputs/current.txt", "delta version"], + ["/workspace/session/outputs/obsolete.txt", "removed before completion"], + ]); + const { bindings, bucket, database } = await createRuntimeSessionOutputFixture(files); + + await dispatchRuntimeEvent({ + bindings, + event: createFileChangedEvent(fileUpsert("outputs/current.txt")), + }); + await dispatchRuntimeEvent({ + bindings, + event: createFileChangedEvent(fileUpsert("outputs/obsolete.txt")), + }); + + files.set("/workspace/session/outputs/current.txt", "terminal version"); + files.delete("/workspace/session/outputs/obsolete.txt"); + await dispatchRuntimeEvent({ bindings, event: createCompletedRunEvent() }); + + const heads = await database + .prepare( + `SELECT h.file_id, h.runtime_event_seq, h.source_path, f.object_key + FROM session_artifact_head AS h + LEFT JOIN file_record AS f ON f.id = h.file_id + WHERE h.session_id = ? + ORDER BY h.source_path`, + ) + .bind(PUBLIC_API_TEST_IDS.ownerSession) + .all<{ + file_id: string | null; + object_key: string | null; + runtime_event_seq: number; + source_path: string; + }>(); + + expect( + heads.results.map(({ file_id, runtime_event_seq, source_path }) => ({ + file_id: file_id === null ? null : "present", + runtime_event_seq, + source_path, + })), + ).toEqual([ + { + file_id: "present", + runtime_event_seq: 3, + source_path: "outputs/current.txt", + }, + { + file_id: null, + runtime_event_seq: 3, + source_path: "outputs/obsolete.txt", + }, + ]); + expect(await (await bucket.get(heads.results[0]?.object_key ?? ""))?.text()).toBe( + "terminal version", + ); + expect( + await database + .prepare( + `SELECT count(*) AS count + FROM runtime_artifact_attempt + WHERE status = 'accepted' AND owned_object_keys_json = '[]'`, + ) + .first(), + ).toEqual({ count: 3 }); + }); + + test("does not resurrect a late headless legacy artifact after an empty snapshot", async () => { + const { bindings, database } = await createRuntimeSessionOutputFixture(); + const staleFileId = createPlatformId(); + await database + .prepare( + `INSERT INTO file_record ( + committed, created_at, created_by_account_id, id, name, object_key, + owner_id, owner_kind, parent_path, path, purpose, scope_id, scope_kind, + session_kind, size, status, updated_at, version + ) VALUES ( + 1, ?, ?, ?, 'stale.txt', ?, ?, 'session', ?, ?, 'session_artifact', + ?, 'session', 'artifact', 5, 'ready', ?, 1 + )`, + ) + .bind( + nowMsForTest(), + PUBLIC_API_TEST_IDS.ownerAccount, + staleFileId, + `objects/${staleFileId}`, + PUBLIC_API_TEST_IDS.ownerSession, + `runtime-output/outputs/stale.txt/${"a".repeat(64)}`, + `session-artifacts/${staleFileId}/stale.txt`, + PUBLIC_API_TEST_IDS.ownerSession, + nowMsForTest(), + ) + .run(); + await dispatchRuntimeEvent({ + bindings, + event: createCompletedRunEvent(), + }); + + await expect( + database + .prepare( + `SELECT json_extract(manifest_json, '$.captureStatus') AS capture_status, + json_extract(manifest_json, '$.mode') AS mode, + status + FROM runtime_artifact_attempt`, + ) + .first(), + ).resolves.toEqual({ capture_status: "complete", mode: "snapshot", status: "accepted" }); + expect( + await fileStore.listReadySessionFiles(database, PUBLIC_API_TEST_IDS.ownerSession), + ).toEqual([]); + expect( + await fileStore.listLatestReadySessionArtifactSources( + database, + PUBLIC_API_TEST_IDS.ownerSession, + ), + ).toEqual([]); + await expect( + database + .prepare("SELECT count(*) AS count FROM file_record WHERE id = ?") + .bind(staleFileId) + .first(), + ).resolves.toEqual({ count: 1 }); + }); + + test("deduplicates runtime outputs by source path and content", async () => { const files = new Map([ ["/workspace/session/outputs/one/report.txt", "alpha"], ["/workspace/session/outputs/two/report.txt", "bravo"], ]); - const { bindings } = await createBindings({ - database, - files, - }); - const link = createRuntimeLink(); + const { bindings, database } = await createRuntimeSessionOutputFixture(files); const readReportCount = async () => { const row = await database .prepare( @@ -370,14 +515,12 @@ describe("runtime session outputs", () => { await dispatchRuntimeEvent({ bindings, - event: createFileChangedEvent("outputs/one/report.txt"), - link, + event: createFileChangedEvent(fileUpsert("outputs/one/report.txt")), }); await dispatchRuntimeEvent({ bindings, - event: createCompletedRunEvent(), - link, + event: createFileChangedEvent(fileUpsert("outputs/two/report.txt")), }); expect(await readReportCount()).toBe(2); @@ -386,64 +529,65 @@ describe("runtime session outputs", () => { await dispatchRuntimeEvent({ bindings, - event: createFileChangedEvent("outputs/one/report.txt"), - link, + event: createFileChangedEvent(fileUpsert("outputs/one/report.txt")), }); expect(await readReportCount()).toBe(3); }); - test("records file change events only when the path is under outputs", async () => { - const database = await createPublicHttpContractDatabase(); - await insertOwnerSession(database); - await insertActiveSandboxSession(database); + test.each([ + ["relative output path", "outputs/live.txt", [{ name: "live.txt", size: 11 }]], + ["external relative path", "src/temp.txt", []], + [ + "absolute output path", + "/workspace/session/outputs/live.txt", + [{ name: "live.txt", size: 11 }], + ], + ["external absolute path", "/workspace/session/src/temp.txt", []], + ["output traversal", "outputs/../src/temp.txt", []], + ] as const)( + "keeps artifact writes inside outputs: %s", + async (_label, eventPath, expectedRows) => { + const { bindings, bucket, database } = await createRuntimeSessionOutputFixture( + new Map([ + ["/workspace/session/outputs/live.txt", "download me"], + ["/workspace/session/src/temp.txt", "ignore me"], + ]), + ); + + await dispatchRuntimeEvent({ + bindings, + event: createFileChangedEvent(fileUpsert(eventPath)), + }); + + const rows = await database + .prepare("SELECT name, size FROM file_record WHERE session_kind = 'artifact'") + .all<{ name: string; size: number }>(); + const artifactObjects = [...bucket.objects.values()].filter( + (object) => !object.key.endsWith("/manifest.json"), + ); + + expect(rows.results).toEqual(expectedRows); + expect(artifactObjects).toHaveLength(expectedRows.length); + }, + ); - const { bindings } = await createBindings({ - database, - files: new Map([ - ["/workspace/session/outputs/live.txt", "download me"], - ["/workspace/session/src/temp.txt", "ignore me"], - ]), - }); - const link = createRuntimeLink(); + test("replays an artifact receipt without reopening its sandbox path", async () => { + const files = new Map([["/workspace/session/outputs/result.txt", "R1 result"]]); + const { bindings, bucket, database } = await createRuntimeSessionOutputFixture(files); + const event = createFileChangedEvent(fileUpsert("outputs/result.txt")); - await dispatchRuntimeEvent({ - bindings, - event: createFileChangedEvent(), - link, - }); + await dispatchRuntimeEvent({ bindings, event }); + files.set("/workspace/session/outputs/result.txt", "R2 result"); + files.set("/workspace/session/outputs/later.txt", "created by R2"); + await dispatchRuntimeEvent({ bindings, event }); const rows = await database - .prepare("SELECT name, size FROM file_record WHERE session_kind = 'artifact'") - .all<{ name: string; size: number }>(); - - expect(rows.results).toEqual([ - { - name: "live.txt", - size: 11, - }, - ]); - }); - - test("skips optional output directory when it has no files", async () => { - const database = await createPublicHttpContractDatabase(); - await insertOwnerSession(database); - await insertActiveSandboxSession(database); - - const { bindings, bucket } = await createBindings({ database }); - const link = createRuntimeLink(); - - await dispatchRuntimeEvent({ - bindings, - event: createCompletedRunEvent(), - link, - }); - - const row = await database - .prepare("SELECT count(*) AS count FROM file_record") - .first<{ count: number }>(); - - expect(row?.count).toBe(0); - expect([...bucket.objects.values()]).toEqual([]); + .prepare( + "SELECT name, object_key FROM file_record WHERE session_kind = 'artifact' ORDER BY name", + ) + .all<{ name: string; object_key: string }>(); + expect(rows.results.map(({ name }) => name)).toEqual(["result.txt"]); + expect(await (await bucket.get(rows.results[0]?.object_key ?? ""))?.text()).toBe("R1 result"); }); }); diff --git a/apps/api/tests/runtime-state-operation-execution.test.ts b/apps/api/tests/runtime-state-operation-execution.test.ts index c11e2499..c1712834 100644 --- a/apps/api/tests/runtime-state-operation-execution.test.ts +++ b/apps/api/tests/runtime-state-operation-execution.test.ts @@ -39,4 +39,44 @@ describe("runtime state operation execution", () => { expect(startedRuntimeSubjectIds).toEqual(["01J0000000000000000000000D", "sandbox-2"]); expect(operationIds).toEqual(["01J0000000000000000000000R", "01J0000000000000000000000R"]); }); + + test("joins every started subject before exposing a sibling failure", async () => { + const delayed = Promise.withResolvers(); + let delayedSubjectFinished = false; + let operationRejected = false; + const plane: RuntimeStateOperationExecutionPlane = { + async recreateSubjectPreservingState() { + throw new Error("Unexpected recreate operation."); + }, + async resetSubjectAgentState() { + throw new Error("Unexpected reset operation."); + }, + async stopSubjectDrivers(_bindings, input) { + if (input.runtimeSubjectId === "failing-subject") { + throw new Error("subject failed"); + } + await delayed.promise; + delayedSubjectFinished = true; + }, + }; + const operation = executeRuntimeStateOperationSubjects({} as ApiBindings, { + executionPlane: plane, + operation: "restartDriver", + operationId: "01J0000000000000000000000R", + subjects: [ + { runtimeSubjectId: "failing-subject", targets: [] }, + { runtimeSubjectId: "delayed-subject", targets: [] }, + ], + }).catch((error: unknown) => { + operationRejected = true; + throw error; + }); + + await Promise.resolve(); + expect(operationRejected).toBe(false); + delayed.resolve(); + + await expect(operation).rejects.toThrow("subject failed"); + expect(delayedSubjectFinished).toBe(true); + }); }); diff --git a/apps/api/tests/runtime-state-operation-phases.test.ts b/apps/api/tests/runtime-state-operation-phases.test.ts index 5a785773..853dbbeb 100644 --- a/apps/api/tests/runtime-state-operation-phases.test.ts +++ b/apps/api/tests/runtime-state-operation-phases.test.ts @@ -1,28 +1,42 @@ import { describe, expect, test } from "bun:test"; +import { createSessionRunTerminalSourceId } from "@mosoo/runtime-events"; + import { completeRuntimeStateOperationPhase, failRuntimeStateOperationPhase, startRuntimeStateOperationPhase, } from "../src/modules/runtime/application/runtime-state-operation-phases"; import type { RuntimeSessionTarget } from "../src/modules/runtime/application/runtime-state-operation-target-store"; +import { recordCanonicalSessionRunTerminal } from "../src/modules/runtime/application/session-runs/session-run-terminal-failure.service"; import type { ApiBindings } from "../src/platform/cloudflare/worker-types"; import { createPublicHttpContractDatabase, createPublicHttpTestBindings, insertNonOwnerSession, + nowMsForTest, } from "./helpers/public-api-http-test-fixture"; function createRuntimeTarget( - input: Omit & { + input: Omit< + RuntimeSessionTarget, + | "sessionRuntimeEventSeqCursor" + | "sessionStatusOperationId" + | "sessionStatusSeq" + | "sessionUpdatedAt" + > & { + readonly sessionRuntimeEventSeqCursor?: number; readonly sessionStatusOperationId?: string | null; readonly sessionStatusSeq?: number; + readonly sessionUpdatedAt?: number; }, ): RuntimeSessionTarget { return { ...input, + sessionRuntimeEventSeqCursor: input.sessionRuntimeEventSeqCursor ?? 0, sessionStatusOperationId: input.sessionStatusOperationId ?? null, sessionStatusSeq: input.sessionStatusSeq ?? 0, + sessionUpdatedAt: input.sessionUpdatedAt ?? nowMsForTest(), }; } @@ -124,7 +138,7 @@ describe("runtime state operation phases", () => { run_status: "cancelled", session_status: "IDLE", session_status_operation_id: null, - session_status_seq: 2, + session_status_seq: 3, }); }); @@ -150,10 +164,15 @@ describe("runtime state operation phases", () => { ], }); - await database - .prepare("UPDATE session_run SET status = ?, status_operation_id = ? WHERE id = ?") - .bind("cancelled", phase.operationId, "01J0000000000000000000000N") - .run(); + await recordCanonicalSessionRunTerminal(bindings, { + assistantMessage: null, + error: null, + expectedSessionOperationId: phase.operationId, + runId: "01J0000000000000000000000N", + sessionId: "01J0000000000000000000000B", + source: "driver", + status: "cancelled", + }); await completeRuntimeStateOperationPhase(bindings, { agentId: "01J00000000000000000000009", @@ -171,12 +190,12 @@ describe("runtime state operation phases", () => { ) .bind( "01J0000000000000000000000B", - `runtime-operation:${phase.operationId}:01J0000000000000000000000N:interrupted`, + createSessionRunTerminalSourceId("01J0000000000000000000000N", "run.cancelled"), ) .first<{ source_event_id: string }>(); expect(event?.source_event_id).toBe( - `runtime-operation:${phase.operationId}:01J0000000000000000000000N:interrupted`, + createSessionRunTerminalSourceId("01J0000000000000000000000N", "run.cancelled"), ); const row = await database @@ -226,10 +245,15 @@ describe("runtime state operation phases", () => { ], }); - await database - .prepare("UPDATE session_run SET status = ?, status_operation_id = ? WHERE id = ?") - .bind("cancelled", phase.operationId, "01J0000000000000000000000N") - .run(); + await recordCanonicalSessionRunTerminal(bindings, { + assistantMessage: null, + error: null, + expectedSessionOperationId: phase.operationId, + runId: "01J0000000000000000000000N", + sessionId: "01J0000000000000000000000B", + source: "driver", + status: "cancelled", + }); await failRuntimeStateOperationPhase(bindings, { agentId: "01J00000000000000000000009", @@ -247,16 +271,16 @@ describe("runtime state operation phases", () => { ) .bind( "01J0000000000000000000000B", - `runtime-operation:${phase.operationId}:01J0000000000000000000000N:interrupted`, + createSessionRunTerminalSourceId("01J0000000000000000000000N", "run.cancelled"), ) .first<{ source_event_id: string }>(); expect(event?.source_event_id).toBe( - `runtime-operation:${phase.operationId}:01J0000000000000000000000N:interrupted`, + createSessionRunTerminalSourceId("01J0000000000000000000000N", "run.cancelled"), ); }); - test("complete does not app terminal runs from another outcome as cancelled", async () => { + test("complete adopts a canonical Driver failure without rewriting its outcome", async () => { const database = await createPublicHttpContractDatabase(); await insertNonOwnerSession(database); await insertRunningSessionRun(database); @@ -278,10 +302,20 @@ describe("runtime state operation phases", () => { ], }); - await database - .prepare("UPDATE session_run SET status = ?, status_operation_id = ? WHERE id = ?") - .bind("completed", phase.operationId, "01J0000000000000000000000N") - .run(); + await recordCanonicalSessionRunTerminal(bindings, { + assistantMessage: null, + error: { + code: "driver.failed", + details: {}, + message: "Driver failed while stopping.", + retryable: true, + }, + expectedSessionOperationId: phase.operationId, + runId: "01J0000000000000000000000N", + sessionId: "01J0000000000000000000000B", + source: "driver", + status: "failed", + }); await completeRuntimeStateOperationPhase(bindings, { agentId: "01J00000000000000000000009", @@ -299,11 +333,16 @@ describe("runtime state operation phases", () => { ) .bind( "01J0000000000000000000000B", - `runtime-operation:${phase.operationId}:01J0000000000000000000000N:interrupted`, + createSessionRunTerminalSourceId("01J0000000000000000000000N", "run.cancelled"), ) .first<{ source_event_id: string }>(); expect(event).toBeNull(); + const run = await database + .prepare("SELECT error_code, status FROM session_run WHERE id = ?") + .bind("01J0000000000000000000000N") + .first<{ error_code: string | null; status: string }>(); + expect(run).toEqual({ error_code: "driver.failed", status: "failed" }); }); test("start ignores stale targets that changed after scope resolution", async () => { @@ -318,7 +357,7 @@ describe("runtime state operation phases", () => { WHERE id = ? `, ) - .bind("run-new", "RUNNING", 1, "01J0000000000000000000000B") + .bind("01J0000000000000000000000Q", "RUNNING", 1, "01J0000000000000000000000B") .run(); const bindings = createPublicHttpTestBindings(database) as ApiBindings; @@ -352,7 +391,7 @@ describe("runtime state operation phases", () => { }>(); expect(row).toEqual({ - last_run_id: "run-new", + last_run_id: "01J0000000000000000000000Q", status: "RUNNING", status_operation_id: null, status_seq: 1, diff --git a/apps/api/tests/runtime-state-operation-scope.test.ts b/apps/api/tests/runtime-state-operation-scope.test.ts index d11660ae..f6bfdf51 100644 --- a/apps/api/tests/runtime-state-operation-scope.test.ts +++ b/apps/api/tests/runtime-state-operation-scope.test.ts @@ -16,10 +16,12 @@ function createRuntimeOperationScopeDatabase(): SqliteD1Database { agent_id text NOT NULL, creator_account_id text NOT NULL, last_run_id text, + runtime_event_seq_cursor integer DEFAULT 0 NOT NULL, status text NOT NULL, status_operation_id text, status_seq integer DEFAULT 0 NOT NULL, - archived_at integer + archived_at integer, + updated_at integer DEFAULT 0 NOT NULL ); CREATE TABLE sandbox ( @@ -134,8 +136,10 @@ function createRuntimeTarget(input: { lastRunId: null, sandboxId: input.sandboxId, sessionId: input.sessionId, + sessionRuntimeEventSeqCursor: 0, sessionStatus: "IDLE", sessionStatusOperationId: null, sessionStatusSeq: 0, + sessionUpdatedAt: 0, }; } diff --git a/apps/api/tests/runtime-state-operation-target-events.test.ts b/apps/api/tests/runtime-state-operation-target-events.test.ts index fb13c738..bdf3ebe5 100644 --- a/apps/api/tests/runtime-state-operation-target-events.test.ts +++ b/apps/api/tests/runtime-state-operation-target-events.test.ts @@ -1,22 +1,37 @@ import { describe, expect, test } from "bun:test"; +import { sandboxSessionsTable } from "@mosoo/db"; +import { + createRuntimeEventSemanticHash, + parseRuntimeEventEnvelope, + stringifyRuntimeEventSemanticValue, +} from "@mosoo/runtime-events"; + import { toRuntimeDiagnosticBaseValue } from "../src/modules/runtime/application/runtime-diagnostic-events"; import { buildRuntimeStateOperationEvents } from "../src/modules/runtime/application/runtime-state-operation-events"; import { appendRuntimeDriverRestartAttemptedEvents, appendRuntimeSubjectTerminatedEvents, - broadcastRuntimeOperationEvent, + commitRuntimeOperationReadySnapshots, writeRuntimeOperationInterruptedSnapshots, writeRuntimeOperationTimedOutSnapshots, } from "../src/modules/runtime/application/runtime-state-operation-target-events"; +import { + adoptRuntimeOperationReadyReceipt, + claimRuntimeOperationTargets, +} from "../src/modules/runtime/application/runtime-state-operation-target-store"; import type { RuntimeSessionTarget } from "../src/modules/runtime/application/runtime-state-operation-target-store"; +import { recordCanonicalSessionRunTerminal } from "../src/modules/runtime/application/session-runs/session-run-terminal-failure.service"; +import { createSessionRuntimeEventProjection } from "../src/modules/sessions/domain/session-runtime-event-projection"; import type { ApiBindings } from "../src/platform/cloudflare/worker-types"; import { PUBLIC_API_TEST_IDS, createPublicHttpContractDatabase, createPublicHttpTestBindings, + insertActiveSandboxSessionFixture, insertNonOwnerSession, insertOwnerSession, + nowMsForTest, } from "./helpers/public-api-http-test-fixture"; async function insertRunningSessionRun( @@ -72,6 +87,82 @@ async function insertRunningSessionRun( .run(); } +async function readRuntimeTarget(database: D1Database): Promise { + const row = await database + .prepare( + `SELECT last_run_id, runtime_event_seq_cursor, status, status_operation_id, + status_seq, updated_at + FROM session + WHERE id = ?`, + ) + .bind(PUBLIC_API_TEST_IDS.nonOwnerSession) + .first<{ + last_run_id: RuntimeSessionTarget["lastRunId"]; + runtime_event_seq_cursor: number; + status: RuntimeSessionTarget["sessionStatus"]; + status_operation_id: RuntimeSessionTarget["sessionStatusOperationId"]; + status_seq: number; + updated_at: number; + }>(); + if (row === null) { + throw new Error("Missing runtime operation target fixture."); + } + return createRuntimeTarget({ + agentId: PUBLIC_API_TEST_IDS.agent, + creatorAccountId: PUBLIC_API_TEST_IDS.nonOwnerAccount, + lastRunId: row.last_run_id, + sandboxId: PUBLIC_API_TEST_IDS.sandbox, + sessionId: PUBLIC_API_TEST_IDS.nonOwnerSession, + sessionRuntimeEventSeqCursor: row.runtime_event_seq_cursor, + sessionStatus: row.status, + sessionStatusOperationId: row.status_operation_id, + sessionStatusSeq: row.status_seq, + sessionUpdatedAt: row.updated_at, + }); +} + +async function rewriteTerminalAuthority( + database: D1Database, + rewrite: (value: Record) => Record, +): Promise { + const row = await database + .prepare( + `SELECT terminal_event_json + FROM session_event + WHERE run_id = ? + AND event_type IN ('run.cancelled', 'run.completed', 'run.failed')`, + ) + .bind(PUBLIC_API_TEST_IDS.run) + .first<{ terminal_event_json: string | null }>(); + if (row?.terminal_event_json === null || row === null) { + throw new Error("Missing terminal semantic authority fixture."); + } + const event = parseRuntimeEventEnvelope(rewrite(JSON.parse(row.terminal_event_json))); + const projection = createSessionRuntimeEventProjection(event); + await database + .prepare( + `UPDATE session_event + SET content_text = ?, semantic_hash = ?, stream_id = ?, terminal_event_json = ? + WHERE run_id = ? + AND event_type IN ('run.cancelled', 'run.completed', 'run.failed')`, + ) + .bind( + projection.contentText, + await createRuntimeEventSemanticHash(event), + projection.streamId, + stringifyRuntimeEventSemanticValue(event), + PUBLIC_API_TEST_IDS.run, + ) + .run(); +} + +function requireRecord(value: unknown): Record { + if (typeof value !== "object" || value === null || Array.isArray(value)) { + throw new Error("Expected a record fixture value."); + } + return value as Record; +} + function createRuntimeDiagnosticTargets(): RuntimeSessionTarget[] { return [ createRuntimeTarget({ @@ -106,6 +197,7 @@ async function insertLiveDriverInstance( input: { driverInstanceId: string; sessionId: string; + tokenByte: number; }, ): Promise { await database @@ -114,6 +206,7 @@ async function insertLiveDriverInstance( INSERT INTO driver_instance ( id, sandbox_id, + sandbox_incarnation, sandbox_session_id, runtime, protocol, @@ -126,18 +219,19 @@ async function insertLiveDriverInstance( created_at, updated_at ) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) `, ) .bind( input.driverInstanceId, PUBLIC_API_TEST_IDS.sandbox, + 1, input.sessionId, "cloudflare-container", "driver-ws", 1, "ready", - new Uint8Array([1, 2, 3]), + new Uint8Array([input.tokenByte]), 10_000, 0, 20_000, @@ -148,12 +242,25 @@ async function insertLiveDriverInstance( } function createRuntimeTarget( - input: Omit, + input: Omit< + RuntimeSessionTarget, + | "sessionRuntimeEventSeqCursor" + | "sessionStatusOperationId" + | "sessionStatusSeq" + | "sessionUpdatedAt" + > & { + readonly sessionRuntimeEventSeqCursor?: number; + readonly sessionStatusOperationId?: RuntimeSessionTarget["sessionStatusOperationId"]; + readonly sessionStatusSeq?: number; + readonly sessionUpdatedAt?: number; + }, ): RuntimeSessionTarget { return { ...input, - sessionStatusOperationId: null, - sessionStatusSeq: 0, + sessionRuntimeEventSeqCursor: input.sessionRuntimeEventSeqCursor ?? 0, + sessionStatusOperationId: input.sessionStatusOperationId ?? null, + sessionStatusSeq: input.sessionStatusSeq ?? 0, + sessionUpdatedAt: input.sessionUpdatedAt ?? nowMsForTest(), }; } @@ -161,6 +268,122 @@ function createExistingRuntimeDiagnosticTargets(): RuntimeSessionTarget[] { return createRuntimeDiagnosticTargets().filter((target) => target.agentId !== null); } +async function createAdoptableRuntimeOperationReadyReceipt(database: D1Database): Promise<{ + readonly target: RuntimeSessionTarget; +}> { + await insertNonOwnerSession(database); + const bindings = createPublicHttpTestBindings(database) as ApiBindings; + const [updatingEvent, readyEvent] = buildRuntimeStateOperationEvents({ + agentId: PUBLIC_API_TEST_IDS.agent, + operation: "restartDriver", + readyAt: "2026-05-08T00:00:01.000Z", + startedAt: "2026-05-08T00:00:00.000Z", + }); + const [claimed] = await claimRuntimeOperationTargets(database, { + event: updatingEvent, + operationId: PUBLIC_API_TEST_IDS.operation, + targets: createExistingRuntimeDiagnosticTargets().slice(0, 1), + }); + if (claimed === undefined) { + throw new Error("Missing runtime operation claim fixture."); + } + await commitRuntimeOperationReadySnapshots(bindings, { + event: readyEvent, + operationId: PUBLIC_API_TEST_IDS.operation, + targets: [claimed.current], + }); + await database + .prepare( + `UPDATE session + SET status = 'RESCHEDULING', + status_operation_id = ?, + status_seq = status_seq + 1 + WHERE id = ?`, + ) + .bind(PUBLIC_API_TEST_IDS.operation, PUBLIC_API_TEST_IDS.nonOwnerSession) + .run(); + + return { target: await readRuntimeTarget(database) }; +} + +async function rewriteRuntimeOperationAuthority( + database: D1Database, + status: "ready" | "updating", + rewrite: (event: Record) => Record, +): Promise { + const sourceEventId = `runtime-operation:${PUBLIC_API_TEST_IDS.operation}:${PUBLIC_API_TEST_IDS.nonOwnerSession}:${status}`; + const row = await database + .prepare( + `SELECT runtime_operation_event_json + FROM session_event + WHERE session_id = ? AND source_event_id = ?`, + ) + .bind(PUBLIC_API_TEST_IDS.nonOwnerSession, sourceEventId) + .first<{ runtime_operation_event_json: string | null }>(); + if (row?.runtime_operation_event_json === null || row === null) { + throw new Error(`Missing runtime operation ${status} authority fixture.`); + } + const event = parseRuntimeEventEnvelope(rewrite(JSON.parse(row.runtime_operation_event_json))); + await database + .prepare( + `UPDATE session_event + SET runtime_operation_event_json = ?, semantic_hash = ? + WHERE session_id = ? AND source_event_id = ?`, + ) + .bind( + stringifyRuntimeEventSemanticValue(event), + await createRuntimeEventSemanticHash(event), + PUBLIC_API_TEST_IDS.nonOwnerSession, + sourceEventId, + ) + .run(); +} + +function raceRuntimeOperationReadyAdoption( + database: D1Database, + race: () => Promise, +): { readonly database: D1Database; readonly raced: () => boolean } { + let raced = false; + + function wrap(statement: D1PreparedStatement, shouldRace: boolean): D1PreparedStatement { + return new Proxy(statement, { + get(target, property, receiver) { + if (property === "bind") { + return (...values: unknown[]) => wrap(target.bind(...values), shouldRace); + } + if (property === "run" && shouldRace) { + return async () => { + if (!raced) { + raced = true; + await race(); + } + return target.run(); + }; + } + return Reflect.get(target, property, receiver); + }, + }); + } + + return { + database: new Proxy(database, { + get(target, property) { + if (property === "prepare") { + return (query: string) => + wrap( + target.prepare(query), + query.includes("UPDATE session") && + query.includes("runtime_operation_event_json = ?"), + ); + } + const value = Reflect.get(target, property); + return typeof value === "function" ? value.bind(target) : value; + }, + }), + raced: () => raced, + }; +} + describe("runtime state operation target events", () => { test("operation events carry the target deployment version", () => { const events = buildRuntimeStateOperationEvents({ @@ -218,6 +441,10 @@ describe("runtime state operation target events", () => { await insertRunningSessionRun(database); const bindings = createPublicHttpTestBindings(database) as ApiBindings; + await database + .prepare("UPDATE session SET status = ?, status_operation_id = ? WHERE id = ?") + .bind("RESCHEDULING", PUBLIC_API_TEST_IDS.operation, PUBLIC_API_TEST_IDS.nonOwnerSession) + .run(); const targets: RuntimeSessionTarget[] = [ createRuntimeTarget({ agentId: PUBLIC_API_TEST_IDS.agent, @@ -225,12 +452,14 @@ describe("runtime state operation target events", () => { lastRunId: PUBLIC_API_TEST_IDS.run, sandboxId: PUBLIC_API_TEST_IDS.sandbox, sessionId: PUBLIC_API_TEST_IDS.nonOwnerSession, - sessionStatus: "RUNNING", + sessionStatus: "RESCHEDULING", + sessionStatusOperationId: PUBLIC_API_TEST_IDS.operation, }), ]; await writeRuntimeOperationInterruptedSnapshots(bindings, { operationId: PUBLIC_API_TEST_IDS.operation, + timestampMs: nowMsForTest(), targets, }); @@ -256,6 +485,10 @@ describe("runtime state operation target events", () => { }); const bindings = createPublicHttpTestBindings(database) as ApiBindings; + await database + .prepare("UPDATE session SET status = ?, status_operation_id = ?") + .bind("RESCHEDULING", PUBLIC_API_TEST_IDS.operation) + .run(); const targets: RuntimeSessionTarget[] = [ createRuntimeTarget({ agentId: PUBLIC_API_TEST_IDS.agent, @@ -263,7 +496,8 @@ describe("runtime state operation target events", () => { lastRunId: PUBLIC_API_TEST_IDS.run, sandboxId: PUBLIC_API_TEST_IDS.sandbox, sessionId: PUBLIC_API_TEST_IDS.nonOwnerSession, - sessionStatus: "RUNNING", + sessionStatus: "RESCHEDULING", + sessionStatusOperationId: PUBLIC_API_TEST_IDS.operation, }), createRuntimeTarget({ agentId: PUBLIC_API_TEST_IDS.agent, @@ -271,12 +505,14 @@ describe("runtime state operation target events", () => { lastRunId: PUBLIC_API_TEST_IDS.runAlt, sandboxId: PUBLIC_API_TEST_IDS.sandbox, sessionId: PUBLIC_API_TEST_IDS.ownerSession, - sessionStatus: "RUNNING", + sessionStatus: "RESCHEDULING", + sessionStatusOperationId: PUBLIC_API_TEST_IDS.operation, }), ]; await writeRuntimeOperationInterruptedSnapshots(bindings, { operationId: PUBLIC_API_TEST_IDS.operation, + timestampMs: nowMsForTest(), targets, }); @@ -310,6 +546,51 @@ describe("runtime state operation target events", () => { ]); }); + test("preserves a terminal lifecycle winner during interrupted recovery", async () => { + const database = await createPublicHttpContractDatabase(); + await insertNonOwnerSession(database); + await insertRunningSessionRun(database); + const bindings = createPublicHttpTestBindings(database) as ApiBindings; + await recordCanonicalSessionRunTerminal(bindings, { + assistantMessage: null, + deliver: false, + error: { + code: "driver.command_failed", + details: {}, + message: "Driver failed before recovery.", + retryable: false, + }, + lifecycle: "TERMINATED", + runId: PUBLIC_API_TEST_IDS.run, + sessionId: PUBLIC_API_TEST_IDS.nonOwnerSession, + source: "maintenance", + status: "failed", + timestampMs: 2, + }); + await database + .prepare( + `UPDATE session + SET status = 'RESCHEDULING', status_operation_id = ?, + status_seq = status_seq + 1, updated_at = 3 + WHERE id = ?`, + ) + .bind(PUBLIC_API_TEST_IDS.operation, PUBLIC_API_TEST_IDS.nonOwnerSession) + .run(); + + await writeRuntimeOperationInterruptedSnapshots(bindings, { + operationId: PUBLIC_API_TEST_IDS.operation, + targets: [await readRuntimeTarget(database)], + timestampMs: 3, + }); + + await expect( + database + .prepare("SELECT status, status_operation_id FROM session WHERE id = ?") + .bind(PUBLIC_API_TEST_IDS.nonOwnerSession) + .first(), + ).resolves.toEqual({ status: "TERMINATED", status_operation_id: null }); + }); + test("terminated subject events are written for target sessions", async () => { const database = await createPublicHttpContractDatabase(); await insertNonOwnerSession(database); @@ -347,13 +628,40 @@ describe("runtime state operation target events", () => { const database = await createPublicHttpContractDatabase(); await insertNonOwnerSession(database); await insertOwnerSession(database); + await insertActiveSandboxSessionFixture(database, { + ownerAccountId: PUBLIC_API_TEST_IDS.nonOwnerAccount, + sandboxId: PUBLIC_API_TEST_IDS.sandbox, + sessionId: PUBLIC_API_TEST_IDS.nonOwnerSession, + }); + await database + .app() + .insert(sandboxSessionsTable) + .values({ + createdAt: nowMsForTest(), + cwd: `/workspace/se/${PUBLIC_API_TEST_IDS.ownerSession}`, + originJson: JSON.stringify({ + callerUserId: PUBLIC_API_TEST_IDS.ownerAccount, + entrypoint: "api", + executionOwnerUserId: PUBLIC_API_TEST_IDS.ownerAccount, + type: "agent", + }), + sandboxId: PUBLIC_API_TEST_IDS.sandbox, + sandboxIncarnation: 1, + sandboxSessionId: PUBLIC_API_TEST_IDS.driverOwner, + sessionId: PUBLIC_API_TEST_IDS.ownerSession, + status: "active", + updatedAt: nowMsForTest(), + }) + .run(); await insertLiveDriverInstance(database, { driverInstanceId: PUBLIC_API_TEST_IDS.driverNonOwner, sessionId: PUBLIC_API_TEST_IDS.nonOwnerSession, + tokenByte: 1, }); await insertLiveDriverInstance(database, { driverInstanceId: PUBLIC_API_TEST_IDS.driverOwner, sessionId: PUBLIC_API_TEST_IDS.ownerSession, + tokenByte: 2, }); const bindings = createPublicHttpTestBindings(database) as ApiBindings; @@ -383,52 +691,484 @@ describe("runtime state operation target events", () => { expect(rows.results.every((row) => row.event_type === "runtime.driver.updated")).toBe(true); }); - test("runtime operation broadcasts events across target sessions", async () => { + test("ready snapshots atomically release operation-owned target sessions", async () => { const database = await createPublicHttpContractDatabase(); await insertNonOwnerSession(database); await insertOwnerSession(database); const bindings = createPublicHttpTestBindings(database) as ApiBindings; - const [event] = buildRuntimeStateOperationEvents({ + const [updatingEvent, readyEvent] = buildRuntimeStateOperationEvents({ agentId: PUBLIC_API_TEST_IDS.agent, operation: "restartDriver", readyAt: "2026-05-08T00:00:01.000Z", startedAt: "2026-05-08T00:00:00.000Z", }); - await broadcastRuntimeOperationEvent(bindings, { - event, + const claimed = await claimRuntimeOperationTargets(database, { + event: updatingEvent, operationId: PUBLIC_API_TEST_IDS.operation, targets: createExistingRuntimeDiagnosticTargets(), }); + await commitRuntimeOperationReadySnapshots(bindings, { + event: readyEvent, + operationId: PUBLIC_API_TEST_IDS.operation, + targets: claimed.map((transition) => transition.current), + }); const rows = await database .prepare( ` - SELECT event_type, seq, session_id + SELECT event_type, runtime_operation_event_json, seq, session_id, source_event_id FROM session_event - ORDER BY session_id + ORDER BY session_id, seq `, ) .all<{ event_type: string; + runtime_operation_event_json: string | null; seq: number; session_id: string; + source_event_id: string; }>(); expect(rows.results.map((row) => ({ seq: row.seq, sessionId: row.session_id }))).toEqual([ { seq: 1, sessionId: PUBLIC_API_TEST_IDS.nonOwnerSession }, + { seq: 2, sessionId: PUBLIC_API_TEST_IDS.nonOwnerSession }, { seq: 1, sessionId: PUBLIC_API_TEST_IDS.ownerSession }, + { seq: 2, sessionId: PUBLIC_API_TEST_IDS.ownerSession }, ]); expect(rows.results.every((row) => row.event_type === "agent.task.updated")).toBe(true); + expect( + rows.results.map((row) => ({ + authority: row.runtime_operation_event_json === null ? "legacy" : "canonical", + source: row.source_event_id.endsWith(":ready") ? "ready" : "updating", + })), + ).toEqual([ + { authority: "canonical", source: "updating" }, + { authority: "canonical", source: "ready" }, + { authority: "canonical", source: "updating" }, + { authority: "canonical", source: "ready" }, + ]); + expect( + await database.prepare("SELECT status, status_operation_id FROM session ORDER BY id").all(), + ).toMatchObject({ + results: expect.arrayContaining([ + { status: "IDLE", status_operation_id: null }, + { status: "IDLE", status_operation_id: null }, + ]), + }); + }); + + test("adopts one canonical ready receipt and acknowledges its replay", async () => { + const database = await createPublicHttpContractDatabase(); + const { target } = await createAdoptableRuntimeOperationReadyReceipt(database); + const input = { operationId: PUBLIC_API_TEST_IDS.operation, target }; + + await expect(adoptRuntimeOperationReadyReceipt(database, input)).resolves.toBe("applied"); + await expect(adoptRuntimeOperationReadyReceipt(database, input)).resolves.toBe("duplicate"); + await expect( + database + .prepare("SELECT status, status_operation_id FROM session WHERE id = ?") + .bind(PUBLIC_API_TEST_IDS.nonOwnerSession) + .first(), + ).resolves.toEqual({ status: "IDLE", status_operation_id: null }); + }); + + test("rejects an updating event forged behind the ready source receipt", async () => { + const database = await createPublicHttpContractDatabase(); + const { target } = await createAdoptableRuntimeOperationReadyReceipt(database); + const sourceEventId = `runtime-operation:${PUBLIC_API_TEST_IDS.operation}:${PUBLIC_API_TEST_IDS.nonOwnerSession}:ready`; + const row = await database + .prepare( + `SELECT runtime_operation_event_json + FROM session_event + WHERE session_id = ? AND source_event_id = ?`, + ) + .bind(PUBLIC_API_TEST_IDS.nonOwnerSession, sourceEventId) + .first<{ runtime_operation_event_json: string | null }>(); + if (row?.runtime_operation_event_json === null || row === null) { + throw new Error("Missing runtime operation ready authority fixture."); + } + const ready = parseRuntimeEventEnvelope(JSON.parse(row.runtime_operation_event_json)); + const payload = requireRecord(ready.payload); + const updating = parseRuntimeEventEnvelope({ + ...ready, + payload: { + agentId: payload["agentId"], + operation: payload["operation"], + operationId: payload["operationId"], + startedAt: ready.occurredAt, + status: "updating", + }, + }); + await database + .prepare( + `UPDATE session_event + SET runtime_operation_event_json = NULL, semantic_hash = ? + WHERE session_id = ? AND source_event_id = ?`, + ) + .bind( + await createRuntimeEventSemanticHash(updating), + PUBLIC_API_TEST_IDS.nonOwnerSession, + sourceEventId, + ) + .run(); + + await expect( + adoptRuntimeOperationReadyReceipt(database, { + operationId: PUBLIC_API_TEST_IDS.operation, + target, + }), + ).rejects.toThrow("has no semantic authority"); + await expect( + database + .prepare("SELECT status, status_operation_id FROM session WHERE id = ?") + .bind(PUBLIC_API_TEST_IDS.nonOwnerSession) + .first(), + ).resolves.toEqual({ + status: "RESCHEDULING", + status_operation_id: PUBLIC_API_TEST_IDS.operation, + }); + }); + + test("requires the canonical updating claim before adopting ready", async () => { + const database = await createPublicHttpContractDatabase(); + const { target } = await createAdoptableRuntimeOperationReadyReceipt(database); + await database + .prepare( + `DELETE FROM session_event + WHERE session_id = ? AND source_event_id LIKE 'runtime-operation:%:updating'`, + ) + .bind(PUBLIC_API_TEST_IDS.nonOwnerSession) + .run(); + + await expect( + adoptRuntimeOperationReadyReceipt(database, { + operationId: PUBLIC_API_TEST_IDS.operation, + target, + }), + ).rejects.toThrow("has no canonical claim"); }); - test("timed out snapshots cancel running runs and persist target events", async () => { + test("rejects a self-consistent claim payload mutation", async () => { + const database = await createPublicHttpContractDatabase(); + const { target } = await createAdoptableRuntimeOperationReadyReceipt(database); + await rewriteRuntimeOperationAuthority(database, "updating", (event) => ({ + ...event, + payload: { ...requireRecord(event["payload"]), forged: true }, + })); + + await expect( + adoptRuntimeOperationReadyReceipt(database, { + operationId: PUBLIC_API_TEST_IDS.operation, + target, + }), + ).rejects.toThrow("updating source"); + }); + + test("fails closed for self-consistent ready authority identity corruption", async () => { + const corruptions: readonly { + readonly rewrite: (event: Record) => Record; + }[] = [ + { + rewrite: (event) => ({ + ...event, + payload: { ...requireRecord(event["payload"]), forged: true }, + }), + }, + { + rewrite: (event) => ({ ...event, sourceEventId: "forged-runtime-operation-ready" }), + }, + { + rewrite: (event) => ({ ...event, sessionId: PUBLIC_API_TEST_IDS.ownerSession }), + }, + { + rewrite: (event) => ({ + ...event, + payload: { + ...requireRecord(event["payload"]), + operationId: PUBLIC_API_TEST_IDS.deployment, + }, + }), + }, + { + rewrite: (event) => ({ + ...event, + payload: { + ...requireRecord(event["payload"]), + agentId: PUBLIC_API_TEST_IDS.ownerAccount, + }, + }), + }, + { + rewrite: (event) => ({ + ...event, + payload: { + ...requireRecord(event["payload"]), + operation: "recreateSandbox", + }, + }), + }, + { + rewrite: (event) => ({ + ...event, + payload: { + ...requireRecord(event["payload"]), + deploymentVersionId: PUBLIC_API_TEST_IDS.deployment, + deploymentVersionNumber: 1, + }, + }), + }, + { + rewrite: (event) => ({ ...event, visibility: "owner_debug" }), + }, + { + rewrite: (event) => ({ ...event, actor: "system", origin: "system" }), + }, + ]; + + for (const corruption of corruptions) { + const database = await createPublicHttpContractDatabase(); + const { target } = await createAdoptableRuntimeOperationReadyReceipt(database); + await rewriteRuntimeOperationAuthority(database, "ready", corruption.rewrite); + + await expect( + adoptRuntimeOperationReadyReceipt(database, { + operationId: PUBLIC_API_TEST_IDS.operation, + target, + }), + ).rejects.toThrow("ready source"); + await expect( + database + .prepare("SELECT status, status_operation_id FROM session WHERE id = ?") + .bind(PUBLIC_API_TEST_IDS.nonOwnerSession) + .first(), + ).resolves.toEqual({ + status: "RESCHEDULING", + status_operation_id: PUBLIC_API_TEST_IDS.operation, + }); + } + }); + + test("recomputes the ready authority hash before adopting it", async () => { + const database = await createPublicHttpContractDatabase(); + const { target } = await createAdoptableRuntimeOperationReadyReceipt(database); + await database + .prepare( + `UPDATE session_event + SET semantic_hash = ? + WHERE session_id = ? AND source_event_id LIKE 'runtime-operation:%:ready'`, + ) + .bind("0".repeat(64), PUBLIC_API_TEST_IDS.nonOwnerSession) + .run(); + + await expect( + adoptRuntimeOperationReadyReceipt(database, { + operationId: PUBLIC_API_TEST_IDS.operation, + target, + }), + ).rejects.toThrow("is not canonical"); + }); + + test("does not cross either receipt mutation that races the adoption CAS", async () => { + for (const status of ["updating", "ready"] as const) { + const database = await createPublicHttpContractDatabase(); + const { target } = await createAdoptableRuntimeOperationReadyReceipt(database); + const racing = raceRuntimeOperationReadyAdoption(database, async () => { + await database + .prepare( + `UPDATE session_event + SET semantic_hash = ? + WHERE session_id = ? AND source_event_id LIKE ?`, + ) + .bind( + "f".repeat(64), + PUBLIC_API_TEST_IDS.nonOwnerSession, + `runtime-operation:%:${status}`, + ) + .run(); + }); + + await expect( + adoptRuntimeOperationReadyReceipt(racing.database, { + operationId: PUBLIC_API_TEST_IDS.operation, + target, + }), + ).rejects.toThrow("is not canonical"); + expect(racing.raced()).toBe(true); + await expect( + database + .prepare("SELECT status, status_operation_id FROM session WHERE id = ?") + .bind(PUBLIC_API_TEST_IDS.nonOwnerSession) + .first(), + ).resolves.toEqual({ + status: "RESCHEDULING", + status_operation_id: PUBLIC_API_TEST_IDS.operation, + }); + } + }); + + test("operation start cannot cross an active runtime provisioning fence", async () => { + const database = await createPublicHttpContractDatabase(); + await insertNonOwnerSession(database); + await database + .prepare( + `UPDATE session + SET runtime_provisioning_heartbeat_at = 1, + runtime_provisioning_operation_id = ?, + runtime_provisioning_sandbox_id = ? + WHERE id = ?`, + ) + .bind( + PUBLIC_API_TEST_IDS.operation, + PUBLIC_API_TEST_IDS.sandbox, + PUBLIC_API_TEST_IDS.nonOwnerSession, + ) + .run(); + const [updatingEvent] = buildRuntimeStateOperationEvents({ + agentId: PUBLIC_API_TEST_IDS.agent, + operation: "restartDriver", + readyAt: "2026-05-08T00:00:01.000Z", + startedAt: "2026-05-08T00:00:00.000Z", + }); + + expect( + await claimRuntimeOperationTargets(database, { + event: updatingEvent, + operationId: PUBLIC_API_TEST_IDS.operation, + targets: createExistingRuntimeDiagnosticTargets().slice(0, 1), + }), + ).toEqual([]); + expect( + await database + .prepare( + `SELECT runtime_event_seq_cursor, + runtime_provisioning_operation_id, + status, + status_operation_id + FROM session + WHERE id = ?`, + ) + .bind(PUBLIC_API_TEST_IDS.nonOwnerSession) + .first(), + ).toEqual({ + runtime_event_seq_cursor: 0, + runtime_provisioning_operation_id: PUBLIC_API_TEST_IDS.operation, + status: "IDLE", + status_operation_id: null, + }); + expect( + await database + .prepare("SELECT COUNT(*) AS count FROM session_event WHERE session_id = ?") + .bind(PUBLIC_API_TEST_IDS.nonOwnerSession) + .first(), + ).toEqual({ count: 0 }); + }); + + test("ready wins atomically when operation timeout races its release", async () => { + const database = await createPublicHttpContractDatabase(); + await insertNonOwnerSession(database); + await database + .prepare("UPDATE session SET updated_at = 1 WHERE id = ?") + .bind(PUBLIC_API_TEST_IDS.nonOwnerSession) + .run(); + const [updatingEvent, readyEvent] = buildRuntimeStateOperationEvents({ + agentId: PUBLIC_API_TEST_IDS.agent, + operation: "restartDriver", + readyAt: "1970-01-01T00:00:00.002Z", + startedAt: "1970-01-01T00:00:00.001Z", + }); + const [claimed] = await claimRuntimeOperationTargets(database, { + event: updatingEvent, + operationId: PUBLIC_API_TEST_IDS.operation, + targets: [ + createRuntimeTarget({ + agentId: PUBLIC_API_TEST_IDS.agent, + creatorAccountId: PUBLIC_API_TEST_IDS.nonOwnerAccount, + lastRunId: null, + sandboxId: PUBLIC_API_TEST_IDS.sandbox, + sessionId: PUBLIC_API_TEST_IDS.nonOwnerSession, + sessionStatus: "IDLE", + sessionUpdatedAt: 1, + }), + ], + }); + if (claimed === undefined) { + throw new Error("Missing runtime operation claim fixture."); + } + const readyBindings = createPublicHttpTestBindings(database) as ApiBindings; + let raced = false; + const racingDatabase = new Proxy(database, { + get(target, property) { + if (property === "batch") { + return async (statements: D1PreparedStatement[]) => { + if (!raced) { + raced = true; + await commitRuntimeOperationReadySnapshots(readyBindings, { + event: readyEvent, + operationId: PUBLIC_API_TEST_IDS.operation, + targets: [claimed.current], + }); + } + return target.batch(statements); + }; + } + + const value = Reflect.get(target, property); + return typeof value === "function" ? value.bind(target) : value; + }, + }) as D1Database; + + await writeRuntimeOperationTimedOutSnapshots( + { + ...(createPublicHttpTestBindings(database) as ApiBindings), + DB: racingDatabase, + }, + { operationId: PUBLIC_API_TEST_IDS.operation, targets: [claimed.current] }, + ); + + expect(raced).toBe(true); + await expect( + database + .prepare("SELECT status, status_operation_id FROM session WHERE id = ?") + .bind(PUBLIC_API_TEST_IDS.nonOwnerSession) + .first(), + ).resolves.toEqual({ status: "IDLE", status_operation_id: null }); + await expect( + database + .prepare( + `SELECT source_event_id + FROM session_event + WHERE source_event_id LIKE 'runtime-operation:%' + ORDER BY seq`, + ) + .all(), + ).resolves.toMatchObject({ + results: [ + { + source_event_id: `runtime-operation:${PUBLIC_API_TEST_IDS.operation}:${PUBLIC_API_TEST_IDS.nonOwnerSession}:updating`, + }, + { + source_event_id: `runtime-operation:${PUBLIC_API_TEST_IDS.operation}:${PUBLIC_API_TEST_IDS.nonOwnerSession}:ready`, + }, + ], + }); + }); + + test("timed out snapshots expire running runs and persist target events", async () => { const database = await createPublicHttpContractDatabase(); await insertNonOwnerSession(database); await insertOwnerSession(database); await insertRunningSessionRun(database); const bindings = createPublicHttpTestBindings(database) as ApiBindings; + await database + .prepare("UPDATE session SET status = ?, status_operation_id = ? WHERE id = ?") + .bind("RESCHEDULING", PUBLIC_API_TEST_IDS.operation, PUBLIC_API_TEST_IDS.nonOwnerSession) + .run(); + await database + .prepare("UPDATE session SET status_operation_id = ? WHERE id = ?") + .bind(PUBLIC_API_TEST_IDS.operation, PUBLIC_API_TEST_IDS.ownerSession) + .run(); const targets: RuntimeSessionTarget[] = [ createRuntimeTarget({ agentId: PUBLIC_API_TEST_IDS.agent, @@ -436,7 +1176,8 @@ describe("runtime state operation target events", () => { lastRunId: PUBLIC_API_TEST_IDS.run, sandboxId: PUBLIC_API_TEST_IDS.sandbox, sessionId: PUBLIC_API_TEST_IDS.nonOwnerSession, - sessionStatus: "RUNNING", + sessionStatus: "RESCHEDULING", + sessionStatusOperationId: PUBLIC_API_TEST_IDS.operation, }), createRuntimeTarget({ agentId: PUBLIC_API_TEST_IDS.agent, @@ -445,6 +1186,7 @@ describe("runtime state operation target events", () => { sandboxId: PUBLIC_API_TEST_IDS.sandbox, sessionId: PUBLIC_API_TEST_IDS.ownerSession, sessionStatus: "IDLE", + sessionStatusOperationId: PUBLIC_API_TEST_IDS.operation, }), ]; @@ -457,7 +1199,7 @@ describe("runtime state operation target events", () => { .prepare("SELECT status FROM session_run WHERE id = ?") .bind(PUBLIC_API_TEST_IDS.run) .first<{ status: string }>(); - expect(run).toEqual({ status: "cancelled" }); + expect(run).toEqual({ status: "expired" }); const events = await database .prepare( ` @@ -474,4 +1216,201 @@ describe("runtime state operation target events", () => { { seq: 1, sessionId: PUBLIC_API_TEST_IDS.ownerSession }, ]); }); + + test("preserves a terminal lifecycle winner while releasing its operation fence", async () => { + const database = await createPublicHttpContractDatabase(); + await insertNonOwnerSession(database); + await insertRunningSessionRun(database); + const bindings = createPublicHttpTestBindings(database) as ApiBindings; + await recordCanonicalSessionRunTerminal(bindings, { + assistantMessage: null, + deliver: false, + error: { + code: "driver.command_failed", + details: {}, + message: "Driver failed before the operation completed.", + retryable: false, + }, + lifecycle: "TERMINATED", + runId: PUBLIC_API_TEST_IDS.run, + sessionId: PUBLIC_API_TEST_IDS.nonOwnerSession, + source: "maintenance", + status: "failed", + timestampMs: 2, + }); + await database + .prepare( + `UPDATE session + SET status = 'RESCHEDULING', status_operation_id = ?, + status_seq = status_seq + 1, updated_at = 3 + WHERE id = ?`, + ) + .bind(PUBLIC_API_TEST_IDS.operation, PUBLIC_API_TEST_IDS.nonOwnerSession) + .run(); + + await writeRuntimeOperationTimedOutSnapshots(bindings, { + operationId: PUBLIC_API_TEST_IDS.operation, + targets: [await readRuntimeTarget(database)], + }); + + await expect( + database + .prepare("SELECT status, status_operation_id FROM session WHERE id = ?") + .bind(PUBLIC_API_TEST_IDS.nonOwnerSession) + .first(), + ).resolves.toEqual({ status: "TERMINATED", status_operation_id: null }); + }); + + test("does not release an operation from a self-consistent missing final authority", async () => { + const database = await createPublicHttpContractDatabase(); + await insertNonOwnerSession(database); + await insertRunningSessionRun(database); + await database + .prepare("UPDATE session SET status = ?, status_operation_id = ? WHERE id = ?") + .bind("RESCHEDULING", PUBLIC_API_TEST_IDS.operation, PUBLIC_API_TEST_IDS.nonOwnerSession) + .run(); + const bindings = createPublicHttpTestBindings(database) as ApiBindings; + await recordCanonicalSessionRunTerminal(bindings, { + assistantMessage: null, + deliver: false, + error: null, + lifecycle: "IDLE", + runId: PUBLIC_API_TEST_IDS.run, + sessionId: PUBLIC_API_TEST_IDS.nonOwnerSession, + source: "driver", + status: "completed", + timestampMs: 2, + }); + await rewriteTerminalAuthority(database, (value) => ({ + ...value, + payload: { + ...requireRecord(value.payload), + finalMessageId: "01J0000000000000000000000M", + }, + })); + const target = await readRuntimeTarget(database); + + await expect( + writeRuntimeOperationTimedOutSnapshots(bindings, { + operationId: PUBLIC_API_TEST_IDS.operation, + targets: [target], + }), + ).rejects.toThrow("Canonical final assistant"); + await expect( + database + .prepare("SELECT status, status_operation_id FROM session WHERE id = ?") + .bind(PUBLIC_API_TEST_IDS.nonOwnerSession) + .first(), + ).resolves.toEqual({ status: "IDLE", status_operation_id: PUBLIC_API_TEST_IDS.operation }); + }); + + test("does not release an operation from a self-consistent conflicting Run error", async () => { + const database = await createPublicHttpContractDatabase(); + await insertNonOwnerSession(database); + await insertRunningSessionRun(database); + await database + .prepare("UPDATE session SET status = ?, status_operation_id = ? WHERE id = ?") + .bind("RESCHEDULING", PUBLIC_API_TEST_IDS.operation, PUBLIC_API_TEST_IDS.nonOwnerSession) + .run(); + const bindings = createPublicHttpTestBindings(database) as ApiBindings; + await recordCanonicalSessionRunTerminal(bindings, { + assistantMessage: null, + deliver: false, + error: { + code: "driver.command_failed", + details: {}, + message: "A", + retryable: false, + }, + lifecycle: "IDLE", + runId: PUBLIC_API_TEST_IDS.run, + sessionId: PUBLIC_API_TEST_IDS.nonOwnerSession, + source: "driver", + status: "failed", + timestampMs: 2, + }); + const conflictingError = { + code: "driver.command_failed", + details: {}, + message: "B", + retryable: false, + }; + await rewriteTerminalAuthority(database, (value) => { + const payload = requireRecord(value.payload); + return { + ...value, + payload: { + ...payload, + error: conflictingError, + run: { ...requireRecord(payload.run), error: conflictingError }, + }, + }; + }); + const target = await readRuntimeTarget(database); + + await expect( + writeRuntimeOperationTimedOutSnapshots(bindings, { + operationId: PUBLIC_API_TEST_IDS.operation, + targets: [target], + }), + ).rejects.toThrow("persisted RunError"); + await expect( + database + .prepare("SELECT status, status_operation_id FROM session WHERE id = ?") + .bind(PUBLIC_API_TEST_IDS.nonOwnerSession) + .first(), + ).resolves.toEqual({ status: "IDLE", status_operation_id: PUBLIC_API_TEST_IDS.operation }); + }); + + test("retries a timeout from the fresh target after losing its lifecycle CAS", async () => { + const database = await createPublicHttpContractDatabase(); + await insertNonOwnerSession(database); + await database + .prepare("UPDATE session SET status = ?, status_operation_id = ? WHERE id = ?") + .bind("RESCHEDULING", PUBLIC_API_TEST_IDS.operation, PUBLIC_API_TEST_IDS.nonOwnerSession) + .run(); + const target = await readRuntimeTarget(database); + let raced = false; + const racingDatabase = new Proxy(database, { + get(source, property) { + if (property === "batch") { + return async (statements: D1PreparedStatement[]) => { + if (!raced) { + raced = true; + await source + .prepare( + `UPDATE session + SET runtime_event_seq_cursor = runtime_event_seq_cursor + 1, + updated_at = updated_at + 1 + WHERE id = ?`, + ) + .bind(PUBLIC_API_TEST_IDS.nonOwnerSession) + .run(); + } + return source.batch(statements); + }; + } + const value = Reflect.get(source, property); + return typeof value === "function" ? value.bind(source) : value; + }, + }) as D1Database; + + await writeRuntimeOperationTimedOutSnapshots( + { + ...(createPublicHttpTestBindings(database) as ApiBindings), + DB: racingDatabase, + }, + { operationId: PUBLIC_API_TEST_IDS.operation, targets: [target] }, + ); + expect(raced).toBe(true); + await expect( + database + .prepare("SELECT status, status_operation_id FROM session WHERE id = ?") + .bind(PUBLIC_API_TEST_IDS.nonOwnerSession) + .first(), + ).resolves.toEqual({ + status: "TERMINATED", + status_operation_id: null, + }); + }); }); diff --git a/apps/api/tests/runtime-subject-activation-record.test.ts b/apps/api/tests/runtime-subject-activation-record.test.ts index 6fd1ad65..e5868bad 100644 --- a/apps/api/tests/runtime-subject-activation-record.test.ts +++ b/apps/api/tests/runtime-subject-activation-record.test.ts @@ -10,15 +10,24 @@ function createRuntimeSubjectDatabase(): SqliteD1Database { database.execute(` CREATE TABLE sandbox ( + agent_id text NOT NULL DEFAULT '01J00000000000000000000009', + app_id text NOT NULL DEFAULT '01J0000000000000000000000A', claim_expires_at integer, claim_owner text, global_mounts_json text NOT NULL, id text PRIMARY KEY NOT NULL, + incarnation integer NOT NULL DEFAULT 0, kind text NOT NULL, last_backup_id text, last_error text, last_error_code text, - status text NOT NULL + network_constraints_hash text, + operation_kind text, + owner_account_id text NOT NULL DEFAULT '01J00000000000000000000008', + status_operation_id text, + status text NOT NULL, + subject_id text NOT NULL DEFAULT '01J00000000000000000000009', + subject_kind text NOT NULL DEFAULT 'agent' ); CREATE TABLE sandbox_backup ( diff --git a/apps/api/tests/runtime-subject-lifecycle.test.ts b/apps/api/tests/runtime-subject-lifecycle.test.ts index 027df519..670f8c61 100644 --- a/apps/api/tests/runtime-subject-lifecycle.test.ts +++ b/apps/api/tests/runtime-subject-lifecycle.test.ts @@ -1,15 +1,26 @@ import { afterEach, describe, expect, test } from "bun:test"; +import { createTimeoutError } from "@mosoo/effects"; import { createPlatformId } from "@mosoo/id"; -import type { SandboxId, SessionId } from "@mosoo/id"; +import type { RuntimeOperationId, SandboxId, SessionId, SessionRunId } from "@mosoo/id"; import { decideRuntimeSubjectTransition } from "../src/modules/runtime/domain/runtime-subject-lifecycle.machine"; +import { hashSandboxNetworkConstraints } from "../src/modules/runtime/domain/sandbox-network-constraints"; import { createRuntimeSubjectLifecycleService } from "../src/modules/runtime/infrastructure/runtime-subject-lifecycle/runtime-subject-lifecycle.service"; import type { ActivateRuntimeSubjectInput } from "../src/modules/runtime/infrastructure/runtime-subject-lifecycle/runtime-subject-lifecycle.service"; -import { destroyRuntimeSubjectContainer } from "../src/modules/runtime/infrastructure/runtime-subject-lifecycle/runtime-subject-platform"; +import { + recreateRuntimeSubjectPreservingState, + resetRuntimeSubjectAgentState, + runRuntimeSubjectOperation, +} from "../src/modules/runtime/infrastructure/runtime-subject-lifecycle/runtime-subject-operations.service"; +import { + destroyRuntimeSubjectContainer, + getRuntimeSubjectKeepAliveHandle, +} from "../src/modules/runtime/infrastructure/runtime-subject-lifecycle/runtime-subject-platform"; import { recycleRuntimeSubject } from "../src/modules/runtime/infrastructure/runtime-subject-lifecycle/runtime-subject-recycle.service"; import { advanceRuntimeSubjectOperationStatus, + claimRuntimeSubjectOperationForRepair, markRuntimeSubjectCold, markRuntimeSubjectOperationStarted, } from "../src/modules/runtime/infrastructure/runtime-subject-lifecycle/runtime-subject-store"; @@ -31,6 +42,10 @@ const RUNTIME_SUBJECT_QUOTA_SCOPE = { appId: APP_ID, executionOwnerUserId: ACCOUNT_ID, } as const; +const FULL_NETWORK_CONSTRAINTS_HASH = await hashSandboxNetworkConstraints({ + allowedHosts: [], + networkPolicy: "full", +}); afterEach(() => { setServerProductAnalyticsTransportForTests(null); @@ -50,12 +65,15 @@ function createRuntimeSubjectLifecycleDatabase(): SqliteD1Database { global_mounts_json text DEFAULT '[]' NOT NULL, id text PRIMARY KEY NOT NULL, inactive_deadline_at integer, + incarnation integer DEFAULT 0 NOT NULL, kind text NOT NULL, last_backup_id text, last_error text, last_error_code text, last_restore_backup_id text, + network_constraints_hash text, owner_account_id text, + operation_kind text, status text NOT NULL, status_changed_at integer DEFAULT 0 NOT NULL, status_event text DEFAULT 'runtime_subject.cold' NOT NULL, @@ -73,16 +91,35 @@ function createRuntimeSubjectLifecycleDatabase(): SqliteD1Database { error_message text, id text PRIMARY KEY NOT NULL, keep integer NOT NULL, + operation_id text, sandbox_id text NOT NULL, + sandbox_incarnation integer DEFAULT 0 NOT NULL, session_run_id text, status text NOT NULL, ttl_seconds integer NOT NULL, updated_at integer NOT NULL ); + CREATE TABLE sandbox_session ( + cloudflare_session_id text NOT NULL, + cleanup_operation_id text, + created_at integer NOT NULL, + cwd text NOT NULL, + origin_json text NOT NULL, + sandbox_id text NOT NULL, + sandbox_incarnation integer DEFAULT 0 NOT NULL, + session_id text PRIMARY KEY NOT NULL, + status text NOT NULL, + updated_at integer NOT NULL + ); + CREATE TABLE driver_instance ( + generation integer DEFAULT 0 NOT NULL, id text PRIMARY KEY NOT NULL, sandbox_id text NOT NULL, + sandbox_incarnation integer DEFAULT 0 NOT NULL, + sandbox_session_id text NOT NULL, + status_operation_id text, status text NOT NULL ); @@ -93,6 +130,23 @@ function createRuntimeSubjectLifecycleDatabase(): SqliteD1Database { session_id text NOT NULL, status text NOT NULL ); + + CREATE TABLE native_resume_ref ( + session_id text PRIMARY KEY NOT NULL + ); + + CREATE TABLE session ( + archived_at integer, + cleanup_operation_kind text, + id text PRIMARY KEY NOT NULL, + last_message_at integer, + runtime_provisioning_operation_id text, + runtime_provisioning_run_id text, + runtime_provisioning_sandbox_id text, + runtime_provisioning_sandbox_incarnation integer, + runtime_provisioning_sandbox_session_id text, + status text NOT NULL DEFAULT 'ready' + ); `); return database; @@ -101,6 +155,9 @@ function createRuntimeSubjectLifecycleDatabase(): SqliteD1Database { async function insertRuntimeSubject( database: D1Database, input: { + readonly incarnation?: number; + readonly kind?: "cattle" | "pet"; + readonly lastBackupId?: string | null; readonly lastError?: string | null; readonly lastErrorCode?: string | null; readonly status: string; @@ -111,16 +168,21 @@ async function insertRuntimeSubject( .prepare( ` INSERT INTO sandbox ( + agent_id, + app_id, claim_expires_at, claim_owner, created_at, id, inactive_deadline_at, + incarnation, kind, last_backup_id, last_error, last_error_code, last_restore_backup_id, + network_constraints_hash, + owner_account_id, status, status_event, status_seq, @@ -129,31 +191,93 @@ async function insertRuntimeSubject( subject_kind, updated_at ) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) `, ) .bind( + AGENT_ID, + APP_ID, null, null, 1, RUNTIME_SUBJECT_ID, 1, - "cattle", - null, + input.incarnation ?? (input.status === "cold" ? 0 : 1), + input.kind ?? "cattle", + input.lastBackupId ?? null, input.lastError ?? null, input.lastErrorCode ?? null, null, + FULL_NETWORK_CONSTRAINTS_HASH, + ACCOUNT_ID, input.status, `runtime_subject.${input.status === "backing_up" ? "back_up" : input.status}`, input.statusSeq ?? 0, "test", - SESSION_ID, - "session", + input.kind === "pet" ? AGENT_ID : SESSION_ID, + input.kind === "pet" ? "agent" : "session", 1, ) .run(); } +async function insertRuntimeProvisioningAuthority( + database: D1Database, + incarnation: number, +): Promise> { + const operationId = createPlatformId(); + const runId = createPlatformId(); + await database + .prepare( + `INSERT INTO session ( + id, runtime_provisioning_operation_id, runtime_provisioning_run_id, + runtime_provisioning_sandbox_id, runtime_provisioning_sandbox_incarnation, + status + ) VALUES (?, ?, ?, ?, ?, 'ready')`, + ) + .bind(SESSION_ID, operationId, runId, RUNTIME_SUBJECT_ID, incarnation) + .run(); + return { operationId, runId, sessionId: SESSION_ID }; +} + +async function insertReadySubjectBackup(database: D1Database, incarnation: number): Promise { + await database + .prepare( + `INSERT INTO sandbox_backup ( + created_at, dir, error_message, id, keep, sandbox_id, + sandbox_incarnation, status, ttl_seconds, updated_at + ) VALUES (1, '/workspace/memory', NULL, ?, 0, ?, ?, 'ready', 600, 1)`, + ) + .bind(STORED_BACKUP_ID, RUNTIME_SUBJECT_ID, incarnation) + .run(); +} + +async function insertRuntimeConversationSession( + database: D1Database, + input: { + readonly incarnation: number; + readonly sessionId: string; + readonly status: "active" | "cleanup_pending" | "closed" | "error"; + }, +): Promise { + await database + .prepare( + `INSERT INTO sandbox_session ( + cloudflare_session_id, cleanup_operation_id, created_at, cwd, origin_json, + sandbox_id, sandbox_incarnation, session_id, status, updated_at + ) VALUES (?, ?, 1, '/workspace', '{}', ?, ?, ?, ?, 1)`, + ) + .bind( + createPlatformId(), + input.status === "cleanup_pending" ? createPlatformId() : null, + RUNTIME_SUBJECT_ID, + input.incarnation, + input.sessionId, + input.status, + ) + .run(); +} + async function readRuntimeSubject(database: D1Database): Promise<{ status: string; status_event: string; @@ -188,7 +312,12 @@ function createSandboxHandle( readonly destroyError?: Error; readonly destroyPromise?: Promise; readonly onConfigureNetwork?: () => void; + readonly onDispose?: () => void; readonly onDestroy?: () => void; + readonly onExec?: (command: string) => void; + readonly inspectKind?: "healthy" | "missing" | "retired" | "stale" | "unknown"; + readonly onActivate?: (incarnation: number) => void; + readonly onReady?: (incarnation: number) => void; readonly onRestore?: (backup: { readonly dir: string; readonly id: string }) => void; readonly prepareError?: Error; } = {}, @@ -198,6 +327,10 @@ function createSandboxHandle( }; return { + [Symbol.dispose]: () => options.onDispose?.(), + activateRuntimeSubjectIncarnation: async (incarnation: number) => { + options.onActivate?.(incarnation); + }, configureNetworkConstraints: async () => { options.onConfigureNetwork?.(); if (options.configureNetworkError) { @@ -205,6 +338,7 @@ function createSandboxHandle( } }, createBackup: unavailable, + createRuntimeSubjectBackup: unavailable, createSession: unavailable, deleteSession: unavailable, destroy: async () => { @@ -216,8 +350,29 @@ function createSandboxHandle( await options.destroyPromise; }, - exec: unavailable, + destroyRuntimeSubjectIncarnation: async () => { + options.onDestroy?.(); + + if (options.destroyError) { + throw options.destroyError; + } + + await options.destroyPromise; + return { kind: "destroyed" as const }; + }, + exec: options.onExec + ? async (command) => { + options.onExec?.(command); + return { exitCode: 0, stderr: "", stdout: "", success: true }; + } + : unavailable, getSession: unavailable, + inspectRuntimeSubjectIncarnation: async () => ({ + kind: options.inspectKind ?? "healthy", + }), + markRuntimeSubjectIncarnationReady: async (incarnation: number) => { + options.onReady?.(incarnation); + }, mkdir: async () => { if (options.prepareError) { throw options.prepareError; @@ -248,9 +403,14 @@ function createBindings( readonly configureNetworkError?: Error; readonly destroyError?: Error; readonly destroyPromise?: Promise; + readonly inspectKind?: "healthy" | "missing" | "retired" | "stale" | "unknown"; + readonly onActivate?: (incarnation: number) => void; readonly onConfigureNetwork?: () => void; + readonly onDispose?: () => void; readonly onDestroy?: () => void; + readonly onExec?: (command: string) => void; readonly onRestore?: (backup: { readonly dir: string; readonly id: string }) => void; + readonly onReady?: (incarnation: number) => void; readonly prepareError?: Error; } = {}, ): ApiBindings { @@ -259,42 +419,40 @@ function createBindings( MOSOO_ACCOUNT_CONCURRENT_SANDBOX_LIMIT: options.accountConcurrentSandboxLimit ?? "5", SANDBOX_FILE_BUCKET_LOCAL: "true", runtimeSubjectHandleFactory: () => createSandboxHandle(options), - } as unknown as ApiBindings; + } as ApiBindings; } describe("runtime subject lifecycle machine", () => { - test("keeps subject operation transitions explicit", () => { - expect( - decideRuntimeSubjectTransition({ - currentStatus: "cold", - targetStatus: "restoring", - }), - ).toMatchObject({ kind: "accepted", nextStatus: "restoring" }); - expect( - decideRuntimeSubjectTransition({ - currentStatus: "restoring", - targetStatus: "backing_up", - }), - ).toMatchObject({ kind: "rejected", reason: "illegal_transition" }); - expect( - decideRuntimeSubjectTransition({ - currentStatus: "backing_up", - targetStatus: "destroying", - }), - ).toMatchObject({ kind: "accepted", nextStatus: "destroying" }); - expect( - decideRuntimeSubjectTransition({ - currentStatus: "restoring", - targetStatus: "destroying", - }), - ).toMatchObject({ kind: "accepted", nextStatus: "destroying" }); - // Confirmed teardown returns to cold; there is no `error` status. - expect( - decideRuntimeSubjectTransition({ currentStatus: "active", targetStatus: "cold" }), - ).toMatchObject({ kind: "accepted", nextStatus: "cold" }); - expect( - decideRuntimeSubjectTransition({ currentStatus: "restoring", targetStatus: "cold" }), - ).toMatchObject({ kind: "accepted", nextStatus: "cold" }); + test("keeps every subject operation transition explicit", () => { + const statuses = ["active", "backing_up", "cold", "destroying", "restoring"] as const; + const accepted = new Set([ + "active->backing_up", + "active->cold", + "active->destroying", + "backing_up->active", + "backing_up->cold", + "backing_up->destroying", + "cold->backing_up", + "cold->destroying", + "cold->restoring", + "destroying->cold", + "restoring->active", + "restoring->cold", + "restoring->destroying", + ]); + + for (const currentStatus of statuses) { + for (const targetStatus of statuses) { + const decision = decideRuntimeSubjectTransition({ currentStatus, targetStatus }); + expect(decision.kind).toBe( + currentStatus === targetStatus + ? "duplicate" + : accepted.has(`${currentStatus}->${targetStatus}`) + ? "accepted" + : "rejected", + ); + } + } }); test("rejects Pet Limited before lifecycle admission", async () => { @@ -387,16 +545,22 @@ describe("runtime subject lifecycle machine", () => { const database = createRuntimeSubjectLifecycleDatabase(); await insertRuntimeSubject(database, { status: "active" }); - await expect( - markRuntimeSubjectOperationStarted(database, { - now: 10, - runtimeSubjectId: "01J0000000000000000000000D", - status: "backing_up", - }), - ).resolves.toBe(true); + const lease = await markRuntimeSubjectOperationStarted(database, { + claimExpiresAt: 60_010, + claimOwner: "operation-test", + now: 10, + operationId: createPlatformId(), + operationKind: "hibernate", + runtimeSubjectId: "01J0000000000000000000000D", + }); + expect(lease).not.toBeNull(); + if (lease === null) { + throw new Error("Runtime subject operation lease was not created."); + } await expect( advanceRuntimeSubjectOperationStatus(database, { expectedStatus: "backing_up", + lease, runtimeSubjectId: "01J0000000000000000000000D", status: "destroying", }), @@ -404,6 +568,7 @@ describe("runtime subject lifecycle machine", () => { await markRuntimeSubjectCold(database, { clearBackups: false, expectedStatus: "destroying", + lease: { ...lease, status: "destroying" }, runtimeSubjectId: "01J0000000000000000000000D", }); @@ -415,22 +580,131 @@ describe("runtime subject lifecycle machine", () => { }); }); - test("does not let a stale operation completion overwrite a newer subject status", async () => { + test("does not let a stale operation completion mutate a newer incarnation", async () => { const database = createRuntimeSubjectLifecycleDatabase(); - await insertRuntimeSubject(database, { status: "active", statusSeq: 7 }); + await insertRuntimeSubject(database, { incarnation: 1, status: "active", statusSeq: 7 }); + await insertRuntimeConversationSession(database, { + incarnation: 1, + sessionId: SESSION_ID, + status: "active", + }); + await database + .prepare("INSERT INTO native_resume_ref (session_id) VALUES (?)") + .bind(SESSION_ID) + .run(); + const operationId = createPlatformId(); + const firstLease = await markRuntimeSubjectOperationStarted(database, { + claimExpiresAt: 10, + claimOwner: "first-worker", + now: 1, + operationId, + operationKind: "reset", + runtimeSubjectId: RUNTIME_SUBJECT_ID, + }); + expect(firstLease).not.toBeNull(); + if (firstLease === null) { + throw new Error("The first operation lease was not created."); + } + await expect( + advanceRuntimeSubjectOperationStatus(database, { + expectedStatus: "backing_up", + lease: firstLease, + runtimeSubjectId: RUNTIME_SUBJECT_ID, + status: "destroying", + }), + ).resolves.toBe(true); + const firstDestroyingLease = { ...firstLease, status: "destroying" as const }; + const batchEntered = Promise.withResolvers(); + const releaseFirstWorker = Promise.withResolvers(); + const gatedDatabase = { + batch: async (statements: D1PreparedStatement[]) => { + batchEntered.resolve(); + await releaseFirstWorker.promise; + return database.batch(statements); + }, + prepare: (query: string) => database.prepare(query), + } as D1Database; - await markRuntimeSubjectCold(database, { + const staleCompletion = markRuntimeSubjectCold(gatedDatabase, { clearBackups: false, - expectedStatus: "backing_up", - runtimeSubjectId: "01J0000000000000000000000D", + clearNativeResumeRefs: true, + expectedStatus: "destroying", + lease: firstDestroyingLease, + runtimeSubjectId: RUNTIME_SUBJECT_ID, }); + await batchEntered.promise; + + const takeoverLease = await claimRuntimeSubjectOperationForRepair(database, { + candidate: { + claimExpiresAt: 10, + claimOwner: "first-worker", + id: RUNTIME_SUBJECT_ID, + incarnation: 1, + kind: "cattle", + operationId, + operationKind: "reset", + status: "destroying", + }, + claimExpiresAt: 100, + claimOwner: "takeover-worker", + now: 11, + }); + expect(takeoverLease).not.toBeNull(); + if (takeoverLease === null) { + throw new Error("The takeover operation lease was not created."); + } + await expect( + markRuntimeSubjectCold(database, { + clearBackups: false, + clearNativeResumeRefs: true, + expectedStatus: "destroying", + lease: takeoverLease, + runtimeSubjectId: RUNTIME_SUBJECT_ID, + }), + ).resolves.toBe(true); + await database + .prepare( + `UPDATE sandbox + SET incarnation = 2, status = 'active', status_event = 'runtime_subject.active', + status_seq = status_seq + 1, status_source = 'test', updated_at = 12 + WHERE id = ?`, + ) + .bind(RUNTIME_SUBJECT_ID) + .run(); + await database + .prepare( + `UPDATE sandbox_session + SET sandbox_incarnation = 2, status = 'active', updated_at = 12 + WHERE session_id = ?`, + ) + .bind(SESSION_ID) + .run(); + await database + .prepare("INSERT INTO native_resume_ref (session_id) VALUES (?)") + .bind(SESSION_ID) + .run(); + releaseFirstWorker.resolve(); + + await expect(staleCompletion).resolves.toBe(false); await expect(readRuntimeSubject(database)).resolves.toEqual({ status: "active", status_event: "runtime_subject.active", - status_seq: 7, + status_seq: 11, status_source: "test", }); + await expect( + database + .prepare("SELECT sandbox_incarnation, status FROM sandbox_session WHERE session_id = ?") + .bind(SESSION_ID) + .first(), + ).resolves.toEqual({ sandbox_incarnation: 2, status: "active" }); + await expect( + database + .prepare("SELECT session_id FROM native_resume_ref WHERE session_id = ?") + .bind(SESSION_ID) + .first("session_id"), + ).resolves.toBe(SESSION_ID); }); test("lets interactive activation preempt best-effort prewarm activation claims", async () => { @@ -482,6 +756,71 @@ describe("runtime subject lifecycle machine", () => { }); }); + test("does not let prewarm retire an idle or shared Pet with a legacy network identity", async () => { + for (const hasActiveRun of [false, true]) { + const database = createRuntimeSubjectLifecycleDatabase(); + await insertRuntimeSubject(database, { incarnation: 4, kind: "pet", status: "active" }); + await database + .prepare("UPDATE sandbox SET network_constraints_hash = NULL WHERE id = ?") + .bind(RUNTIME_SUBJECT_ID) + .run(); + if (hasActiveRun) { + const driverInstanceId = createPlatformId(); + await database + .prepare( + `INSERT INTO driver_instance ( + id, sandbox_id, sandbox_incarnation, sandbox_session_id, status + ) VALUES (?, ?, 4, ?, 'ready')`, + ) + .bind(driverInstanceId, RUNTIME_SUBJECT_ID, SESSION_ID) + .run(); + await database + .prepare( + `INSERT INTO session_run (agent_id, driver_instance_id, id, session_id, status) + VALUES (?, ?, ?, ?, 'running')`, + ) + .bind(AGENT_ID, driverInstanceId, createPlatformId(), SESSION_ID) + .run(); + } + let destroyCalls = 0; + + await expect( + createRuntimeSubjectLifecycleService( + createBindings(database, { + onDestroy: () => { + destroyCalls++; + }, + }), + ).activate({ + ...RUNTIME_SUBJECT_QUOTA_SCOPE, + kind: "pet", + networkConstraints: { allowedHosts: [], networkPolicy: "full" }, + purpose: "prewarm", + runtimeSubjectId: RUNTIME_SUBJECT_ID, + subjectId: AGENT_ID, + subjectKind: "agent", + }), + ).rejects.toThrow("network retirement"); + + expect(destroyCalls).toBe(0); + await expect( + database + .prepare( + `SELECT claim_owner, incarnation, operation_kind, status, status_operation_id + FROM sandbox WHERE id = ?`, + ) + .bind(RUNTIME_SUBJECT_ID) + .first(), + ).resolves.toEqual({ + claim_owner: null, + incarnation: 4, + operation_kind: null, + status: "active", + status_operation_id: null, + }); + } + }); + test("captures one sandbox creation when a cold subject becomes active", async () => { const database = createRuntimeSubjectLifecycleDatabase(); await insertRuntimeSubject(database, { status: "cold" }); @@ -496,7 +835,7 @@ describe("runtime subject lifecycle machine", () => { } as ApiBindings; const service = createRuntimeSubjectLifecycleService(bindings); const activation = { - executionOwnerUserId: "01J00000000000000000000002", + ...RUNTIME_SUBJECT_QUOTA_SCOPE, kind: "cattle" as const, networkConstraints: { allowedHosts: [], networkPolicy: "full" as const }, runtimeSubjectId: RUNTIME_SUBJECT_ID, @@ -634,6 +973,120 @@ describe("runtime subject lifecycle machine", () => { expect((await readRuntimeSubject(database)).status).toBe("active"); }); + test("converges a lost recreate to cold while preserving the last ready backup", async () => { + const database = createRuntimeSubjectLifecycleDatabase(); + await insertRuntimeSubject(database, { + incarnation: 1, + kind: "pet", + lastBackupId: STORED_BACKUP_ID, + status: "active", + }); + let destroyCalls = 0; + + await expect( + recreateRuntimeSubjectPreservingState( + createBindings(database, { + inspectKind: "missing", + onDestroy: () => { + destroyCalls++; + }, + }), + { + operationId: createPlatformId(), + reason: "test missing recreate", + runtimeSubjectId: RUNTIME_SUBJECT_ID, + targets: [], + }, + ), + ).rejects.toMatchObject({ name: "RuntimeSubjectPhysicalStateLostError" }); + + expect(destroyCalls).toBe(1); + await expect( + database + .prepare( + `SELECT status, status_operation_id, claim_owner, last_backup_id, + last_error_code + FROM sandbox WHERE id = ?`, + ) + .bind(RUNTIME_SUBJECT_ID) + .first(), + ).resolves.toEqual({ + claim_owner: null, + last_backup_id: STORED_BACKUP_ID, + last_error_code: "runtime.subject_operation_failed", + status: "cold", + status_operation_id: null, + }); + }); + + test("retries a lost cold reset on a new incarnation and clears the old subject backup", async () => { + const database = createRuntimeSubjectLifecycleDatabase(); + await insertReadySubjectBackup(database, 1); + await insertRuntimeSubject(database, { + incarnation: 1, + kind: "pet", + lastBackupId: STORED_BACKUP_ID, + status: "active", + }); + const restoredBackups: Array<{ readonly dir: string; readonly id: string }> = []; + const clearCommands: string[] = []; + const bindings = createBindings(database); + bindings.runtimeSubjectHandleFactory = (physicalId) => + createSandboxHandle({ + inspectKind: physicalId === `${RUNTIME_SUBJECT_ID}-i1` ? "missing" : "healthy", + onExec: (command) => { + clearCommands.push(command); + }, + onRestore: (backup) => { + restoredBackups.push(backup); + }, + }); + + await expect( + resetRuntimeSubjectAgentState(bindings, { + operationId: createPlatformId(), + reason: "test missing reset", + runtimeSubjectId: RUNTIME_SUBJECT_ID, + targets: [], + }), + ).rejects.toMatchObject({ name: "RuntimeSubjectPhysicalStateLostError" }); + await expect( + database + .prepare("SELECT status, last_backup_id FROM sandbox WHERE id = ?") + .bind(RUNTIME_SUBJECT_ID) + .first(), + ).resolves.toEqual({ last_backup_id: STORED_BACKUP_ID, status: "cold" }); + + await expect( + resetRuntimeSubjectAgentState(bindings, { + operationId: createPlatformId(), + reason: "retry missing reset", + runtimeSubjectId: RUNTIME_SUBJECT_ID, + targets: [], + }), + ).resolves.toBeUndefined(); + + expect(restoredBackups).toEqual([{ dir: "/workspace/memory", id: CLOUDFLARE_BACKUP_ID }]); + expect(clearCommands).toHaveLength(1); + await expect( + database + .prepare( + `SELECT incarnation, status, last_backup_id, last_restore_backup_id, + last_error, last_error_code + FROM sandbox WHERE id = ?`, + ) + .bind(RUNTIME_SUBJECT_ID) + .first(), + ).resolves.toEqual({ + incarnation: 2, + last_backup_id: null, + last_error: null, + last_error_code: null, + last_restore_backup_id: null, + status: "cold", + }); + }); + test("lets interactive activation retry after a prior activation failure", async () => { const database = createRuntimeSubjectLifecycleDatabase(); // A prior activation failure leaves the subject cold (no live container) @@ -787,6 +1240,7 @@ describe("runtime subject lifecycle machine", () => { createBindings(database, { destroyPromise }), RUNTIME_SUBJECT_ID, 5, + 5, ), ).rejects.toThrow("Runtime subject destroy"); }); @@ -865,4 +1319,491 @@ describe("runtime subject lifecycle machine", () => { expect(recovered.subject).toBeTruthy(); expect((await readRuntimeSubject(database)).status).toBe("active"); }); + + test("disposes an activation handle that fails before it can be returned", async () => { + const database = createRuntimeSubjectLifecycleDatabase(); + await insertRuntimeSubject(database, { status: "cold" }); + const bindings = createBindings(database); + let createdHandles = 0; + let activationHandleDisposals = 0; + bindings.runtimeSubjectHandleFactory = () => { + const isActivationHandle = createdHandles++ === 0; + return createSandboxHandle( + isActivationHandle + ? { + onDispose: () => (activationHandleDisposals += 1), + prepareError: new Error("injected prepare failure"), + } + : {}, + ); + }; + + await expect( + createRuntimeSubjectLifecycleService(bindings).activate({ + ...RUNTIME_SUBJECT_QUOTA_SCOPE, + kind: "cattle", + networkConstraints: { allowedHosts: [], networkPolicy: "full" }, + runtimeSubjectId: RUNTIME_SUBJECT_ID, + subjectId: SESSION_ID, + subjectKind: "session", + }), + ).rejects.toThrow("injected prepare failure"); + + expect(activationHandleDisposals).toBe(1); + }); + + test("routes each incarnation to one physical sandbox id", async () => { + const physicalIds: string[] = []; + const bindings = createBindings(createRuntimeSubjectLifecycleDatabase()); + bindings.runtimeSubjectHandleFactory = (physicalId) => { + physicalIds.push(physicalId); + return createSandboxHandle(); + }; + + await getRuntimeSubjectKeepAliveHandle(bindings, RUNTIME_SUBJECT_ID, 0); + await getRuntimeSubjectKeepAliveHandle(bindings, RUNTIME_SUBJECT_ID, 36); + + expect(physicalIds).toEqual([RUNTIME_SUBJECT_ID, `${RUNTIME_SUBJECT_ID}-i10`]); + }); + + test("recovers a missing active runtime into a new physical incarnation", async () => { + const database = createRuntimeSubjectLifecycleDatabase(); + await insertRuntimeSubject(database, { status: "active" }); + await insertRuntimeConversationSession(database, { + incarnation: 4, + sessionId: SESSION_ID, + status: "active", + }); + await insertRuntimeConversationSession(database, { + incarnation: 4, + sessionId: "01J0000000000000000000000A", + status: "cleanup_pending", + }); + await database + .prepare( + `INSERT INTO sandbox_backup ( + created_at, dir, error_message, id, keep, sandbox_id, sandbox_incarnation, + status, ttl_seconds, updated_at + ) VALUES (?, ?, NULL, ?, false, ?, ?, 'ready', 600, ?)`, + ) + .bind(1, "/workspace/memory", STORED_BACKUP_ID, RUNTIME_SUBJECT_ID, 4, 1) + .run(); + await database + .prepare( + `UPDATE sandbox + SET incarnation = 4, kind = 'pet', last_backup_id = ?, subject_id = ?, subject_kind = 'agent' + WHERE id = ?`, + ) + .bind(STORED_BACKUP_ID, AGENT_ID, RUNTIME_SUBJECT_ID) + .run(); + const provisioningAuthority = await insertRuntimeProvisioningAuthority(database, 4); + + let releaseOldMutation: (() => void) | undefined; + let oldRetired = false; + const oldMutationBarrier = new Promise((resolve) => { + releaseOldMutation = resolve; + }); + const oldHandle = { + ...createSandboxHandle({ inspectKind: "missing" }), + destroyRuntimeSubjectIncarnation: async () => { + oldRetired = true; + return { kind: "destroyed" as const }; + }, + exec: async () => { + await oldMutationBarrier; + if (oldRetired) { + throw new Error("Runtime subject incarnation is retired."); + } + return { exitCode: 0, stderr: "", stdout: "", success: true }; + }, + } as SandboxHandle; + let restored = false; + let readyIncarnation: number | null = null; + const newHandle = createSandboxHandle({ + onReady: (incarnation) => { + readyIncarnation = incarnation; + }, + onRestore: () => { + restored = true; + }, + }); + const physicalIds: string[] = []; + const bindings = createBindings(database); + bindings.runtimeSubjectHandleFactory = (physicalId) => { + physicalIds.push(physicalId); + return physicalId === `${RUNTIME_SUBJECT_ID}-i4` ? oldHandle : newHandle; + }; + + const staleMutation = oldHandle.exec("stale-write"); + const lifecycle = createRuntimeSubjectLifecycleService(bindings); + const activationInput = { + ...RUNTIME_SUBJECT_QUOTA_SCOPE, + kind: "pet" as const, + networkConstraints: { allowedHosts: [], networkPolicy: "full" as const }, + provisioningAuthority, + runtimeSubjectId: RUNTIME_SUBJECT_ID, + subjectId: AGENT_ID, + subjectKind: "agent" as const, + }; + + await expect(lifecycle.activate(activationInput)).rejects.toThrow("retired"); + const activation = await lifecycle.activate(activationInput); + + releaseOldMutation?.(); + await expect(staleMutation).rejects.toThrow("retired"); + expect(activation.incarnation).toBe(5); + expect(restored).toBe(true); + expect(readyIncarnation).toBe(5); + expect(physicalIds).toContain(`${RUNTIME_SUBJECT_ID}-i4`); + expect(physicalIds).toContain(`${RUNTIME_SUBJECT_ID}-i5`); + await expect( + database + .prepare("SELECT incarnation, status FROM sandbox WHERE id = ?") + .bind(RUNTIME_SUBJECT_ID) + .first(), + ).resolves.toEqual({ incarnation: 5, status: "active" }); + await expect( + database + .prepare("SELECT sandbox_incarnation, status FROM sandbox_session WHERE session_id = ?") + .bind(SESSION_ID) + .first(), + ).resolves.toEqual({ sandbox_incarnation: 4, status: "closed" }); + await expect( + database + .prepare( + "SELECT cleanup_operation_id, sandbox_incarnation, status FROM sandbox_session WHERE session_id = ?", + ) + .bind("01J0000000000000000000000A") + .first(), + ).resolves.toEqual({ cleanup_operation_id: null, sandbox_incarnation: 4, status: "closed" }); + }); + + test("retires an ambiguously timed-out restoring incarnation before retry", async () => { + const database = createRuntimeSubjectLifecycleDatabase(); + await insertRuntimeSubject(database, { incarnation: 3, kind: "pet", status: "cold" }); + + let releaseOldMutation: (() => void) | undefined; + let oldRetired = false; + const oldMutationBarrier = new Promise((resolve) => { + releaseOldMutation = resolve; + }); + const oldHandle = { + ...createSandboxHandle({ + inspectKind: "healthy", + prepareError: createTimeoutError({ + label: "Runtime subject filesystem prepare", + timeoutMs: 15_000, + }), + }), + destroyRuntimeSubjectIncarnation: async () => { + oldRetired = true; + return { kind: "destroyed" as const }; + }, + exec: async () => { + await oldMutationBarrier; + if (oldRetired) { + throw new Error("Runtime subject incarnation is retired."); + } + return { exitCode: 0, stderr: "", stdout: "late", success: true }; + }, + } as SandboxHandle; + const newHandle = createSandboxHandle(); + const bindings = createBindings(database); + bindings.runtimeSubjectHandleFactory = (physicalId) => + physicalId === `${RUNTIME_SUBJECT_ID}-i4` ? oldHandle : newHandle; + const lifecycle = createRuntimeSubjectLifecycleService(bindings); + const activationInput = { + ...RUNTIME_SUBJECT_QUOTA_SCOPE, + kind: "pet" as const, + networkConstraints: { allowedHosts: [], networkPolicy: "full" as const }, + runtimeSubjectId: RUNTIME_SUBJECT_ID, + subjectId: AGENT_ID, + subjectKind: "agent" as const, + }; + + const staleMutation = oldHandle.exec("late-write"); + await expect(lifecycle.activate(activationInput)).rejects.toThrow("timed out"); + await expect( + database + .prepare("SELECT incarnation, status FROM sandbox WHERE id = ?") + .bind(RUNTIME_SUBJECT_ID) + .first(), + ).resolves.toEqual({ incarnation: 4, status: "cold" }); + + const recovered = await lifecycle.activate(activationInput); + releaseOldMutation?.(); + await expect(staleMutation).rejects.toThrow("retired"); + expect(recovered.incarnation).toBe(5); + }); + + test("keeps an active incarnation on an unknown health result", async () => { + const database = createRuntimeSubjectLifecycleDatabase(); + await insertRuntimeSubject(database, { status: "active" }); + await database + .prepare("UPDATE sandbox SET incarnation = 4 WHERE id = ?") + .bind(RUNTIME_SUBJECT_ID) + .run(); + let destroyCalls = 0; + let disposeCalls = 0; + + await expect( + createRuntimeSubjectLifecycleService( + createBindings(database, { + inspectKind: "unknown", + onDestroy: () => (destroyCalls += 1), + onDispose: () => (disposeCalls += 1), + }), + ).activate({ + ...RUNTIME_SUBJECT_QUOTA_SCOPE, + kind: "cattle", + networkConstraints: { allowedHosts: [], networkPolicy: "full" }, + runtimeSubjectId: RUNTIME_SUBJECT_ID, + subjectId: SESSION_ID, + subjectKind: "session", + }), + ).rejects.toThrow("health is unknown"); + + expect(destroyCalls).toBe(0); + expect(disposeCalls).toBe(1); + await expect( + database + .prepare("SELECT claim_owner, incarnation, status FROM sandbox WHERE id = ?") + .bind(RUNTIME_SUBJECT_ID) + .first(), + ).resolves.toEqual({ claim_owner: null, incarnation: 4, status: "active" }); + }); + + test("retires a cattle conversation after its missing incarnation is destroyed", async () => { + const database = createRuntimeSubjectLifecycleDatabase(); + await insertRuntimeSubject(database, { status: "active" }); + await database + .prepare("UPDATE sandbox SET incarnation = 4 WHERE id = ?") + .bind(RUNTIME_SUBJECT_ID) + .run(); + await insertRuntimeConversationSession(database, { + incarnation: 4, + sessionId: SESSION_ID, + status: "error", + }); + const provisioningAuthority = await insertRuntimeProvisioningAuthority(database, 4); + const bindings = createBindings(database, { inspectKind: "missing" }); + + const lifecycle = createRuntimeSubjectLifecycleService(bindings); + const activationInput = { + ...RUNTIME_SUBJECT_QUOTA_SCOPE, + kind: "cattle" as const, + networkConstraints: { allowedHosts: [], networkPolicy: "full" as const }, + provisioningAuthority, + runtimeSubjectId: RUNTIME_SUBJECT_ID, + subjectId: SESSION_ID, + subjectKind: "session" as const, + }; + + await expect(lifecycle.activate(activationInput)).rejects.toThrow("retired"); + const activation = await lifecycle.activate(activationInput); + + expect(activation.incarnation).toBe(5); + await expect( + database + .prepare("SELECT sandbox_incarnation, status FROM sandbox_session WHERE session_id = ?") + .bind(SESSION_ID) + .first(), + ).resolves.toEqual({ sandbox_incarnation: 4, status: "closed" }); + }); + + test("repairs active-incarnation retirement without closing a newer conversation", async () => { + const database = createRuntimeSubjectLifecycleDatabase(); + await insertRuntimeSubject(database, { status: "active" }); + await insertRuntimeConversationSession(database, { + incarnation: 4, + sessionId: SESSION_ID, + status: "active", + }); + await insertRuntimeConversationSession(database, { + incarnation: 4, + sessionId: "01J00000000000000000000008", + status: "cleanup_pending", + }); + const newerSessionId = "01J0000000000000000000000A"; + await insertRuntimeConversationSession(database, { + incarnation: 5, + sessionId: newerSessionId, + status: "active", + }); + const operationId = createPlatformId(); + const claimExpiresAt = Date.now() + 60_000; + await database + .prepare( + `UPDATE sandbox + SET claim_expires_at = ?, claim_owner = 'activation-repair', incarnation = 4, + operation_kind = 'activate', status = 'destroying', status_operation_id = ? + WHERE id = ?`, + ) + .bind(claimExpiresAt, operationId, RUNTIME_SUBJECT_ID) + .run(); + + await runRuntimeSubjectOperation(createBindings(database), { + kind: "pet", + lease: { + claimExpiresAt, + claimOwner: "activation-repair", + incarnation: 4, + kind: "activate", + operationId, + status: "destroying", + }, + reason: "repair active incarnation retirement", + runtimeSubjectId: RUNTIME_SUBJECT_ID, + }); + + await expect( + database + .prepare( + `SELECT cleanup_operation_id, sandbox_incarnation, status + FROM sandbox_session + ORDER BY session_id`, + ) + .all(), + ).resolves.toMatchObject({ + results: [ + { cleanup_operation_id: null, sandbox_incarnation: 4, status: "closed" }, + { cleanup_operation_id: null, sandbox_incarnation: 4, status: "closed" }, + { cleanup_operation_id: null, sandbox_incarnation: 5, status: "active" }, + ], + }); + expect((await readRuntimeSubject(database)).status).toBe("cold"); + }); + + test("keeps generic activation repair draining until every exact-incarnation Run is terminal", async () => { + const database = createRuntimeSubjectLifecycleDatabase(); + await insertRuntimeSubject(database, { incarnation: 4, kind: "pet", status: "active" }); + const operationId = createPlatformId(); + const driverInstanceId = createPlatformId(); + const runId = createPlatformId(); + await database + .prepare( + `UPDATE sandbox + SET claim_expires_at = 1, claim_owner = 'expired-activation', + operation_kind = 'activate', status = 'destroying', status_operation_id = ? + WHERE id = ?`, + ) + .bind(operationId, RUNTIME_SUBJECT_ID) + .run(); + await database + .prepare( + `INSERT INTO driver_instance ( + id, sandbox_id, sandbox_incarnation, sandbox_session_id, status + ) VALUES (?, ?, 4, ?, 'ready')`, + ) + .bind(driverInstanceId, RUNTIME_SUBJECT_ID, SESSION_ID) + .run(); + await database + .prepare( + `INSERT INTO session_run (agent_id, driver_instance_id, id, session_id, status) + VALUES (?, ?, ?, ?, 'running')`, + ) + .bind(AGENT_ID, driverInstanceId, runId, SESSION_ID) + .run(); + const firstRepairLease = await claimRuntimeSubjectOperationForRepair(database, { + candidate: { + claimExpiresAt: 1, + claimOwner: "expired-activation", + id: RUNTIME_SUBJECT_ID, + incarnation: 4, + kind: "pet", + operationId, + operationKind: "activate", + status: "destroying", + }, + claimExpiresAt: 60_002, + claimOwner: "first-repair", + now: 2, + }); + expect(firstRepairLease).not.toBeNull(); + if (firstRepairLease === null) { + return; + } + let destroyCalls = 0; + const bindings = createBindings(database, { + onDestroy: () => { + destroyCalls++; + }, + }); + + await expect( + runRuntimeSubjectOperation(bindings, { + kind: "pet", + lease: firstRepairLease, + reason: "test activation retirement drain", + runtimeSubjectId: RUNTIME_SUBJECT_ID, + }), + ).rejects.toThrow("still draining active Runs"); + expect(destroyCalls).toBe(0); + const waiting = await database + .prepare( + `SELECT claim_expires_at, claim_owner, operation_kind, status, status_operation_id + FROM sandbox WHERE id = ?`, + ) + .bind(RUNTIME_SUBJECT_ID) + .first<{ + claim_expires_at: number; + claim_owner: string; + operation_kind: "activate"; + status: "destroying"; + status_operation_id: RuntimeOperationId; + }>(); + expect(waiting).toMatchObject({ + claim_owner: "first-repair", + operation_kind: "activate", + status: "destroying", + status_operation_id: operationId, + }); + if (waiting === null) { + return; + } + + await database + .prepare("UPDATE session_run SET status = 'completed' WHERE id = ?") + .bind(runId) + .run(); + await database + .prepare("UPDATE driver_instance SET status = 'stopped' WHERE id = ?") + .bind(driverInstanceId) + .run(); + const nextNow = waiting.claim_expires_at + 1; + const nextRepairLease = await claimRuntimeSubjectOperationForRepair(database, { + candidate: { + claimExpiresAt: waiting.claim_expires_at, + claimOwner: waiting.claim_owner, + id: RUNTIME_SUBJECT_ID, + incarnation: 4, + kind: "pet", + operationId, + operationKind: "activate", + status: "destroying", + }, + claimExpiresAt: nextNow + 60_000, + claimOwner: "next-repair", + now: nextNow, + }); + expect(nextRepairLease).not.toBeNull(); + if (nextRepairLease === null) { + return; + } + + await runRuntimeSubjectOperation(bindings, { + kind: "pet", + lease: nextRepairLease, + reason: "test activation retirement drained", + runtimeSubjectId: RUNTIME_SUBJECT_ID, + }); + + expect(destroyCalls).toBe(1); + await expect( + database + .prepare("SELECT claim_owner, operation_kind, status FROM sandbox WHERE id = ?") + .bind(RUNTIME_SUBJECT_ID) + .first(), + ).resolves.toEqual({ claim_owner: null, operation_kind: null, status: "cold" }); + }); }); diff --git a/apps/api/tests/runtime-subject-maintenance.test.ts b/apps/api/tests/runtime-subject-maintenance.test.ts index 8160c4cc..3d8c3523 100644 --- a/apps/api/tests/runtime-subject-maintenance.test.ts +++ b/apps/api/tests/runtime-subject-maintenance.test.ts @@ -1,12 +1,26 @@ import { describe, expect, test } from "bun:test"; -import { expireStaleReschedulingSessions } from "../src/modules/runtime/infrastructure/runtime-subject-lifecycle/runtime-subject-maintenance.service"; +import { buildRuntimeStateOperationEvents } from "../src/modules/runtime/application/runtime-state-operation-events"; +import { commitRuntimeOperationReadySnapshots } from "../src/modules/runtime/application/runtime-state-operation-target-events"; +import { claimRuntimeOperationTargets } from "../src/modules/runtime/application/runtime-state-operation-target-store"; +import { recordCanonicalSessionRunTerminal } from "../src/modules/runtime/application/session-runs/session-run-terminal-failure.service"; +import { + expireStaleReschedulingSessions, + repairStaleRuntimeOperationTargets, + runSandboxMaintenance, +} from "../src/modules/runtime/infrastructure/runtime-subject-lifecycle/runtime-subject-maintenance.service"; import type { ApiBindings } from "../src/platform/cloudflare/worker-types"; import { + PUBLIC_API_TEST_IDS, createPublicHttpContractDatabase, createPublicHttpTestBindings, + insertActiveSandboxSessionFixture, insertNonOwnerSession, + insertOwnerSession, } from "./helpers/public-api-http-test-fixture"; +import type { SqliteD1Database } from "./helpers/sqlite-d1"; + +const RESCHEDULING_RUN_ID = "01J0000000000000000000000R"; async function insertRunningSessionRun(database: D1Database): Promise { await database @@ -30,7 +44,7 @@ async function insertRunningSessionRun(database: D1Database): Promise { `, ) .bind( - "run-rescheduling", + RESCHEDULING_RUN_ID, "01J0000000000000000000000B", "01J00000000000000000000009", "01J00000000000000000000002", @@ -46,11 +60,104 @@ async function insertRunningSessionRun(database: D1Database): Promise { .run(); await database .prepare("UPDATE session SET last_run_id = ?, status = ?, updated_at = ? WHERE id = ?") - .bind("run-rescheduling", "RESCHEDULING", 1, "01J0000000000000000000000B") + .bind(RESCHEDULING_RUN_ID, "RESCHEDULING", 1, "01J0000000000000000000000B") .run(); } +async function insertSandboxSession(database: SqliteD1Database): Promise { + await insertActiveSandboxSessionFixture(database, { + cwd: "/workspace", + ownerAccountId: PUBLIC_API_TEST_IDS.nonOwnerAccount, + sandboxId: PUBLIC_API_TEST_IDS.sandbox, + sandboxSessionId: "01J0000000000000000000000S", + sessionId: PUBLIC_API_TEST_IDS.nonOwnerSession, + timestampMs: 1, + }); +} + +function beforeFirstBatch(database: D1Database, action: () => Promise): D1Database { + let raced = false; + + return new Proxy(database, { + get(target, property) { + if (property === "batch") { + return async (statements: D1PreparedStatement[]) => { + if (!raced) { + raced = true; + await action(); + } + return target.batch(statements); + }; + } + + const value = Reflect.get(target, property); + return typeof value === "function" ? value.bind(target) : value; + }, + }); +} + describe("runtime subject maintenance", () => { + test("continues later maintenance after a terminal projection is poisoned", async () => { + const database = await createPublicHttpContractDatabase(); + await Promise.all([insertOwnerSession(database), insertNonOwnerSession(database)]); + await database + .prepare( + `INSERT INTO session_run ( + id, session_id, agent_id, created_by_account_id, trigger, status, + provider, model, runtime_id, trace_id, started_at, completed_at, + created_at, updated_at + ) VALUES (?, ?, ?, ?, 'user_prompt', 'completed', 'openai', 'gpt-5.4', + 'openai-runtime', 'trace-poisoned-terminal', 1, 1, 1, 1)`, + ) + .bind( + PUBLIC_API_TEST_IDS.run, + PUBLIC_API_TEST_IDS.ownerSession, + PUBLIC_API_TEST_IDS.agent, + PUBLIC_API_TEST_IDS.ownerAccount, + ) + .run(); + await database + .prepare( + `INSERT INTO session_message ( + content_text, created_at, created_by_account_id, id, plan_json, + projection_format, role, segments_json, seq, session_id, session_run_id + ) VALUES ('invalid carrier', 1, ?, ?, NULL, 'materialized', 'assistant', + NULL, 1, ?, ?)`, + ) + .bind( + PUBLIC_API_TEST_IDS.ownerAccount, + PUBLIC_API_TEST_IDS.run, + PUBLIC_API_TEST_IDS.ownerSession, + PUBLIC_API_TEST_IDS.run, + ) + .run(); + await database + .prepare("UPDATE session SET last_run_id = ?, status = 'IDLE' WHERE id = ?") + .bind(PUBLIC_API_TEST_IDS.run, PUBLIC_API_TEST_IDS.ownerSession) + .run(); + await database + .prepare("UPDATE session SET status = 'RESCHEDULING', updated_at = 1 WHERE id = ?") + .bind(PUBLIC_API_TEST_IDS.nonOwnerSession) + .run(); + + await runSandboxMaintenance(createPublicHttpTestBindings(database) as ApiBindings); + + await expect( + database + .prepare("SELECT status FROM session WHERE id = ?") + .bind(PUBLIC_API_TEST_IDS.nonOwnerSession) + .first(), + ).resolves.toEqual({ status: "TERMINATED" }); + await expect( + database + .prepare("SELECT terminal_reconciliation_attempted_at FROM session_run WHERE id = ?") + .bind(PUBLIC_API_TEST_IDS.run) + .first(), + ).resolves.toMatchObject({ + terminal_reconciliation_attempted_at: expect.any(Number), + }); + }); + test("expires stale rescheduling sessions", async () => { const database = await createPublicHttpContractDatabase(); await insertNonOwnerSession(database); @@ -58,16 +165,27 @@ describe("runtime subject maintenance", () => { const bindings = createPublicHttpTestBindings(database) as ApiBindings; + await expireStaleReschedulingSessions(bindings); await expireStaleReschedulingSessions(bindings); const run = await database .prepare("SELECT error_code, status FROM session_run WHERE id = ?") - .bind("run-rescheduling") + .bind(RESCHEDULING_RUN_ID) .first<{ error_code: string | null; status: string }>(); expect(run).toEqual({ error_code: "session.rescheduling_timeout", status: "failed", }); + const session = await database + .prepare("SELECT status FROM session WHERE id = ?") + .bind("01J0000000000000000000000B") + .first<{ status: string }>(); + const terminalEvents = await database + .prepare("SELECT COUNT(*) AS count FROM session_event WHERE run_id = ? AND event_type = ?") + .bind(RESCHEDULING_RUN_ID, "run.failed") + .first<{ count: number }>(); + expect(session).toEqual({ status: "TERMINATED" }); + expect(terminalEvents).toEqual({ count: 1 }); }); test("does not expire runtime operation owned rescheduling sessions", async () => { @@ -88,7 +206,7 @@ describe("runtime subject maintenance", () => { .first<{ status: string; status_operation_id: string | null }>(); const run = await database .prepare("SELECT error_code, status FROM session_run WHERE id = ?") - .bind("run-rescheduling") + .bind(RESCHEDULING_RUN_ID) .first<{ error_code: string | null; status: string }>(); expect(session).toEqual({ @@ -100,4 +218,381 @@ describe("runtime subject maintenance", () => { status: "running", }); }); + + test("does not append a lifecycle timeout after a no-Run Session reconnects", async () => { + const database = await createPublicHttpContractDatabase(); + await insertNonOwnerSession(database); + await database + .prepare( + `UPDATE session + SET status = 'RESCHEDULING', status_seq = 1, updated_at = 1 + WHERE id = ?`, + ) + .bind(PUBLIC_API_TEST_IDS.nonOwnerSession) + .run(); + const racingDatabase = beforeFirstBatch(database, async () => { + await database + .prepare( + `UPDATE session + SET status = 'RUNNING', status_seq = status_seq + 1, updated_at = 2 + WHERE id = ?`, + ) + .bind(PUBLIC_API_TEST_IDS.nonOwnerSession) + .run(); + }); + + await expireStaleReschedulingSessions({ + ...(createPublicHttpTestBindings(database) as ApiBindings), + DB: racingDatabase, + }); + + await expect( + database + .prepare("SELECT status, status_seq, updated_at FROM session WHERE id = ?") + .bind(PUBLIC_API_TEST_IDS.nonOwnerSession) + .first(), + ).resolves.toEqual({ status: "RUNNING", status_seq: 2, updated_at: 2 }); + await expect( + database.prepare("SELECT COUNT(*) AS count FROM session_event").first(), + ).resolves.toEqual({ count: 0 }); + }); + + test("does not fail an active Run after its stale Session reconnects", async () => { + const database = await createPublicHttpContractDatabase(); + await insertNonOwnerSession(database); + await insertRunningSessionRun(database); + const racingDatabase = beforeFirstBatch(database, async () => { + await database + .prepare( + `UPDATE session + SET status = 'RUNNING', status_seq = status_seq + 1, updated_at = 2 + WHERE id = ?`, + ) + .bind(PUBLIC_API_TEST_IDS.nonOwnerSession) + .run(); + }); + + await expireStaleReschedulingSessions({ + ...(createPublicHttpTestBindings(database) as ApiBindings), + DB: racingDatabase, + }); + + await expect( + database + .prepare("SELECT error_code, status FROM session_run WHERE id = ?") + .bind(RESCHEDULING_RUN_ID) + .first(), + ).resolves.toEqual({ error_code: null, status: "running" }); + await expect( + database + .prepare("SELECT status, status_seq, updated_at FROM session WHERE id = ?") + .bind(PUBLIC_API_TEST_IDS.nonOwnerSession) + .first(), + ).resolves.toEqual({ status: "RUNNING", status_seq: 1, updated_at: 2 }); + await expect( + database.prepare("SELECT COUNT(*) AS count FROM session_event").first(), + ).resolves.toEqual({ count: 0 }); + }); + + test("converges an operation target after the atomic start claim crashes", async () => { + const database = await createPublicHttpContractDatabase(); + await insertNonOwnerSession(database); + await insertRunningSessionRun(database); + await insertSandboxSession(database); + await database + .prepare("UPDATE session SET status = 'RUNNING' WHERE id = ?") + .bind(PUBLIC_API_TEST_IDS.nonOwnerSession) + .run(); + const operationId = PUBLIC_API_TEST_IDS.operation; + const [updatingEvent] = buildRuntimeStateOperationEvents({ + agentId: PUBLIC_API_TEST_IDS.agent, + operation: "restartDriver", + readyAt: "1970-01-01T00:00:00.002Z", + startedAt: "1970-01-01T00:00:00.001Z", + }); + const claimed = await claimRuntimeOperationTargets(database, { + event: updatingEvent, + operationId, + targets: [ + { + agentId: PUBLIC_API_TEST_IDS.agent, + creatorAccountId: PUBLIC_API_TEST_IDS.nonOwnerAccount, + lastRunId: RESCHEDULING_RUN_ID, + sandboxId: PUBLIC_API_TEST_IDS.sandbox, + sessionId: PUBLIC_API_TEST_IDS.nonOwnerSession, + sessionRuntimeEventSeqCursor: 0, + sessionStatus: "RUNNING", + sessionStatusOperationId: null, + sessionStatusSeq: 0, + sessionUpdatedAt: 1, + }, + ], + }); + expect(claimed).toHaveLength(1); + expect( + await database + .prepare("SELECT status, status_operation_id, updated_at FROM session WHERE id = ?") + .bind(PUBLIC_API_TEST_IDS.nonOwnerSession) + .first(), + ).toEqual({ + status: "RESCHEDULING", + status_operation_id: operationId, + updated_at: 1, + }); + const bindings = createPublicHttpTestBindings(database) as ApiBindings; + + expect( + await repairStaleRuntimeOperationTargets(bindings, { + limit: 10, + staleUpdatedAtLte: 1, + }), + ).toBe(1); + expect( + await repairStaleRuntimeOperationTargets(bindings, { + limit: 10, + staleUpdatedAtLte: 1, + }), + ).toBe(0); + + expect( + await database + .prepare( + `SELECT session.status AS session_status, + session.status_operation_id, + session_run.completed_at, + session_run.error_code, + session_run.status AS run_status + FROM session + JOIN session_run ON session_run.id = session.last_run_id + WHERE session.id = ?`, + ) + .bind(PUBLIC_API_TEST_IDS.nonOwnerSession) + .first(), + ).toEqual({ + completed_at: 120_001, + error_code: "agent.runtime_state_operation_timeout", + run_status: "expired", + session_status: "TERMINATED", + status_operation_id: null, + }); + expect( + await database + .prepare( + `SELECT event_type, occurred_at + FROM session_event + WHERE run_id = ? OR event_type = 'agent.task.updated' + ORDER BY seq`, + ) + .bind(RESCHEDULING_RUN_ID) + .all(), + ).toMatchObject({ + results: [ + { event_type: "agent.task.updated", occurred_at: 1 }, + { event_type: "run.cancelled", occurred_at: 120_001 }, + ], + }); + }); + + test("leaves a durable ready winner unchanged during maintenance", async () => { + const database = await createPublicHttpContractDatabase(); + await insertNonOwnerSession(database); + await insertSandboxSession(database); + await database + .prepare("UPDATE session SET updated_at = 1 WHERE id = ?") + .bind(PUBLIC_API_TEST_IDS.nonOwnerSession) + .run(); + const operationId = PUBLIC_API_TEST_IDS.operation; + const [updatingEvent, readyEvent] = buildRuntimeStateOperationEvents({ + agentId: PUBLIC_API_TEST_IDS.agent, + operation: "restartDriver", + readyAt: "1970-01-01T00:00:00.002Z", + startedAt: "1970-01-01T00:00:00.001Z", + }); + const [claimed] = await claimRuntimeOperationTargets(database, { + event: updatingEvent, + operationId, + targets: [ + { + agentId: PUBLIC_API_TEST_IDS.agent, + creatorAccountId: PUBLIC_API_TEST_IDS.nonOwnerAccount, + lastRunId: null, + sandboxId: PUBLIC_API_TEST_IDS.sandbox, + sessionId: PUBLIC_API_TEST_IDS.nonOwnerSession, + sessionRuntimeEventSeqCursor: 0, + sessionStatus: "IDLE", + sessionStatusOperationId: null, + sessionStatusSeq: 0, + sessionUpdatedAt: 1, + }, + ], + }); + if (claimed === undefined) { + throw new Error("Missing runtime operation claim fixture."); + } + const bindings = createPublicHttpTestBindings(database) as ApiBindings; + await commitRuntimeOperationReadySnapshots(bindings, { + event: readyEvent, + operationId, + targets: [claimed.current], + }); + + expect( + await repairStaleRuntimeOperationTargets(bindings, { + limit: 10, + staleUpdatedAtLte: 1, + }), + ).toBe(0); + await expect( + database + .prepare( + `SELECT runtime_event_seq_cursor, status, status_operation_id, updated_at + FROM session + WHERE id = ?`, + ) + .bind(PUBLIC_API_TEST_IDS.nonOwnerSession) + .first(), + ).resolves.toEqual({ + runtime_event_seq_cursor: 2, + status: "IDLE", + status_operation_id: null, + updated_at: 2, + }); + await expect( + database.prepare("SELECT event_type, source_event_id FROM session_event ORDER BY seq").all(), + ).resolves.toMatchObject({ + results: [ + { + event_type: "agent.task.updated", + source_event_id: `runtime-operation:${operationId}:${PUBLIC_API_TEST_IDS.nonOwnerSession}:updating`, + }, + { + event_type: "agent.task.updated", + source_event_id: `runtime-operation:${operationId}:${PUBLIC_API_TEST_IDS.nonOwnerSession}:ready`, + }, + ], + }); + }); + + test("does not adopt archive or delete cleanup ownership as a runtime operation", async () => { + for (const cleanupOperationKind of ["archive", "delete"] as const) { + const database = await createPublicHttpContractDatabase(); + await insertNonOwnerSession(database); + await insertSandboxSession(database); + await database + .prepare( + `UPDATE session + SET archived_at = 1, + cleanup_operation_kind = ?, + status = 'RESCHEDULING', + status_operation_id = ?, + updated_at = 1 + WHERE id = ?`, + ) + .bind( + cleanupOperationKind, + PUBLIC_API_TEST_IDS.operation, + PUBLIC_API_TEST_IDS.nonOwnerSession, + ) + .run(); + + expect( + await repairStaleRuntimeOperationTargets( + createPublicHttpTestBindings(database) as ApiBindings, + { limit: 10, staleUpdatedAtLte: 1 }, + ), + ).toBe(0); + expect( + await database + .prepare( + "SELECT cleanup_operation_kind, status, status_operation_id FROM session WHERE id = ?", + ) + .bind(PUBLIC_API_TEST_IDS.nonOwnerSession) + .first(), + ).toEqual({ + cleanup_operation_kind: cleanupOperationKind, + status: "RESCHEDULING", + status_operation_id: PUBLIC_API_TEST_IDS.operation, + }); + } + }); + + test("releases a crashed operation fence when a canonical Driver terminal already won", async () => { + const database = await createPublicHttpContractDatabase(); + await insertNonOwnerSession(database); + await insertRunningSessionRun(database); + await insertSandboxSession(database); + await database + .prepare("UPDATE session SET status = 'RUNNING' WHERE id = ?") + .bind(PUBLIC_API_TEST_IDS.nonOwnerSession) + .run(); + const operationId = PUBLIC_API_TEST_IDS.operation; + const [updatingEvent] = buildRuntimeStateOperationEvents({ + agentId: PUBLIC_API_TEST_IDS.agent, + operation: "restartDriver", + readyAt: "1970-01-01T00:00:00.002Z", + startedAt: "1970-01-01T00:00:00.001Z", + }); + await claimRuntimeOperationTargets(database, { + event: updatingEvent, + operationId, + targets: [ + { + agentId: PUBLIC_API_TEST_IDS.agent, + creatorAccountId: PUBLIC_API_TEST_IDS.nonOwnerAccount, + lastRunId: RESCHEDULING_RUN_ID, + sandboxId: PUBLIC_API_TEST_IDS.sandbox, + sessionId: PUBLIC_API_TEST_IDS.nonOwnerSession, + sessionRuntimeEventSeqCursor: 0, + sessionStatus: "RUNNING", + sessionStatusOperationId: null, + sessionStatusSeq: 0, + sessionUpdatedAt: 1, + }, + ], + }); + const bindings = createPublicHttpTestBindings(database) as ApiBindings; + await recordCanonicalSessionRunTerminal(bindings, { + assistantMessage: null, + error: null, + expectedSessionOperationId: operationId, + runId: RESCHEDULING_RUN_ID, + sessionId: PUBLIC_API_TEST_IDS.nonOwnerSession, + source: "driver", + status: "cancelled", + }); + + expect( + await repairStaleRuntimeOperationTargets(bindings, { + limit: 10, + staleUpdatedAtLte: Number.MAX_SAFE_INTEGER, + }), + ).toBe(1); + + expect( + await database + .prepare( + `SELECT session.status AS session_status, + session.status_operation_id, + session_run.status AS run_status, + session_run.status_source + FROM session + JOIN session_run ON session_run.id = session.last_run_id + WHERE session.id = ?`, + ) + .bind(PUBLIC_API_TEST_IDS.nonOwnerSession) + .first(), + ).toEqual({ + run_status: "cancelled", + session_status: "IDLE", + status_operation_id: null, + status_source: "driver", + }); + expect( + await database + .prepare( + "SELECT COUNT(*) AS count FROM session_event WHERE event_type = 'session.lifecycle.updated'", + ) + .first(), + ).toEqual({ count: 0 }); + }); }); diff --git a/apps/api/tests/runtime-subject-network.test.ts b/apps/api/tests/runtime-subject-network.test.ts index 04b2b798..ff13e102 100644 --- a/apps/api/tests/runtime-subject-network.test.ts +++ b/apps/api/tests/runtime-subject-network.test.ts @@ -4,7 +4,7 @@ import { resolveRuntimeSubjectNetworkConstraints } from "../src/modules/runtime/ import type { ApiBindings } from "../src/platform/cloudflare/worker-types"; function createBindings(overrides: Record = {}): ApiBindings { - return overrides as unknown as ApiBindings; + return overrides; } const LIMITED_NETWORK = { diff --git a/apps/api/tests/runtime-subject-operation-authority-migration.test.ts b/apps/api/tests/runtime-subject-operation-authority-migration.test.ts new file mode 100644 index 00000000..282e29d6 --- /dev/null +++ b/apps/api/tests/runtime-subject-operation-authority-migration.test.ts @@ -0,0 +1,959 @@ +import { describe, expect, test } from "bun:test"; + +import { + installProtocolV3CutoverSql, + INSTALL_PROTOCOL_V3_POST_MIGRATION_CUTOVER_SQL, + PROTOCOL_V3_CUTOVER_OBJECTS_SQL, + PROTOCOL_V3_POST_MIGRATION_CUTOVER_OBJECT_COUNT, + REMOVE_PROTOCOL_V3_CUTOVER_SQL, +} from "../bin/protocol-v3-cutover"; +import { applyDrizzleMigration, applyDrizzleMigrationsBefore } from "./helpers/drizzle-migrations"; +import { SqliteD1Database } from "./helpers/sqlite-d1"; + +const MIGRATION_TAG = "0019_runtime-subject-operation-authority"; +const id = (suffix: string) => `01J0000000000000000000001${suffix}`; + +const ACCOUNT_ID = id("A"); +const APP_ID = id("B"); +const PET_AGENT_ID = id("C"); +const CATTLE_AGENT_ID = id("D"); +const PET_SESSION_ID = id("E"); +const CATTLE_SESSION_ID = id("F"); +const PET_SANDBOX_ID = id("G"); +const CATTLE_SANDBOX_ID = id("H"); +const PET_CLOUDFLARE_SESSION_ID = id("J"); +const CATTLE_CLOUDFLARE_SESSION_ID = id("K"); +const RUN_ID = id("M"); +const DRIVER_ID = id("N"); +const DRIVER_COMMAND_ID = id("P"); +const EFFECT_ID = id("Q"); +const SERVER_ID = id("R"); +const API_COMMAND_ID = id("S"); +const PET_BACKUP_ID = id("T"); +const CATTLE_BACKUP_ID = id("V"); +const PRUNED_BACKUP_ID = id("W"); +const OPERATION_ID = id("X"); +const DEPLOYMENT_ID = id("5"); + +function createLegacyDatabase(): SqliteD1Database { + const database = new SqliteD1Database(); + applyDrizzleMigrationsBefore(database, MIGRATION_TAG); + database.execute(` + INSERT INTO app ( + created_at, id, name, organization_id, owner_account_id, updated_at + ) VALUES (1, '${APP_ID}', 'Fixture', '${ACCOUNT_ID}', '${ACCOUNT_ID}', 1); + + INSERT INTO agent ( + config_json, created_at, id, kind, model, name, owner_account_id, + app_id, prompt, provider, runtime_id, status, updated_at, visibility + ) VALUES + ('{}', 1, '${PET_AGENT_ID}', 'pet', 'gpt-5.4', 'Pet', '${ACCOUNT_ID}', + '${APP_ID}', '', 'openai', 'codex', 'draft', 1, 'private'), + ('{}', 1, '${CATTLE_AGENT_ID}', 'cattle', 'gpt-5.4', 'Cattle', '${ACCOUNT_ID}', + '${APP_ID}', '', 'openai', 'codex', 'draft', 1, 'private'); + + INSERT INTO session ( + agent_id, created_at, creator_account_id, id, kind, model, app_id, + provider, renamed, runtime_id, status, updated_at + ) VALUES + ('${PET_AGENT_ID}', 1, '${ACCOUNT_ID}', '${PET_SESSION_ID}', 'pet', 'gpt-5.4', + '${APP_ID}', 'openai', 0, 'codex', 'IDLE', 1), + ('${CATTLE_AGENT_ID}', 1, '${ACCOUNT_ID}', '${CATTLE_SESSION_ID}', 'cattle', + 'gpt-5.4', '${APP_ID}', 'openai', 0, 'codex', 'IDLE', 1); + + INSERT INTO sandbox ( + agent_id, app_id, created_at, id, kind, owner_account_id, status, + subject_id, subject_kind, updated_at + ) VALUES + (NULL, NULL, 1, '${PET_SANDBOX_ID}', 'pet', NULL, 'cold', + '${PET_AGENT_ID}', 'agent', 1), + ('${CATTLE_AGENT_ID}', '${APP_ID}', 1, '${CATTLE_SANDBOX_ID}', 'cattle', + '${ACCOUNT_ID}', 'cold', '${CATTLE_SESSION_ID}', 'session', 1); + + INSERT INTO sandbox_session ( + cloudflare_session_id, created_at, cwd, origin_json, sandbox_id, + session_id, status, updated_at + ) VALUES + ('${PET_CLOUDFLARE_SESSION_ID}', 1, '/workspace', '{}', '${PET_SANDBOX_ID}', + '${PET_SESSION_ID}', 'closed', 1), + ('${CATTLE_CLOUDFLARE_SESSION_ID}', 1, '/workspace', '{}', + '${CATTLE_SANDBOX_ID}', '${CATTLE_SESSION_ID}', 'error', 1); + + INSERT INTO session_run ( + agent_id, completed_at, created_at, created_by_account_id, id, session_id, + status, status_event, trace_id, trigger, updated_at + ) VALUES ( + '${CATTLE_AGENT_ID}', 2, 1, '${ACCOUNT_ID}', '${RUN_ID}', '${CATTLE_SESSION_ID}', + 'completed', 'run.complete', 'trace', 'user', 2 + ); + + INSERT INTO driver_instance ( + boot_token_expires_at, boot_token_hash, created_at, expires_at, + generation, heartbeat_count, id, protocol, protocol_version, runtime, + sandbox_id, sandbox_session_id, status, updated_at + ) VALUES ( + 10, x'01', 1, 10, 7, 0, '${DRIVER_ID}', 'rpc', 2, 'openai-runtime', + '${CATTLE_SANDBOX_ID}', '${CATTLE_SESSION_ID}', 'failed', 2 + ); + + INSERT INTO driver_command ( + driver_instance_id, id, issued_at, kind, payload_json, seq, status + ) VALUES ('${DRIVER_ID}', '${DRIVER_COMMAND_ID}', 1, 'session.stop', '{}', 1, 'completed'); + + INSERT INTO external_tool_effect ( + command_id, created_at, driver_instance_id, id, idempotency_key, server_id, + session_run_id, status, tool_name, updated_at + ) VALUES ( + '${DRIVER_COMMAND_ID}', 1, '${DRIVER_ID}', '${EFFECT_ID}', 'fixture-effect', + '${SERVER_ID}', '${RUN_ID}', 'succeeded', 'write', 2 + ); + + INSERT INTO api_command ( + created_at, dedupe_key, id, kind, payload_json, status, updated_at + ) VALUES (1, 'fixture-command', '${API_COMMAND_ID}', 'scheduled_maintenance', '{}', + 'succeeded', 2); + + INSERT INTO sandbox_backup ( + created_at, dir, id, keep, sandbox_id, session_run_id, status, ttl_seconds, + updated_at + ) VALUES + (1, '/pet', '${PET_BACKUP_ID}', 1, '${PET_SANDBOX_ID}', NULL, 'ready', 60, 2), + (1, '/workspace', '${CATTLE_BACKUP_ID}', 0, '${CATTLE_SANDBOX_ID}', + '${RUN_ID}', 'ready', 60, 2), + (1, '/archive', '${PRUNED_BACKUP_ID}', 0, '${CATTLE_SANDBOX_ID}', NULL, + 'pruned', 60, 2); + + UPDATE sandbox + SET last_backup_id = '${PET_BACKUP_ID}' + WHERE id = '${PET_SANDBOX_ID}'; + + UPDATE sandbox + SET last_backup_id = '${CATTLE_BACKUP_ID}', + last_restore_backup_id = '${PRUNED_BACKUP_ID}' + WHERE id = '${CATTLE_SANDBOX_ID}'; + `); + return database; +} + +async function columnNames(database: SqliteD1Database, table: string): Promise { + const result = await database.prepare(`PRAGMA table_info(${table})`).all<{ name: string }>(); + return result.results.map(({ name }) => name); +} + +async function cutoverCatalog(database: SqliteD1Database) { + return ( + await database + .prepare( + `SELECT name, sql, tbl_name, type + FROM sqlite_master + WHERE name = '__protocol_v3_cutover' + OR name GLOB '__protocol_v3_cutover_*' + ORDER BY type, name`, + ) + .all() + ).results; +} + +describe("0019 runtime subject operation authority migration", () => { + test("backfills authoritative identity and preserves only durable backup lineage", async () => { + const database = createLegacyDatabase(); + + applyDrizzleMigration(database, MIGRATION_TAG); + + expect( + await database + .prepare( + `SELECT id, agent_id, app_id, owner_account_id, incarnation, + network_constraints_hash, operation_kind, status_operation_id + FROM sandbox ORDER BY id`, + ) + .all(), + ).toMatchObject({ + results: [ + { + agent_id: PET_AGENT_ID, + app_id: APP_ID, + id: PET_SANDBOX_ID, + incarnation: 0, + network_constraints_hash: null, + operation_kind: null, + owner_account_id: ACCOUNT_ID, + status_operation_id: null, + }, + { + agent_id: CATTLE_AGENT_ID, + app_id: APP_ID, + id: CATTLE_SANDBOX_ID, + incarnation: 0, + network_constraints_hash: null, + operation_kind: null, + owner_account_id: ACCOUNT_ID, + status_operation_id: null, + }, + ], + }); + + expect( + await database + .prepare( + `SELECT id, operation_id, sandbox_incarnation, session_run_id, staging_id, + workspace_session_id + FROM sandbox_backup ORDER BY id`, + ) + .all(), + ).toMatchObject({ + results: [ + { + id: PET_BACKUP_ID, + operation_id: null, + sandbox_incarnation: 0, + session_run_id: null, + staging_id: PET_BACKUP_ID, + workspace_session_id: null, + }, + { + id: CATTLE_BACKUP_ID, + operation_id: null, + sandbox_incarnation: 0, + session_run_id: RUN_ID, + staging_id: CATTLE_BACKUP_ID, + workspace_session_id: CATTLE_SESSION_ID, + }, + { + id: PRUNED_BACKUP_ID, + operation_id: null, + sandbox_incarnation: 0, + session_run_id: null, + staging_id: PRUNED_BACKUP_ID, + workspace_session_id: null, + }, + ], + }); + + expect( + await database + .prepare( + `SELECT name FROM sqlite_master + WHERE name IN ('__runtime_subject_authority_guard', '__runtime_subject_identity')`, + ) + .all(), + ).toMatchObject({ results: [] }); + expect(await columnNames(database, "driver_instance")).toContain("sandbox_incarnation"); + expect(await columnNames(database, "app_deployment")).toContain("active_script_name"); + expect(await columnNames(database, "sandbox_session")).toContain("cleanup_operation_id"); + expect(await columnNames(database, "sandbox_backup_staging")).toEqual( + expect.arrayContaining(["driver_generation", "driver_instance_id"]), + ); + expect(await columnNames(database, "session")).toContain( + "runtime_provisioning_sandbox_incarnation", + ); + expect( + await database + .prepare("SELECT count(*) AS count FROM environment_package_artifact_backup_staging") + .first(), + ).toEqual({ count: 0 }); + expect( + await database + .prepare(`SELECT delivery_generation FROM api_command WHERE id = '${API_COMMAND_ID}'`) + .first(), + ).toEqual({ delivery_generation: 1 }); + expect( + await database.prepare("SELECT count(*) AS count FROM __protocol_v3_cutover").first(), + ).toEqual({ count: 0 }); + expect((await cutoverCatalog(database)).length).toBe( + PROTOCOL_V3_POST_MIGRATION_CUTOVER_OBJECT_COUNT, + ); + }); + + test("migrates legacy Pages runs to Workers Static Assets without losing run state", async () => { + const database = createLegacyDatabase(); + const deploymentRunId = id("6"); + database.execute( + `INSERT INTO app_deployment_run ( + app_id, created_at, deployment_id, error_code, error_message, + external_deployment_id, external_project_id, external_version_id, + generated_wrangler_config_json, id, mosoo_config_json, plan_json, + source_branch, source_commit_sha, status, target_kind, + target_project_name, target_script_name, updated_at, url + ) VALUES ( + '${APP_ID}', 1, '${DEPLOYMENT_ID}', 'legacy-code', 'legacy-message', + 'external-deployment', 'external-project', 'external-version', + '{"name":"fixture"}', '${deploymentRunId}', '{"build":"bun"}', + '{"output":"dist"}', 'main', '0123456789abcdef', 'success', + 'cloudflare_pages', 'legacy-project', 'candidate-script', 2, + 'https://fixture.example' + )`, + ); + + applyDrizzleMigration(database, MIGRATION_TAG); + + expect(await database.prepare("SELECT * FROM app_deployment_run").first()).toEqual({ + app_id: APP_ID, + created_at: 1, + deployment_id: DEPLOYMENT_ID, + error_code: "legacy-code", + error_message: "legacy-message", + external_deployment_id: "external-deployment", + external_project_id: "external-project", + external_version_id: "external-version", + generated_wrangler_config_json: '{"name":"fixture"}', + id: deploymentRunId, + mosoo_config_json: '{"build":"bun"}', + plan_json: '{"output":"dist"}', + source_branch: "main", + source_commit_sha: "0123456789abcdef", + status: "success", + target_kind: "cloudflare_static_assets", + target_project_name: "legacy-project", + target_script_name: "candidate-script", + updated_at: 2, + url: "https://fixture.example", + }); + expect( + ( + await database + .prepare( + `SELECT name FROM sqlite_master + WHERE type = 'index' AND tbl_name = 'app_deployment_run' AND sql IS NOT NULL + ORDER BY name`, + ) + .all<{ name: string }>() + ).results.map(({ name }) => name), + ).toEqual([ + "app_deployment_run_active_app_idx", + "app_deployment_run_app_id_idx", + "app_deployment_run_deployment_id_idx", + ]); + expect((await database.prepare("PRAGMA foreign_key_check").all()).results).toEqual([]); + expect(() => + database.execute( + `UPDATE app_deployment_run SET target_kind = 'cloudflare_pages' + WHERE id = '${deploymentRunId}'`, + ), + ).toThrow(); + }); + + test("matches the canonical inert post-migration gate exactly", async () => { + const migrated = createLegacyDatabase(); + applyDrizzleMigration(migrated, MIGRATION_TAG); + + const canonical = createLegacyDatabase(); + applyDrizzleMigration(canonical, MIGRATION_TAG); + canonical.execute(REMOVE_PROTOCOL_V3_CUTOVER_SQL); + canonical.execute(INSTALL_PROTOCOL_V3_POST_MIGRATION_CUTOVER_SQL); + + expect(await cutoverCatalog(migrated)).toEqual(await cutoverCatalog(canonical)); + }); + + test("keeps the complete post-migration schema outside the exact cutover inventory", async () => { + const database = createLegacyDatabase(); + applyDrizzleMigration(database, MIGRATION_TAG); + + expect(await database.prepare(PROTOCOL_V3_CUTOVER_OBJECTS_SQL).first()).toEqual({ + exact_object_count: PROTOCOL_V3_POST_MIGRATION_CUTOVER_OBJECT_COUNT, + object_count: PROTOCOL_V3_POST_MIGRATION_CUTOVER_OBJECT_COUNT, + }); + + database.execute(` + DROP TRIGGER app_deployment_run_target_script_update_authority; + CREATE TRIGGER app_deployment_run_target_script_update_authority + BEFORE UPDATE ON app_deployment_run WHEN 0 + BEGIN + SELECT 1; + END; + `); + expect(await database.prepare(PROTOCOL_V3_CUTOVER_OBJECTS_SQL).first()).toEqual({ + exact_object_count: 0, + object_count: PROTOCOL_V3_POST_MIGRATION_CUTOVER_OBJECT_COUNT + 1, + }); + }); + + test("preserves a completed archive as static business state", async () => { + const database = createLegacyDatabase(); + database.execute( + `UPDATE session + SET archived_at = 2, cleanup_operation_kind = 'archive' + WHERE id = '${PET_SESSION_ID}'`, + ); + + applyDrizzleMigration(database, MIGRATION_TAG); + + expect( + await database + .prepare( + `SELECT archived_at, cleanup_operation_kind, status, status_operation_id + FROM session WHERE id = '${PET_SESSION_ID}'`, + ) + .first(), + ).toEqual({ + archived_at: 2, + cleanup_operation_kind: "archive", + status: "IDLE", + status_operation_id: null, + }); + }); + + test("preserves an enabled production gate and immediately rejects old writers", async () => { + const database = createLegacyDatabase(); + const releaseTreeOid = "0123456789abcdef0123456789abcdef01234567"; + database.execute(installProtocolV3CutoverSql(releaseTreeOid)); + + applyDrizzleMigration(database, MIGRATION_TAG); + + expect( + await database + .prepare("SELECT enabled, release_tree_oid FROM __protocol_v3_cutover WHERE id = 1") + .first(), + ).toEqual({ enabled: 1, release_tree_oid: releaseTreeOid }); + expect((await cutoverCatalog(database)).length).toBe( + PROTOCOL_V3_POST_MIGRATION_CUTOVER_OBJECT_COUNT, + ); + expect(() => + database.execute( + `UPDATE sandbox + SET claim_expires_at = 100, claim_owner = 'old-worker' + WHERE id = '${PET_SANDBOX_ID}'`, + ), + ).toThrow("protocol v3 cutover blocks sandbox activation"); + expect(() => + database.execute( + `INSERT INTO sandbox_backup_staging ( + created_at, dir, id, operation_id, sandbox_id, sandbox_incarnation, + ttl_seconds, updated_at + ) VALUES (1, '/workspace', '${id("6")}', '${OPERATION_ID}', '${PET_SANDBOX_ID}', + 1, 60, 1)`, + ), + ).toThrow("protocol v3 cutover blocks new sandbox backup staging"); + }); + + test("freezes identity while allowing an incarnation-fenced activation transition", () => { + const database = createLegacyDatabase(); + applyDrizzleMigration(database, MIGRATION_TAG); + + for (const assignment of [ + `id = '${id("Y")}'`, + "kind = 'cattle'", + "subject_kind = 'session'", + `subject_id = '${CATTLE_SESSION_ID}'`, + `agent_id = '${CATTLE_AGENT_ID}'`, + `app_id = '${id("Z")}'`, + `owner_account_id = '${id("0")}'`, + ]) { + expect(() => + database.execute(`UPDATE sandbox SET ${assignment} WHERE id = '${PET_SANDBOX_ID}'`), + ).toThrow("sandbox identity is immutable"); + } + + expect(() => + database.execute( + `UPDATE sandbox + SET status = 'active', network_constraints_hash = '${"0".repeat(64)}' + WHERE id = '${PET_SANDBOX_ID}'`, + ), + ).toThrow(); + + expect(() => + database.execute( + `INSERT INTO sandbox_backup_staging ( + created_at, dir, driver_generation, driver_instance_id, id, operation_id, + sandbox_id, sandbox_incarnation, ttl_seconds, updated_at + ) VALUES (1, '/workspace', 7, '${DRIVER_ID}', '${id("4")}', '${OPERATION_ID}', + '${PET_SANDBOX_ID}', 1, 60, 1)`, + ), + ).toThrow(); + expect(() => + database.execute( + `INSERT INTO sandbox_backup_staging ( + created_at, dir, id, sandbox_id, sandbox_incarnation, session_run_id, + ttl_seconds, updated_at, workspace_session_id + ) VALUES (1, '/workspace', '${id("5")}', '${CATTLE_SANDBOX_ID}', 1, + '${RUN_ID}', 60, 1, '${CATTLE_SESSION_ID}')`, + ), + ).toThrow(); + for (const [driverGeneration, driverInstanceId, suffix] of [ + ["7", "NULL", "7"], + ["NULL", `'${DRIVER_ID}'`, "8"], + ["1.5", `'${DRIVER_ID}'`, "9"], + ["9007199254740992", `'${DRIVER_ID}'`, "A"], + ] as const) { + expect(() => + database.execute( + `INSERT INTO sandbox_backup_staging ( + created_at, dir, driver_generation, driver_instance_id, id, sandbox_id, + sandbox_incarnation, session_run_id, ttl_seconds, updated_at, + workspace_session_id + ) VALUES (1, '/workspace', ${driverGeneration}, ${driverInstanceId}, '${id(suffix)}', + '${CATTLE_SANDBOX_ID}', 1, '${RUN_ID}', 60, 1, + '${CATTLE_SESSION_ID}')`, + ), + ).toThrow(); + } + database.execute( + `INSERT INTO sandbox_backup_staging ( + created_at, dir, driver_generation, driver_instance_id, id, sandbox_id, + sandbox_incarnation, session_run_id, ttl_seconds, updated_at, workspace_session_id + ) VALUES (1, '/workspace', 7, '${DRIVER_ID}', '${id("6")}', + '${CATTLE_SANDBOX_ID}', 1, '${RUN_ID}', 60, 1, '${CATTLE_SESSION_ID}')`, + ); + + database.execute( + `UPDATE sandbox + SET claim_expires_at = 100, + claim_owner = 'activation', + incarnation = incarnation + 1, + network_constraints_hash = '${"0".repeat(64)}', + operation_kind = 'activate', + status = 'restoring', + status_operation_id = '${OPERATION_ID}' + WHERE id = '${PET_SANDBOX_ID}'`, + ); + }); + + test("enforces exact incarnation and provisioning authority on new writes", () => { + const database = createLegacyDatabase(); + applyDrizzleMigration(database, MIGRATION_TAG); + + expect(() => + database.execute( + `UPDATE sandbox_session SET status = 'active' WHERE session_id = '${PET_SESSION_ID}'`, + ), + ).toThrow(); + database.execute( + `UPDATE sandbox_session + SET sandbox_incarnation = 1, status = 'active' + WHERE session_id = '${PET_SESSION_ID}'`, + ); + + expect(() => + database.execute( + `INSERT INTO sandbox_backup_staging ( + created_at, dir, id, operation_id, sandbox_id, sandbox_incarnation, + ttl_seconds, updated_at + ) VALUES (1, '/workspace', '${id("2")}', '${OPERATION_ID}', '${PET_SANDBOX_ID}', + 0, 60, 1)`, + ), + ).toThrow(); + + database.execute( + `UPDATE session + SET runtime_provisioning_heartbeat_at = 1, + runtime_provisioning_operation_id = '${OPERATION_ID}', + runtime_provisioning_sandbox_id = '${PET_SANDBOX_ID}' + WHERE id = '${PET_SESSION_ID}'`, + ); + expect(() => + database.execute( + `UPDATE session + SET runtime_provisioning_heartbeat_at = 1, + runtime_provisioning_operation_id = '${id("3")}', + runtime_provisioning_sandbox_id = '${PET_SANDBOX_ID}' + WHERE id = '${CATTLE_SESSION_ID}'`, + ), + ).toThrow(); + }); + + test("creates the environment artifact staging authority with native constraints", () => { + const database = createLegacyDatabase(); + applyDrizzleMigration(database, MIGRATION_TAG); + const pathsJson = '{"executable":[],"node":[],"python":[]}'; + const digest = "0".repeat(64); + + const insertCommand = ( + commandId: string, + commandDigest: string, + claimOwner: string, + dedupeKey: string, + ): void => { + const payloadJson = JSON.stringify({ appId: APP_ID, inputDigest: commandDigest }); + database.execute( + `INSERT INTO api_command ( + attempt_count, claim_expires_at, claim_owner, created_at, dedupe_key, + delivery_generation, id, kind, payload_json, status, updated_at + ) VALUES (1, 9007199254740991, '${claimOwner}', 1, '${dedupeKey}', 1, + '${commandId}', 'environment_package_artifact_build', '${payloadJson}', + 'running', 1)`, + ); + }; + const insertStage = ( + commandId: string, + stageDigest: string, + options: { + readonly appId?: string; + readonly attemptCount?: string; + readonly claimOwner?: string; + readonly deliveryGeneration?: string; + } = {}, + ): void => { + database.execute( + `INSERT INTO environment_package_artifact_backup_staging ( + app_id, attempt_count, claim_owner, command_id, created_at, + delivery_generation, dir, input_digest, paths_json, updated_at + ) VALUES ('${options.appId ?? APP_ID}', ${options.attemptCount ?? "1"}, + '${options.claimOwner ?? "owner"}', '${commandId}', 1, + ${options.deliveryGeneration ?? "1"}, '/artifact', '${stageDigest}', + '${pathsJson}', 1)`, + ); + }; + + expect(() => insertStage(API_COMMAND_ID, digest)).toThrow( + "environment artifact backup stage lacks command authority", + ); + database.execute( + `UPDATE api_command + SET attempt_count = 1, claim_expires_at = 1, claim_owner = 'owner', + kind = 'environment_package_artifact_build', + payload_json = '${JSON.stringify({ appId: APP_ID, inputDigest: digest })}', + status = 'running' + WHERE id = '${API_COMMAND_ID}'`, + ); + expect(() => insertStage(API_COMMAND_ID, digest)).toThrow( + "environment artifact backup stage lacks command authority", + ); + database.execute( + `UPDATE api_command SET claim_expires_at = 9007199254740991 + WHERE id = '${API_COMMAND_ID}'`, + ); + + for (const [attemptCount, claimOwner, deliveryGeneration, inputDigest] of [ + ["1", "owner", "1", "invalid"], + ["0", "owner", "1", digest], + ["1.5", "owner", "1", digest], + ["9007199254740992", "owner", "1", digest], + ["1", "", "1", digest], + ["1", "owner", "0", digest], + ["1", "owner", "1.5", digest], + ["1", "owner", "9007199254740992", digest], + ] as const) { + expect(() => + insertStage(API_COMMAND_ID, inputDigest, { + attemptCount, + claimOwner, + deliveryGeneration, + }), + ).toThrow(); + } + for (const options of [ + { attemptCount: "2" }, + { claimOwner: "other-owner" }, + { deliveryGeneration: "2" }, + { appId: id("Z") }, + ] as const) { + expect(() => insertStage(API_COMMAND_ID, digest, options)).toThrow( + "environment artifact backup stage lacks command authority", + ); + } + expect(() => insertStage(id("Y"), digest)).toThrow(); + + insertStage(API_COMMAND_ID, digest); + expect(() => + database.execute( + `UPDATE environment_package_artifact_backup_staging SET dir = '/other' + WHERE command_id = '${API_COMMAND_ID}'`, + ), + ).toThrow("environment artifact backup stage is immutable"); + database.execute( + `UPDATE environment_package_artifact_backup_staging + SET actual_backup_id = '${id("4")}', updated_at = 2 + WHERE command_id = '${API_COMMAND_ID}'`, + ); + + const duplicateIntentCommandId = id("2"); + insertCommand(duplicateIntentCommandId, digest, "owner-2", "fixture-command-2"); + expect(() => + insertStage(duplicateIntentCommandId, digest, { claimOwner: "owner-2" }), + ).toThrow(); + + const otherDigest = "1".repeat(64); + const otherCommandId = id("3"); + insertCommand(otherCommandId, otherDigest, "owner-3", "fixture-command-3"); + insertStage(otherCommandId, otherDigest, { claimOwner: "owner-3" }); + expect(() => + database.execute( + `UPDATE environment_package_artifact_backup_staging + SET actual_backup_id = '${id("4")}', updated_at = 2 + WHERE command_id = '${otherCommandId}'`, + ), + ).toThrow(); + + expect(() => + database.execute( + `UPDATE api_command SET delivery_generation = 0 WHERE id = '${API_COMMAND_ID}'`, + ), + ).toThrow(); + expect(() => + database.execute(`DELETE FROM api_command WHERE id = '${API_COMMAND_ID}'`), + ).toThrow(); + }); + + test("lets only exact pre-gate Environment command authority finish staging", async () => { + const database = createLegacyDatabase(); + applyDrizzleMigration(database, MIGRATION_TAG); + const digest = "0".repeat(64); + const payloadJson = JSON.stringify({ appId: APP_ID, inputDigest: digest }); + database.execute(` + UPDATE api_command + SET attempt_count = 1, + claim_expires_at = 9007199254740991, + claim_owner = 'pre-gate-owner', + created_at = 1, + kind = 'environment_package_artifact_build', + payload_json = '${payloadJson}', + status = 'running' + WHERE id = '${API_COMMAND_ID}'; + INSERT INTO __protocol_v3_cutover (id, enabled, release_tree_oid) + VALUES (1, 1, '${"0".repeat(40)}'); + `); + + expect(() => + database.execute( + `INSERT INTO api_command ( + attempt_count, created_at, dedupe_key, delivery_generation, id, kind, + payload_json, status, updated_at + ) VALUES ( + 0, 2, 'post-gate-environment-command', 1, '${id("Y")}', + 'environment_package_artifact_build', '${payloadJson}', 'queued', 2 + )`, + ), + ).toThrow("protocol v3 cutover blocks new nonterminal API commands"); + + database.execute( + `INSERT INTO environment_package_artifact_backup_staging ( + app_id, attempt_count, claim_owner, command_id, created_at, + delivery_generation, dir, input_digest, paths_json, updated_at + ) VALUES ( + '${APP_ID}', 1, 'pre-gate-owner', '${API_COMMAND_ID}', 1, + 1, '/artifact', '${digest}', + '{"executable":[],"node":[],"python":[]}', 1 + )`, + ); + await expect( + database + .prepare( + `SELECT command_id FROM environment_package_artifact_backup_staging + WHERE command_id = '${API_COMMAND_ID}'`, + ) + .first(), + ).resolves.toEqual({ command_id: API_COMMAND_ID }); + }); + + test("rejects public deployment traffic without durable script authority", async () => { + const database = createLegacyDatabase(); + applyDrizzleMigration(database, MIGRATION_TAG); + database.execute( + `INSERT INTO app_deployment ( + active_script_name, app_id, created_at, default_branch, deleted_at, id, + last_successful_url, latest_run_id, mosoo_subdomain, owner_account_id, + repo_name, repo_owner, repo_url, source_kind, updated_at + ) VALUES (NULL, '${APP_ID}', 1, 'main', NULL, '${DEPLOYMENT_ID}', NULL, NULL, + 'fixture-app', '${ACCOUNT_ID}', 'repo', 'owner', + 'https://example.com/repo', 'github_public', 1)`, + ); + + for (const assignment of [ + "last_successful_url = 'https://fixture.example'", + "active_script_name = 'fixture-script'", + "active_script_name = '', last_successful_url = 'https://fixture.example'", + ]) { + expect(() => + database.execute(`UPDATE app_deployment SET ${assignment} WHERE id = '${DEPLOYMENT_ID}'`), + ).toThrow(); + } + + database.execute( + `UPDATE app_deployment + SET active_script_name = NULL, deleted_at = 2, last_successful_url = NULL + WHERE id = '${DEPLOYMENT_ID}'`, + ); + + expect( + await database + .prepare( + `SELECT active_script_name, deleted_at, last_successful_url + FROM app_deployment WHERE id = '${DEPLOYMENT_ID}'`, + ) + .first(), + ).toEqual({ active_script_name: null, deleted_at: 2, last_successful_url: null }); + }); + + test("keeps an older backup incarnation when its workspace advances", async () => { + const database = createLegacyDatabase(); + applyDrizzleMigration(database, MIGRATION_TAG); + database.execute(` + UPDATE sandbox SET incarnation = 2 WHERE id = '${CATTLE_SANDBOX_ID}'; + UPDATE sandbox_session SET sandbox_incarnation = 2 + WHERE session_id = '${CATTLE_SESSION_ID}'; + INSERT INTO sandbox_backup ( + created_at, dir, id, keep, sandbox_id, sandbox_incarnation, session_run_id, + staging_id, status, ttl_seconds, updated_at, workspace_session_id + ) VALUES ( + 3, '/workspace', '${id("7")}', 0, '${CATTLE_SANDBOX_ID}', 1, '${RUN_ID}', + '${id("8")}', 'ready', 60, 3, '${CATTLE_SESSION_ID}' + ); + `); + + expect( + await database + .prepare( + `SELECT backup.sandbox_incarnation AS backup_incarnation, + workspace.sandbox_incarnation AS workspace_incarnation + FROM sandbox_backup AS backup + JOIN sandbox_session AS workspace + ON workspace.session_id = backup.workspace_session_id + WHERE backup.id = '${id("7")}'`, + ) + .first(), + ).toEqual({ backup_incarnation: 1, workspace_incarnation: 2 }); + }); + + const rollbackCases = [ + ["active sandbox", `UPDATE sandbox SET status = 'active' WHERE id = '${PET_SANDBOX_ID}'`], + [ + "partial sandbox identity", + `UPDATE sandbox SET agent_id = '${PET_AGENT_ID}' WHERE id = '${PET_SANDBOX_ID}'`, + ], + [ + "mismatched sandbox identity", + `UPDATE sandbox SET agent_id = '${CATTLE_AGENT_ID}', app_id = '${APP_ID}', + owner_account_id = '${ACCOUNT_ID}' + WHERE id = '${PET_SANDBOX_ID}'`, + ], + [ + "invalid sandbox subject", + `UPDATE sandbox SET subject_kind = 'user' WHERE id = '${PET_SANDBOX_ID}'`, + ], + ["running session", `UPDATE session SET status = 'RUNNING' WHERE id = '${PET_SESSION_ID}'`], + [ + "session status operation", + `UPDATE session SET status_operation_id = '${OPERATION_ID}' WHERE id = '${PET_SESSION_ID}'`, + ], + [ + "session delete cleanup operation", + `UPDATE session + SET archived_at = 1, + cleanup_operation_kind = 'delete', + status = 'RESCHEDULING', + status_operation_id = '${OPERATION_ID}' + WHERE id = '${PET_SESSION_ID}'`, + ], + [ + "session provisioning authority", + `UPDATE session + SET runtime_provisioning_heartbeat_at = 1, + runtime_provisioning_operation_id = '${OPERATION_ID}', + runtime_provisioning_sandbox_id = '${PET_SANDBOX_ID}' + WHERE id = '${PET_SESSION_ID}'`, + ], + [ + "sandbox claim", + `UPDATE sandbox SET claim_owner = 'legacy', claim_expires_at = 10 + WHERE id = '${PET_SANDBOX_ID}'`, + ], + [ + "active sandbox session", + `UPDATE sandbox_session SET status = 'active' WHERE session_id = '${PET_SESSION_ID}'`, + ], + [ + "cross-subject sandbox session", + `UPDATE sandbox_session SET sandbox_id = '${CATTLE_SANDBOX_ID}' + WHERE session_id = '${PET_SESSION_ID}'`, + ], + ["failed backup", `UPDATE sandbox_backup SET status = 'failed' WHERE id = '${PET_BACKUP_ID}'`], + [ + "backup error", + `UPDATE sandbox_backup SET error_message = 'failed' WHERE id = '${PET_BACKUP_ID}'`, + ], + [ + "duplicate terminal backup", + `INSERT INTO sandbox_backup ( + created_at, dir, id, keep, sandbox_id, session_run_id, status, ttl_seconds, updated_at + ) VALUES (1, '/workspace', '${id("4")}', 0, '${CATTLE_SANDBOX_ID}', '${RUN_ID}', + 'pruned', 60, 2)`, + ], + [ + "cross-subject terminal backup", + `UPDATE session_run SET session_id = '${PET_SESSION_ID}' WHERE id = '${RUN_ID}'`, + ], + [ + "cross-sandbox terminal backup", + `UPDATE sandbox SET last_backup_id = NULL WHERE id = '${CATTLE_SANDBOX_ID}'; + UPDATE sandbox_backup SET sandbox_id = '${PET_SANDBOX_ID}' + WHERE id = '${CATTLE_BACKUP_ID}'`, + ], + [ + "cross-directory terminal backup", + `UPDATE sandbox_backup SET dir = '/other' WHERE id = '${CATTLE_BACKUP_ID}'`, + ], + [ + "mismatched terminal run agent", + `UPDATE session_run SET agent_id = '${PET_AGENT_ID}' WHERE id = '${RUN_ID}'`, + ], + [ + "non-completed terminal backup", + `UPDATE session_run SET status = 'failed' WHERE id = '${RUN_ID}'`, + ], + [ + "dangling backup pointer", + `UPDATE sandbox SET last_backup_id = '${id("5")}' WHERE id = '${PET_SANDBOX_ID}'`, + ], + ["live driver", `UPDATE driver_instance SET status = 'ready' WHERE id = '${DRIVER_ID}'`], + ["unsafe driver generation", `UPDATE driver_instance SET generation = 1.5`], + ["active run", `UPDATE session_run SET status = 'running' WHERE id = '${RUN_ID}'`], + [ + "active app deployment run", + `INSERT INTO app_deployment ( + app_id, created_at, default_branch, deleted_at, id, last_successful_url, + latest_run_id, mosoo_subdomain, owner_account_id, repo_name, repo_owner, + repo_url, source_kind, updated_at + ) VALUES ('${APP_ID}', 1, 'main', NULL, '${DEPLOYMENT_ID}', NULL, + '${id("6")}', 'fixture-app', '${ACCOUNT_ID}', 'repo', 'owner', + 'https://example.com/repo', 'github_public', 1); + INSERT INTO app_deployment_run ( + app_id, created_at, deployment_id, id, source_branch, source_commit_sha, + status, updated_at + ) VALUES ('${APP_ID}', 1, '${DEPLOYMENT_ID}', '${id("6")}', 'main', + '0123456789abcdef', 'building', 1)`, + ], + ["queued driver command", `UPDATE driver_command SET driver_generation = 7, status = 'queued'`], + ["claimed external effect", `UPDATE external_tool_effect SET status = 'claimed'`], + ["queued API command", `UPDATE api_command SET status = 'queued'`], + ["running API command", `UPDATE api_command SET status = 'running'`], + [ + "legacy active app deployment traffic", + `INSERT INTO app_deployment ( + app_id, created_at, default_branch, deleted_at, id, last_successful_url, + latest_run_id, mosoo_subdomain, owner_account_id, repo_name, repo_owner, + repo_url, source_kind, updated_at + ) VALUES ('${APP_ID}', 1, 'main', NULL, '${DEPLOYMENT_ID}', + 'https://fixture.example', NULL, 'fixture-app', '${ACCOUNT_ID}', + 'repo', 'owner', 'https://example.com/repo', 'github_public', 1)`, + ], + [ + "legacy deleted app deployment traffic marker", + `INSERT INTO app_deployment ( + app_id, created_at, default_branch, deleted_at, id, last_successful_url, + latest_run_id, mosoo_subdomain, owner_account_id, repo_name, repo_owner, + repo_url, source_kind, updated_at + ) VALUES ('${APP_ID}', 1, 'main', 2, '${DEPLOYMENT_ID}', + 'https://fixture.example', NULL, 'fixture-app', '${ACCOUNT_ID}', + 'repo', 'owner', 'https://example.com/repo', 'github_public', 2)`, + ], + ] as const; + + for (const [name, mutation] of rollbackCases) { + test(`rolls back every statement for ${name}`, async () => { + const database = createLegacyDatabase(); + database.execute(mutation); + + expect(() => applyDrizzleMigration(database, MIGRATION_TAG)).toThrow(); + expect(await columnNames(database, "sandbox")).not.toContain("incarnation"); + expect( + await database + .prepare( + `SELECT count(*) AS count FROM sqlite_master + WHERE name IN ('sandbox_backup_staging', 'sandbox_identity_immutable')`, + ) + .first<{ count: number }>(), + ).toEqual({ count: 0 }); + }); + } +}); diff --git a/apps/api/tests/runtime-subject-recycle.test.ts b/apps/api/tests/runtime-subject-recycle.test.ts index ccd85298..9508808a 100644 --- a/apps/api/tests/runtime-subject-recycle.test.ts +++ b/apps/api/tests/runtime-subject-recycle.test.ts @@ -11,11 +11,16 @@ import { resumeRuntimeSubjectRecycleOperation, } from "../src/modules/runtime/infrastructure/runtime-subject-lifecycle/runtime-subject-recycle.service"; import { + claimRuntimeSubjectOperationForRepair, listInactiveRuntimeSubjects, listStaleRuntimeSubjectOperations, } from "../src/modules/runtime/infrastructure/runtime-subject-lifecycle/runtime-subject-store"; +import type { RuntimeSubjectOperationLease } from "../src/modules/runtime/infrastructure/runtime-subject-lifecycle/runtime-subject-store"; import { encodeSandboxBackupIdForStorage } from "../src/modules/runtime/infrastructure/sandbox-backup-id"; -import type { SandboxHandle } from "../src/modules/runtime/infrastructure/sandbox-handles"; +import type { + RuntimeSubjectIncarnationHandle, + SandboxHandle, +} from "../src/modules/runtime/infrastructure/sandbox-handles"; import type { ApiBindings } from "../src/platform/cloudflare/worker-types"; import { SqliteD1Database } from "./helpers/sqlite-d1"; @@ -27,10 +32,15 @@ const CLOUDFLARE_BACKUP_IDS = [ ] as const; const BACKUP_ID = encodeSandboxBackupIdForStorage(CLOUDFLARE_BACKUP_ID); const CLAIM_OWNER = "scheduled-maintenance-owner"; -const OPERATION_ID = "01J0000000000000000000000R"; +const OPERATION_ID = parsePlatformId( + "01J0000000000000000000000R", + "operation id", +); const SANDBOX_ID = PLATFORM_ID_FIXTURES.sandbox; -let currentSandbox: SandboxHandle | null = null; +type RuntimeSubjectTestHandle = RuntimeSubjectIncarnationHandle & SandboxHandle; + +let currentSandbox: RuntimeSubjectTestHandle | null = null; function createRuntimeSubjectRecycleDatabase(): SqliteD1Database { const database = new SqliteD1Database({ foreignKeys: false }); @@ -39,19 +49,29 @@ function createRuntimeSubjectRecycleDatabase(): SqliteD1Database { CREATE TABLE driver_instance ( id text PRIMARY KEY NOT NULL, sandbox_id text NOT NULL, + sandbox_incarnation integer DEFAULT 1 NOT NULL, + sandbox_session_id text, generation integer DEFAULT 0 NOT NULL, - status text NOT NULL + status text NOT NULL, + status_operation_id text ); CREATE TABLE sandbox ( + agent_id text NOT NULL DEFAULT '01J00000000000000000000009', + app_id text NOT NULL DEFAULT '01J0000000000000000000000A', claim_expires_at integer, claim_owner text, id text PRIMARY KEY NOT NULL, inactive_deadline_at integer, + incarnation integer NOT NULL, kind text NOT NULL, last_backup_id text, last_error text, last_error_code text, + last_restore_backup_id text, + network_constraints_hash text, + operation_kind text, + owner_account_id text NOT NULL DEFAULT '01J00000000000000000000008', status text NOT NULL, status_changed_at integer DEFAULT 0 NOT NULL, status_event text DEFAULT 'runtime_subject.active' NOT NULL, @@ -66,27 +86,78 @@ function createRuntimeSubjectRecycleDatabase(): SqliteD1Database { CREATE TABLE sandbox_backup ( created_at integer NOT NULL, dir text NOT NULL, - error_message text, id text PRIMARY KEY NOT NULL, keep integer NOT NULL, + operation_id text, sandbox_id text NOT NULL, + sandbox_incarnation integer DEFAULT 1 NOT NULL, session_run_id text, + staging_id text NOT NULL, status text NOT NULL, ttl_seconds integer NOT NULL, - updated_at integer NOT NULL + updated_at integer NOT NULL, + workspace_session_id text + ); + + CREATE TABLE sandbox_backup_staging ( + actual_backup_id text, + claim_owner text, + created_at integer NOT NULL, + dir text NOT NULL, + driver_generation integer, + driver_instance_id text, + id text PRIMARY KEY NOT NULL, + operation_id text, + sandbox_id text NOT NULL, + sandbox_incarnation integer NOT NULL, + session_run_id text, + ttl_seconds integer NOT NULL, + updated_at integer NOT NULL, + updates_subject_backup integer NOT NULL, + workspace_session_id text + ); + + CREATE TABLE sandbox_backup_delete_intent ( + attempted_at integer, + backup_id text PRIMARY KEY NOT NULL, + created_at integer NOT NULL, + delete_after integer NOT NULL, + deleted_at integer + ); + + CREATE TABLE environment_package_artifact_backup ( + backup_id text PRIMARY KEY NOT NULL + ); + + CREATE TABLE environment_package_artifact_backup_staging ( + actual_backup_id text + ); + + CREATE TABLE native_resume_ref ( + committed_session_run_id text, + committed_value text, + observed_session_run_id text, + session_id text PRIMARY KEY NOT NULL, + value text NOT NULL ); CREATE TABLE sandbox_session ( + cleanup_operation_id text, cwd text NOT NULL, sandbox_id text NOT NULL, + sandbox_incarnation integer DEFAULT 1 NOT NULL, session_id text PRIMARY KEY NOT NULL, status text NOT NULL, updated_at integer ); CREATE TABLE session ( + archived_at integer, + cleanup_operation_kind text, id text PRIMARY KEY NOT NULL, last_message_at integer, + runtime_provisioning_operation_id text, + runtime_provisioning_sandbox_id text, status text NOT NULL ); @@ -103,10 +174,13 @@ function createRuntimeSubjectRecycleDatabase(): SqliteD1Database { claim_owner, id, inactive_deadline_at, + incarnation, kind, last_backup_id, last_error, last_error_code, + network_constraints_hash, + operation_kind, status, status_operation_id, status_seq, @@ -115,7 +189,11 @@ function createRuntimeSubjectRecycleDatabase(): SqliteD1Database { subject_kind, updated_at ) - VALUES (9999999999999, '${CLAIM_OWNER}', '${SANDBOX_ID}', 1, 'pet', NULL, NULL, NULL, 'active', NULL, 0, 'test', '01J00000000000000000000009', 'session', 1); + VALUES ( + 9999999999999, '${CLAIM_OWNER}', '${SANDBOX_ID}', 1, 1, 'pet', NULL, NULL, NULL, + '${"0".repeat(64)}', NULL, 'active', NULL, 0, 'test', + '01J00000000000000000000009', 'agent', 1 + ); `); return database; @@ -135,7 +213,7 @@ function createBindings(database: D1Database): ApiBindings { delete: async () => {}, }, Sandbox: {}, - } as unknown as ApiBindings; + }; } function requireRuntimeOperationId(value: string | null | undefined): RuntimeOperationId { @@ -146,6 +224,56 @@ function requireRuntimeOperationId(value: string | null | undefined): RuntimeOpe return parsePlatformId(value, "runtime operation id"); } +function operationLease( + operationId: RuntimeOperationId, + status: RuntimeSubjectOperationLease["status"], +): RuntimeSubjectOperationLease { + return { + claimExpiresAt: Number.MAX_SAFE_INTEGER, + claimOwner: CLAIM_OWNER, + incarnation: 1, + kind: "hibernate", + operationId, + status, + }; +} + +async function claimStaleOperation( + database: D1Database, + operationId: RuntimeOperationId, + status: RuntimeSubjectOperationLease["status"], +): Promise { + const candidates = await listStaleRuntimeSubjectOperations(database, { + limit: 10, + staleChangedAtLte: Number.MAX_SAFE_INTEGER, + }); + expect(candidates).toHaveLength(1); + const candidate = candidates[0]; + if (!candidate) { + throw new Error("Runtime subject repair candidate was not found."); + } + expect(candidate).toMatchObject({ + claimOwner: CLAIM_OWNER, + id: SANDBOX_ID, + incarnation: 1, + kind: "pet", + operationId, + operationKind: "hibernate", + status, + }); + const now = Date.now(); + const lease = await claimRuntimeSubjectOperationForRepair(database, { + candidate, + claimExpiresAt: now + 60_000, + claimOwner: "repair-owner", + now, + }); + if (lease === null) { + throw new Error("Runtime subject repair candidate could not be claimed."); + } + return lease; +} + async function readRuntimeSubjectRecycleRow(database: D1Database): Promise<{ last_backup_id: string | null; last_error: string | null; @@ -177,22 +305,30 @@ async function readRuntimeSubjectRecycleRow(database: D1Database): Promise<{ return row; } -function createSandboxHandle(): SandboxHandle { +function createSandboxHandle(): RuntimeSubjectTestHandle { const unavailable = async () => { throw new Error("Unexpected sandbox test method call."); }; return { + activateRuntimeSubjectIncarnation: async () => {}, configureNetworkConstraints: async () => {}, createBackup: async (options) => ({ dir: options.dir, id: CLOUDFLARE_BACKUP_ID, }), + createRuntimeSubjectBackup: async (_incarnation, options) => ({ + dir: options.dir, + id: CLOUDFLARE_BACKUP_ID, + }), createSession: unavailable, deleteSession: unavailable, destroy: async () => {}, + destroyRuntimeSubjectIncarnation: async () => ({ kind: "destroyed" }), exec: unavailable, getSession: unavailable, + inspectRuntimeSubjectIncarnation: async () => ({ kind: "healthy" }), + markRuntimeSubjectIncarnationReady: async () => {}, mkdir: async () => {}, mountBucket: unavailable, readFile: unavailable, @@ -204,7 +340,7 @@ function createSandboxHandle(): SandboxHandle { watch: unavailable, writeFile: unavailable, wsConnect: unavailable, - } as SandboxHandle; + }; } describe("runtime subject recycle", () => { @@ -215,7 +351,9 @@ describe("runtime subject recycle", () => { SET claim_expires_at = NULL, claim_owner = NULL, inactive_deadline_at = NULL, - kind = 'cattle' + kind = 'cattle', + subject_id = 'session-1', + subject_kind = 'session' WHERE id = '${SANDBOX_ID}'; INSERT INTO sandbox_session (cwd, sandbox_id, session_id, status, updated_at) @@ -246,7 +384,17 @@ describe("runtime subject recycle", () => { test("uses a generated operation id instead of the maintenance claim owner", async () => { const database = createRuntimeSubjectRecycleDatabase(); - currentSandbox = createSandboxHandle(); + let observedOperationId: string | null = null; + currentSandbox = { + ...createSandboxHandle(), + createRuntimeSubjectBackup: async (_incarnation, options) => { + observedOperationId = await database + .prepare("SELECT status_operation_id FROM sandbox WHERE id = ?") + .bind(SANDBOX_ID) + .first("status_operation_id"); + return { dir: options.dir, id: CLOUDFLARE_BACKUP_ID }; + }, + }; await expect( recycleRuntimeSubject(createBindings(database), { @@ -275,8 +423,9 @@ describe("runtime subject recycle", () => { expect(subject?.status).toBe("cold"); expect(subject?.last_backup_id).toBe(BACKUP_ID); - expect(subject?.status_operation_id).not.toBe(CLAIM_OWNER); - expect(isPlatformId(subject?.status_operation_id)).toBe(true); + expect(observedOperationId).not.toBe(CLAIM_OWNER); + expect(isPlatformId(observedOperationId)).toBe(true); + expect(subject?.status_operation_id).toBeNull(); }); test("hibernates one idle pet subject across sessions after checkpointing durable state", async () => { @@ -308,12 +457,11 @@ describe("runtime subject recycle", () => { ('01J0000000000000000000000U', 1, 'TERMINATED'); `); const checkpointDirs: string[] = []; - const preparedDirs: string[] = []; const lifecycleCalls: string[] = []; let backupIndex = 0; currentSandbox = { ...createSandboxHandle(), - createBackup: async (options) => { + createRuntimeSubjectBackup: async (_incarnation, options) => { checkpointDirs.push(options.dir); const id = CLOUDFLARE_BACKUP_IDS[backupIndex]; backupIndex += 1; @@ -322,11 +470,9 @@ describe("runtime subject recycle", () => { } return { dir: options.dir, id }; }, - mkdir: async (path) => { - preparedDirs.push(path); - }, - destroy: async () => { + destroyRuntimeSubjectIncarnation: async () => { lifecycleCalls.push("destroy"); + return { kind: "destroyed" }; }, setKeepAlive: async (keepAlive) => { lifecycleCalls.push(`keepAlive:${keepAlive}`); @@ -362,8 +508,7 @@ describe("runtime subject recycle", () => { "/workspace/se/session-1", "/workspace/se/session-2", ]); - expect(preparedDirs.toSorted()).toEqual(checkpointDirs.toSorted()); - expect(lifecycleCalls).toEqual(["keepAlive:false", "destroy"]); + expect(lifecycleCalls).toEqual(["destroy"]); await expect(readRuntimeSubjectRecycleRow(database)).resolves.toMatchObject({ last_error: null, last_error_code: null, @@ -389,17 +534,17 @@ describe("runtime subject recycle", () => { const database = createRuntimeSubjectRecycleDatabase(); currentSandbox = { ...createSandboxHandle(), - destroy: async () => {}, + destroyRuntimeSubjectIncarnation: async () => ({ kind: "destroyed" }), }; await database .prepare( ` UPDATE sandbox - SET status = ?, status_operation_id = ?, status_changed_at = ?, status_source = ? + SET operation_kind = ?, status = ?, status_operation_id = ?, status_changed_at = ?, status_source = ? WHERE id = ? `, ) - .bind("destroying", OPERATION_ID, 1, "maintenance", SANDBOX_ID) + .bind("hibernate", "destroying", OPERATION_ID, 1, "maintenance", SANDBOX_ID) .run(); await database .prepare( @@ -414,10 +559,9 @@ describe("runtime subject recycle", () => { await expect( resumeRuntimeSubjectRecycleOperation(createBindings(database), { kind: "pet", - operationId: OPERATION_ID, + lease: operationLease(OPERATION_ID, "destroying"), reason: "test.repair", runtimeSubjectId: SANDBOX_ID, - status: "destroying", }), ).resolves.toBe(true); @@ -441,7 +585,7 @@ describe("runtime subject recycle", () => { expect(subject).toEqual({ status: "cold", - status_operation_id: OPERATION_ID, + status_operation_id: null, }); expect(session).toEqual({ status: "closed" }); }); @@ -461,7 +605,7 @@ describe("runtime subject recycle", () => { .run(); currentSandbox = { ...createSandboxHandle(), - createBackup: async (options) => { + createRuntimeSubjectBackup: async (_incarnation, options) => { if (!backupAvailable) { backupAvailable = true; throw new Error("backup service unavailable"); @@ -472,8 +616,9 @@ describe("runtime subject recycle", () => { id: CLOUDFLARE_BACKUP_ID, }; }, - destroy: async () => { + destroyRuntimeSubjectIncarnation: async () => { lifecycleCalls.push("destroy"); + return { kind: "destroyed" }; }, setKeepAlive: async (keepAlive) => { lifecycleCalls.push(`keepAlive:${keepAlive}`); @@ -507,27 +652,14 @@ describe("runtime subject recycle", () => { .bind("01J0000000000000000000000S") .first("status"), ).resolves.toBe("active"); - await expect( - listStaleRuntimeSubjectOperations(database, { - limit: 10, - staleChangedAtLte: Number.MAX_SAFE_INTEGER, - }), - ).resolves.toEqual([ - { - id: SANDBOX_ID, - kind: "pet", - operationId, - status: "backing_up", - }, - ]); + const repairLease = await claimStaleOperation(database, operationId, "backing_up"); await expect( resumeRuntimeSubjectRecycleOperation(createBindings(database), { kind: "pet", - operationId, + lease: repairLease, reason: "test.repair", runtimeSubjectId: SANDBOX_ID, - status: "backing_up", }), ).resolves.toBe(true); @@ -536,9 +668,9 @@ describe("runtime subject recycle", () => { last_error: null, last_error_code: null, status: "cold", - status_operation_id: operationId, + status_operation_id: null, }); - expect(lifecycleCalls).toEqual(["keepAlive:false", "destroy"]); + expect(lifecycleCalls).toEqual(["destroy"]); }); test("keeps destroy failures as stale repair candidates with the recorded backup", async () => { @@ -546,17 +678,18 @@ describe("runtime subject recycle", () => { let destroyAvailable = false; currentSandbox = { ...createSandboxHandle(), - createBackup: async (options) => { + createRuntimeSubjectBackup: async (_incarnation, options) => { return { dir: options.dir, id: CLOUDFLARE_BACKUP_ID, }; }, - destroy: async () => { + destroyRuntimeSubjectIncarnation: async () => { if (!destroyAvailable) { destroyAvailable = true; throw new Error("destroy service unavailable"); } + return { kind: "destroyed" }; }, }; @@ -580,27 +713,14 @@ describe("runtime subject recycle", () => { status: "destroying", status_operation_id: operationId, }); - await expect( - listStaleRuntimeSubjectOperations(database, { - limit: 10, - staleChangedAtLte: Number.MAX_SAFE_INTEGER, - }), - ).resolves.toEqual([ - { - id: SANDBOX_ID, - kind: "pet", - operationId, - status: "destroying", - }, - ]); + const repairLease = await claimStaleOperation(database, operationId, "destroying"); await expect( resumeRuntimeSubjectRecycleOperation(createBindings(database), { kind: "pet", - operationId, + lease: repairLease, reason: "test.repair", runtimeSubjectId: SANDBOX_ID, - status: "destroying", }), ).resolves.toBe(true); @@ -609,7 +729,7 @@ describe("runtime subject recycle", () => { last_error: null, last_error_code: null, status: "cold", - status_operation_id: operationId, + status_operation_id: null, }); }); @@ -619,11 +739,11 @@ describe("runtime subject recycle", () => { .prepare( ` UPDATE sandbox - SET status = ?, status_operation_id = ?, status_changed_at = ?, status_source = ? + SET claim_expires_at = ?, operation_kind = ?, status = ?, status_operation_id = ?, status_changed_at = ?, status_source = ? WHERE id = ? `, ) - .bind("destroying", OPERATION_ID, 10, "maintenance", SANDBOX_ID) + .bind(10, "hibernate", "destroying", OPERATION_ID, 10, "maintenance", SANDBOX_ID) .run(); await expect( @@ -639,9 +759,13 @@ describe("runtime subject recycle", () => { }), ).resolves.toEqual([ { + claimExpiresAt: 10, + claimOwner: CLAIM_OWNER, id: SANDBOX_ID, + incarnation: 1, kind: "pet", operationId: OPERATION_ID, + operationKind: "hibernate", status: "destroying", }, ]); diff --git a/apps/api/tests/runtime-subject-run-lease.test.ts b/apps/api/tests/runtime-subject-run-lease.test.ts index 4435a17b..6a0195ba 100644 --- a/apps/api/tests/runtime-subject-run-lease.test.ts +++ b/apps/api/tests/runtime-subject-run-lease.test.ts @@ -47,21 +47,34 @@ function createRuntimeSubjectLeaseDatabase(): SqliteD1Database { database.execute(` CREATE TABLE driver_instance ( id text PRIMARY KEY NOT NULL, + generation integer NOT NULL, sandbox_id text NOT NULL, + sandbox_incarnation integer NOT NULL, sandbox_session_id text NOT NULL, status text NOT NULL, + status_changed_at integer DEFAULT 0 NOT NULL, + status_event text DEFAULT 'driver.provision' NOT NULL, + status_operation_id text, + status_seq integer DEFAULT 0 NOT NULL, + status_source text DEFAULT 'system' NOT NULL, updated_at integer NOT NULL ); CREATE TABLE sandbox ( + claim_owner text, id text PRIMARY KEY NOT NULL, inactive_deadline_at integer, + incarnation integer NOT NULL, kind text NOT NULL, + operation_kind text, + status text NOT NULL, + status_operation_id text, updated_at integer NOT NULL ); CREATE TABLE sandbox_session ( sandbox_id text NOT NULL, + sandbox_incarnation integer NOT NULL, session_id text PRIMARY KEY NOT NULL, status text NOT NULL ); @@ -80,20 +93,25 @@ function createRuntimeSubjectLeaseDatabase(): SqliteD1Database { WHERE driver_instance_id IS NOT NULL AND status IN ('queued', 'booting', 'running', 'waiting_input'); - INSERT INTO sandbox (id, inactive_deadline_at, kind, updated_at) - VALUES ('${SANDBOX_ID}', 1, 'cattle', 1); + INSERT INTO sandbox ( + claim_owner, id, inactive_deadline_at, incarnation, kind, operation_kind, + status, status_operation_id, updated_at + ) + VALUES (NULL, '${SANDBOX_ID}', 1, 1, 'cattle', NULL, 'active', NULL, 1); - INSERT INTO sandbox_session (sandbox_id, session_id, status) - VALUES ('${SANDBOX_ID}', '${SESSION_ID}', 'active'); + INSERT INTO sandbox_session (sandbox_id, sandbox_incarnation, session_id, status) + VALUES ('${SANDBOX_ID}', 1, '${SESSION_ID}', 'active'); INSERT INTO driver_instance ( id, + generation, sandbox_id, + sandbox_incarnation, sandbox_session_id, status, updated_at ) - VALUES ('${DRIVER_INSTANCE_ID}', '${SANDBOX_ID}', '${SESSION_ID}', 'ready', 1); + VALUES ('${DRIVER_INSTANCE_ID}', 0, '${SANDBOX_ID}', 1, '${SESSION_ID}', 'ready', 1); INSERT INTO session_run (id, session_id, status, status_seq, updated_at) VALUES ('${SESSION_RUN_ID}', '${SESSION_ID}', 'running', 0, 1); @@ -109,8 +127,10 @@ function leaseInput( } = {}, ) { return { + driverGeneration: 0, driverInstanceId: input.driverInstanceId ?? DRIVER_INSTANCE_ID, runtimeSubjectId: SANDBOX_ID, + runtimeSubjectIncarnation: 1, sessionId: SESSION_ID, sessionRunId: input.sessionRunId ?? SESSION_RUN_ID, }; @@ -128,6 +148,7 @@ describe("runtime subject run lease store", () => { await expect( recordRuntimeRunLeaseReleased(database, { driverInstanceId: DRIVER_INSTANCE_ID, + expectedDriverGeneration: 0, expectedSessionRunId: SESSION_RUN_ID, }), ).resolves.toBe(true); @@ -155,6 +176,95 @@ describe("runtime subject run lease store", () => { expect(sandbox?.inactive_deadline_at).toBeNull(); }); + test("does not let an old Driver generation release the replacement generation's lease", async () => { + const database = createRuntimeSubjectLeaseDatabase(); + await recordRuntimeRunLeaseAcquired(database, leaseInput()); + const originalBatch = database.batch.bind(database) as D1Database["batch"]; + let rotateBeforeRelease = true; + database.batch = (async (statements: D1PreparedStatement[]) => { + if (rotateBeforeRelease) { + rotateBeforeRelease = false; + database.execute( + `UPDATE driver_instance SET generation = 1 WHERE id = '${DRIVER_INSTANCE_ID}'`, + ); + } + return originalBatch(statements); + }) as D1Database["batch"]; + + await expect( + recordRuntimeRunLeaseReleasedOutcome(database, { + driverInstanceId: DRIVER_INSTANCE_ID, + expectedDriverGeneration: 0, + expectedSessionRunId: SESSION_RUN_ID, + }), + ).resolves.toEqual({ + reason: "driver_changed", + status: "stale", + transition: "release", + }); + await expect( + database + .prepare("SELECT driver_instance_id FROM session_run WHERE id = ?") + .bind(SESSION_RUN_ID) + .first("driver_instance_id"), + ).resolves.toBe(DRIVER_INSTANCE_ID); + }); + + test("does not let an old Driver generation acquire a lease for its replacement", async () => { + const database = createRuntimeSubjectLeaseDatabase(); + const originalBatch = database.batch.bind(database) as D1Database["batch"]; + let rotateBeforeAcquire = true; + database.batch = (async (statements: D1PreparedStatement[]) => { + if (rotateBeforeAcquire) { + rotateBeforeAcquire = false; + database.execute( + `UPDATE driver_instance SET generation = 1 WHERE id = '${DRIVER_INSTANCE_ID}'`, + ); + } + return originalBatch(statements); + }) as D1Database["batch"]; + + await expect(recordRuntimeRunLeaseAcquiredOutcome(database, leaseInput())).resolves.toEqual({ + reason: "driver_changed", + status: "stale", + transition: "acquire", + }); + await expect( + database + .prepare("SELECT driver_instance_id FROM session_run WHERE id = ?") + .bind(SESSION_RUN_ID) + .first("driver_instance_id"), + ).resolves.toBeNull(); + }); + + test("rolls back the run link when clearing the inactive deadline fails", async () => { + const database = createRuntimeSubjectLeaseDatabase(); + database.execute(` + CREATE TRIGGER reject_inactive_deadline_clear + BEFORE UPDATE OF inactive_deadline_at ON sandbox + WHEN NEW.inactive_deadline_at IS NULL + BEGIN + SELECT RAISE(ABORT, 'injected deadline failure'); + END; + `); + + await expect(recordRuntimeRunLeaseAcquired(database, leaseInput())).rejects.toThrow( + "injected deadline failure", + ); + + const run = await database + .prepare("SELECT driver_instance_id FROM session_run WHERE id = ?") + .bind(SESSION_RUN_ID) + .first<{ driver_instance_id: string | null }>(); + const deadline = await database + .prepare("SELECT inactive_deadline_at FROM sandbox WHERE id = ?") + .bind(SANDBOX_ID) + .first("inactive_deadline_at"); + + expect(run?.driver_instance_id).toBeNull(); + expect(deadline).toBe(1); + }); + test("arms the pet idle deadline after the final run while its conversation stays active", async () => { const database = createRuntimeSubjectLeaseDatabase(); database.execute(`UPDATE sandbox SET kind = 'pet' WHERE id = '${SANDBOX_ID}'`); @@ -166,6 +276,7 @@ describe("runtime subject run lease store", () => { await expect( recordRuntimeRunLeaseReleased(database, { driverInstanceId: DRIVER_INSTANCE_ID, + expectedDriverGeneration: 0, expectedSessionRunId: SESSION_RUN_ID, }), ).resolves.toBe(true); @@ -199,6 +310,7 @@ describe("runtime subject run lease store", () => { await expect( recordRuntimeRunLeaseReleased(database, { driverInstanceId: DRIVER_INSTANCE_ID, + expectedDriverGeneration: 0, expectedSessionRunId: SESSION_RUN_ID, }), ).resolves.toBe(true); @@ -216,6 +328,64 @@ describe("runtime subject run lease store", () => { expect(run?.driver_instance_id).toBe(DRIVER_INSTANCE_ID); }); + test("keeps a terminal Driver non-assignable until its physical close", async () => { + const database = createRuntimeSubjectLeaseDatabase(); + await recordRuntimeRunLeaseAcquired(database, leaseInput()); + database.execute(` + UPDATE session_run SET status = 'completed', status_seq = 1 + WHERE id = '${SESSION_RUN_ID}'; + UPDATE driver_instance SET status_operation_id = '${SESSION_RUN_ID}' + WHERE id = '${DRIVER_INSTANCE_ID}'; + INSERT INTO session_run (id, session_id, status, status_seq, updated_at) + VALUES ('${OTHER_SESSION_RUN_ID}', '${SESSION_ID}', 'running', 0, 2); + `); + + const terminalRelease = await recordRuntimeRunLeaseReleasedOutcome(database, { + driverInstanceId: DRIVER_INSTANCE_ID, + expectedDriverGeneration: 0, + expectedDriverOperationId: SESSION_RUN_ID, + expectedSessionRunId: SESSION_RUN_ID, + retainDriverOperationUntilTerminal: true, + }); + expect(terminalRelease).toMatchObject({ status: "applied" }); + await expect( + database + .prepare("SELECT status, status_operation_id FROM driver_instance WHERE id = ?") + .bind(DRIVER_INSTANCE_ID) + .first(), + ).resolves.toEqual({ status: "stopping", status_operation_id: SESSION_RUN_ID }); + await expect( + recordRuntimeRunLeaseAcquiredOutcome( + database, + leaseInput({ sessionRunId: OTHER_SESSION_RUN_ID }), + ), + ).resolves.toEqual({ + reason: "driver_not_assignable", + status: "rejected", + transition: "acquire", + }); + + database.execute(` + UPDATE driver_instance SET status = 'stopped' + WHERE id = '${DRIVER_INSTANCE_ID}' + `); + await expect( + recordRuntimeRunLeaseReleasedOutcome(database, { + driverInstanceId: DRIVER_INSTANCE_ID, + expectedDriverGeneration: 0, + expectedDriverOperationId: SESSION_RUN_ID, + expectedSessionRunId: SESSION_RUN_ID, + retainDriverOperationUntilTerminal: true, + }), + ).resolves.toMatchObject({ status: "applied" }); + await expect( + database + .prepare("SELECT status, status_operation_id FROM driver_instance WHERE id = ?") + .bind(DRIVER_INSTANCE_ID) + .first(), + ).resolves.toEqual({ status: "stopped", status_operation_id: null }); + }); + test("treats acquiring the same run as idempotent", async () => { const database = createRuntimeSubjectLeaseDatabase(); @@ -260,6 +430,21 @@ describe("runtime subject run lease store", () => { expect(run?.driver_instance_id).toBeNull(); }); + test("does not acquire while terminal cleanup owns the Driver", async () => { + const database = createRuntimeSubjectLeaseDatabase(); + database.execute(` + UPDATE driver_instance + SET status_operation_id = '${SESSION_RUN_ID}' + WHERE id = '${DRIVER_INSTANCE_ID}' + `); + + await expect(recordRuntimeRunLeaseAcquiredOutcome(database, leaseInput())).resolves.toEqual({ + reason: "driver_not_assignable", + status: "rejected", + transition: "acquire", + }); + }); + test("does not steal a run linked to another driver", async () => { const database = createRuntimeSubjectLeaseDatabase(); @@ -459,12 +644,14 @@ describe("runtime subject run lease store", () => { await expect( recordRuntimeRunLeaseReleased(database, { driverInstanceId: DRIVER_INSTANCE_ID, + expectedDriverGeneration: 0, expectedSessionRunId: UNLINKED_SESSION_RUN_ID, }), ).resolves.toBe(false); await expect( recordRuntimeRunLeaseReleasedOutcome(database, { driverInstanceId: DRIVER_INSTANCE_ID, + expectedDriverGeneration: 0, expectedSessionRunId: UNLINKED_SESSION_RUN_ID, }), ).resolves.toEqual({ diff --git a/apps/api/tests/sandbox-backup-lifecycle.test.ts b/apps/api/tests/sandbox-backup-lifecycle.test.ts new file mode 100644 index 00000000..0511d3a7 --- /dev/null +++ b/apps/api/tests/sandbox-backup-lifecycle.test.ts @@ -0,0 +1,3762 @@ +import { describe, expect, test } from "bun:test"; + +import type { ApiCommandId } from "@mosoo/db"; +import { createPlatformId, parsePlatformId } from "@mosoo/id"; +import type { AppId, RuntimeOperationId, SandboxBackupId, SandboxId, SessionId } from "@mosoo/id"; + +import { + publishEnvironmentPackageArtifactBackup, + resolveEnvironmentPackageArtifactBackup, +} from "../src/modules/environments/application/environment-package-artifact-backup"; +import { + claimEnvironmentPackageArtifactBackupActual, + commitEnvironmentPackageArtifactBackup, + getEnvironmentPackageArtifactBackupManifest, + getEnvironmentPackageArtifactBackupStage, + retireExpiredEnvironmentPackageArtifactBackups, + stageEnvironmentPackageArtifactBackup, +} from "../src/modules/environments/application/environment-package-artifact-backup-store"; +import { resolveEnvironmentPackageArtifact } from "../src/modules/environments/application/environment-package-artifact.service"; +import { + createEnvironmentPackageArtifactBackupName, + environmentPackageArtifactDir, + environmentPackageArtifactMetadataKey, + ENVIRONMENT_PACKAGE_ARTIFACT_BACKUP_REFRESH_WINDOW_MS, + ENVIRONMENT_PACKAGE_ARTIFACT_BACKUP_TTL_SECONDS, +} from "../src/modules/environments/domain/environment-package-artifact"; +import type { + EnvironmentPackageArtifactKey, + EnvironmentPackageArtifactPaths, +} from "../src/modules/environments/domain/environment-package-artifact"; +import { encodeSandboxBackupIdForStorage } from "../src/modules/runtime/infrastructure/sandbox-backup-id"; +import { + createRuntimeSandboxBackupName, + deleteAuthorizedSandboxBackupObjects, + getSandboxBackupObjectKeys, +} from "../src/modules/runtime/infrastructure/sandbox-backup-platform"; +import { reconcileSandboxBackupPage } from "../src/modules/runtime/infrastructure/sandbox-backup-reconciliation.service"; +import { + authorizeSandboxBackupDeletion, + claimSandboxBackupStageActual, + finalizeSandboxBackupStage, + getSandboxBackupStage, + listPendingSandboxBackupDeletions, + revokeSandboxBackupStage, + revokeSandboxBackupsForSessionDelete, + stageSandboxBackupWrites, +} from "../src/modules/runtime/infrastructure/sandbox-backup-store"; +import type { ApiBindings } from "../src/platform/cloudflare/worker-types"; +import { applyDrizzleMigrationsThrough } from "./helpers/drizzle-migrations"; +import { createApiCommandQueueStub } from "./helpers/public-api-http-test-fixture"; +import { SqliteD1Database } from "./helpers/sqlite-d1"; + +const SANDBOX_ID = parsePlatformId("01J0000000000000000000000D"); +const SESSION_ID = parsePlatformId("01J0000000000000000000000S"); +const OPERATION_ID = parsePlatformId("01J0000000000000000000000A"); +const CLEANUP_OPERATION_ID = parsePlatformId("01J0000000000000000000000B"); +const DIR = "/workspace/current"; +const NOW = Date.now(); +const OLD_UPLOAD = new Date(NOW - 25 * 60 * 60_000); +const ENVIRONMENT_COMMAND_ID = parsePlatformId("01J0000000000000000000000J"); +const ENVIRONMENT_APP_ID = parsePlatformId("01J0000000000000000000000K"); +const ENVIRONMENT_KEY: EnvironmentPackageArtifactKey = { + appId: ENVIRONMENT_APP_ID, + inputDigest: "a".repeat(64), +}; +const ENVIRONMENT_DIR = environmentPackageArtifactDir(ENVIRONMENT_KEY); +const ENVIRONMENT_PATHS: EnvironmentPackageArtifactPaths = { + executable: [`${ENVIRONMENT_DIR}/python/bin`], + node: [`${ENVIRONMENT_DIR}/npm/node_modules`], + python: [`${ENVIRONMENT_DIR}/python/site-packages`], +}; + +function createDatabase(): SqliteD1Database { + const database = new SqliteD1Database(); + applyDrizzleMigrationsThrough(database, "0020_sandbox-backup-object-authority"); + database.execute(` + INSERT INTO sandbox ( + agent_id, app_id, claim_expires_at, claim_owner, created_at, id, incarnation, + kind, network_constraints_hash, operation_kind, owner_account_id, status, status_operation_id, + subject_id, subject_kind, updated_at + ) VALUES ( + '01J0000000000000000000000E', '01J0000000000000000000000F', ${NOW + 60_000}, + 'owner', ${NOW}, '${SANDBOX_ID}', 1, 'pet', '${"0".repeat(64)}', 'hibernate', + '01J0000000000000000000000G', 'backing_up', '${OPERATION_ID}', + '01J0000000000000000000000E', 'agent', ${NOW} + ); + `); + return database; +} + +async function setEnvironmentArtifactCommand( + database: D1Database, + input: { + readonly attemptCount: number; + readonly claimOwner: string; + readonly commandId?: ApiCommandId; + readonly dedupeKey?: string; + readonly deliveryGeneration: number; + readonly key?: EnvironmentPackageArtifactKey; + }, +): Promise { + const commandId = input.commandId ?? ENVIRONMENT_COMMAND_ID; + const key = input.key ?? ENVIRONMENT_KEY; + await database + .prepare( + `INSERT INTO api_command ( + attempt_count, claim_expires_at, claim_owner, created_at, dedupe_key, + delivery_generation, id, kind, payload_json, status, updated_at + ) VALUES (?, ?, ?, ?, ?, ?, ?, 'environment_package_artifact_build', ?, 'running', ?) + ON CONFLICT(id) DO UPDATE SET + attempt_count = excluded.attempt_count, + claim_expires_at = excluded.claim_expires_at, + claim_owner = excluded.claim_owner, + delivery_generation = excluded.delivery_generation, + status = 'running', + updated_at = excluded.updated_at`, + ) + .bind( + input.attemptCount, + Date.now() + 60_000, + input.claimOwner, + NOW, + input.dedupeKey ?? `environment-artifact:${key.appId}:${key.inputDigest}`, + input.deliveryGeneration, + commandId, + JSON.stringify(key), + NOW, + ) + .run(); +} + +async function createEnvironmentArtifactStage( + database: D1Database, + input: { + readonly attemptCount?: number; + readonly claimOwner?: string; + readonly deliveryGeneration?: number; + } = {}, +) { + const authority = { + attemptCount: input.attemptCount ?? 1, + claimOwner: input.claimOwner ?? "environment-owner", + commandId: ENVIRONMENT_COMMAND_ID, + deliveryGeneration: input.deliveryGeneration ?? 1, + }; + await setEnvironmentArtifactCommand(database, authority); + const stage = await stageEnvironmentPackageArtifactBackup(database, { + ...authority, + dir: ENVIRONMENT_DIR, + key: ENVIRONMENT_KEY, + paths: ENVIRONMENT_PATHS, + }); + return { authority, stage }; +} + +async function succeedEnvironmentArtifactCommand( + database: D1Database, + commandId: ApiCommandId = ENVIRONMENT_COMMAND_ID, +): Promise { + await database + .prepare( + `UPDATE api_command + SET claim_expires_at = NULL, claim_owner = NULL, + completed_at = CAST(unixepoch('subsec') * 1000 AS INTEGER), status = 'succeeded' + WHERE id = ?`, + ) + .bind(commandId) + .run(); +} + +async function expireEnvironmentArtifactManifest( + database: SqliteD1Database, + expiresAt = Date.now() - 1_000, + key: EnvironmentPackageArtifactKey = ENVIRONMENT_KEY, +): Promise { + const trigger = await database + .prepare( + `SELECT sql FROM sqlite_schema + WHERE type = 'trigger' AND name = 'environment_package_artifact_backup_rotation_authority'`, + ) + .first<{ sql: string }>(); + if (trigger === null) { + throw new Error("Rotation authority trigger is missing."); + } + database.execute("DROP TRIGGER environment_package_artifact_backup_rotation_authority"); + await database + .prepare( + `UPDATE environment_package_artifact_backup + SET committed_at = ?, expires_at = ? + WHERE app_id = ? AND input_digest = ?`, + ) + .bind(expiresAt - 25 * 60 * 60_000, expiresAt, key.appId, key.inputDigest) + .run(); + database.execute(trigger.sql); +} + +async function insertRawEnvironmentArtifactManifest( + database: D1Database, + backupId: SandboxBackupId, +): Promise { + await database + .prepare( + `INSERT INTO environment_package_artifact_backup ( + app_id, attempt_count, backup_id, command_id, committed_at, + delivery_generation, expires_at, input_digest, manifest_generation, paths_json + ) VALUES ( + ?, 1, ?, ?, CAST(unixepoch('subsec') * 1000 AS INTEGER), 1, + CAST(unixepoch('subsec') * 1000 AS INTEGER) + 315359999000, ?, 1, ? + )`, + ) + .bind( + ENVIRONMENT_APP_ID, + backupId, + ENVIRONMENT_COMMAND_ID, + ENVIRONMENT_KEY.inputDigest, + JSON.stringify(ENVIRONMENT_PATHS), + ) + .run(); +} + +interface StoredObject { + readonly body: string; + readonly uploaded: Date; +} + +class MemoryR2Bucket { + readonly gets: string[] = []; + readonly objects = new Map(); + readonly #cursorKeys = new Map(); + #cursorSequence = 0; + failAfterDeletingKey: string | null = null; + + async delete(keys: string | string[]): Promise { + for (const key of typeof keys === "string" ? [keys] : keys) { + this.objects.delete(key); + if (this.failAfterDeletingKey === key) { + this.failAfterDeletingKey = null; + throw new Error("Simulated partial R2 deletion failure."); + } + } + } + + async get(key: string): Promise { + this.gets.push(key); + const object = this.objects.get(key); + return object === undefined + ? null + : ({ + text: async () => object.body, + uploaded: object.uploaded, + } as R2ObjectBody); + } + + async head(key: string): Promise { + const object = this.objects.get(key); + return object === undefined ? null : ({ key, uploaded: object.uploaded } as R2Object); + } + + async list(options: R2ListOptions): Promise { + const keys = [...this.objects.keys()] + .filter((key) => key.startsWith(options.prefix ?? "")) + .toSorted(); + const cursorKey = + options.cursor === undefined ? undefined : this.#cursorKeys.get(options.cursor); + if (options.cursor !== undefined && cursorKey === undefined) { + throw new Error("Unknown R2 test cursor."); + } + const start = cursorKey === undefined ? 0 : keys.findIndex((key) => key > cursorKey); + const normalizedStart = start === -1 ? keys.length : start; + const end = Math.min(normalizedStart + (options.limit ?? 1_000), keys.length); + const pageKeys = keys.slice(normalizedStart, end); + const cursor = `opaque-${++this.#cursorSequence}`; + const lastKey = pageKeys.at(-1); + if (lastKey !== undefined) { + this.#cursorKeys.set(cursor, lastKey); + } + return { + cursor, + delimitedPrefixes: [], + objects: pageKeys.map((key) => ({ + key, + uploaded: this.objects.get(key)!.uploaded, + })) as R2Object[], + truncated: end < keys.length, + }; + } + + async put( + key: string, + body = "", + options: Date | R2PutOptions = OLD_UPLOAD, + ): Promise { + const uploaded = options instanceof Date ? options : new Date(); + this.objects.set(key, { body, uploaded }); + return { key, uploaded } as R2Object; + } + + putBackup(input: { + readonly createdAt?: Date; + readonly dir?: string; + readonly metadataId?: string; + readonly name?: string | null; + readonly platformId: string; + readonly stagingId?: SandboxBackupId; + readonly uploaded?: Date; + }): SandboxBackupId { + const backupId = encodeSandboxBackupIdForStorage(input.platformId); + const [dataKey, metadataKey] = getSandboxBackupObjectKeys(backupId); + const createdAt = input.createdAt ?? new Date(); + void this.put(dataKey, "archive", input.uploaded ?? OLD_UPLOAD); + const name = + input.name !== undefined + ? input.name + : input.stagingId === undefined + ? (() => { + throw new Error("A runtime backup stage ID is required."); + })() + : createRuntimeSandboxBackupName(input.stagingId); + void this.put( + metadataKey, + JSON.stringify({ + dir: input.dir ?? DIR, + id: input.metadataId ?? input.platformId, + name, + createdAt: createdAt.toISOString(), + ttl: ENVIRONMENT_PACKAGE_ARTIFACT_BACKUP_TTL_SECONDS, + }), + input.uploaded ?? OLD_UPLOAD, + ); + return backupId; + } + + backupExpiresAt(backupId: SandboxBackupId): number { + const [, metadataKey] = getSandboxBackupObjectKeys(backupId); + const metadata = JSON.parse(this.objects.get(metadataKey)!.body) as { createdAt: string }; + return Date.parse(metadata.createdAt) + ENVIRONMENT_PACKAGE_ARTIFACT_BACKUP_TTL_SECONDS * 1_000; + } +} + +function createBindings(database: D1Database, bucket: MemoryR2Bucket): ApiBindings { + return { + DB: database, + SANDBOX_STATE_BUCKET: bucket, + } as ApiBindings; +} + +function loseRunAcknowledgementOnce( + database: D1Database, + queryNeedle: string, +): { readonly database: D1Database; readonly wasLost: () => boolean } { + let lost = false; + const wrap = (statement: D1PreparedStatement): D1PreparedStatement => + new Proxy(statement, { + get(target, property, receiver) { + if (property === "bind") { + return (...values: unknown[]) => wrap(Reflect.apply(target.bind, target, values)); + } + if (property === "run") { + return async (...args: unknown[]) => { + const result = await Reflect.apply(target.run, target, args); + if (!lost) { + lost = true; + throw new Error("Simulated D1 write acknowledgement loss."); + } + return result; + }; + } + return Reflect.get(target, property, receiver); + }, + }); + return { + database: new Proxy(database, { + get(target, property) { + if (property === "prepare") { + return (query: string) => { + const statement = target.prepare(query); + return query.includes(queryNeedle) ? wrap(statement) : statement; + }; + } + const value = Reflect.get(target, property, target) as unknown; + return typeof value === "function" ? value.bind(target) : value; + }, + }), + wasLost: () => lost, + }; +} + +async function createOperationStage( + database: D1Database, + input: { readonly updateSubjectBackup?: boolean; readonly workspace?: boolean } = {}, +) { + if (input.workspace === true) { + await database + .prepare( + `INSERT INTO session ( + agent_id, app_id, created_at, creator_account_id, id, kind, last_message_at, + model, provider, renamed, runtime_id, status, updated_at + ) VALUES (?, ?, ?, ?, ?, 'pet', 1, 'gpt-5.4', 'openai', 0, 'openai-runtime', 'IDLE', ?)`, + ) + .bind( + "01J0000000000000000000000E", + "01J0000000000000000000000F", + NOW, + "01J0000000000000000000000G", + SESSION_ID, + NOW, + ) + .run(); + await database + .prepare( + `INSERT INTO sandbox_session ( + cloudflare_session_id, created_at, cwd, origin_json, sandbox_id, + sandbox_incarnation, session_id, status, updated_at + ) VALUES (?, ?, ?, '{}', ?, 1, ?, 'active', ?)`, + ) + .bind("01J0000000000000000000000H", NOW, DIR, SANDBOX_ID, SESSION_ID, NOW) + .run(); + } + const [write] = await stageSandboxBackupWrites(database, { + admission: { + kind: "operation", + lease: { + claimExpiresAt: NOW + 60_000, + claimOwner: "owner", + incarnation: 1, + kind: "hibernate", + operationId: OPERATION_ID, + status: "backing_up", + }, + }, + sandboxId: SANDBOX_ID, + targets: [ + { + dir: DIR, + updateSandboxLastBackup: input.updateSubjectBackup ?? false, + workspaceSessionId: input.workspace === true ? SESSION_ID : null, + }, + ], + ttlSeconds: 100, + }); + if (write?.kind !== "staged") { + throw new Error("Test stage was not created."); + } + return write.stage; +} + +async function readyRows(database: D1Database): Promise> { + return ( + await database + .prepare("SELECT id, staging_id FROM sandbox_backup WHERE status = 'ready' ORDER BY id") + .all<{ id: string; staging_id: string }>() + ).results; +} + +describe("sandbox backup lifecycle", () => { + test("scanner adopts a complete object after the create ACK is lost", async () => { + const database = createDatabase(); + const bucket = new MemoryR2Bucket(); + const stage = await createOperationStage(database); + const actualId = bucket.putBackup({ + platformId: "550e8400-e29b-41d4-a716-446655440001", + stagingId: stage.id, + }); + + await reconcileSandboxBackupPage(createBindings(database, bucket), { + cursor: null, + }); + + expect(await readyRows(database)).toEqual([{ id: actualId, staging_id: stage.id }]); + expect(await getSandboxBackupStage(database, stage.id)).toBeNull(); + }); + + test("two candidates converge on one ready row and the scanner deletes the loser", async () => { + const database = createDatabase(); + const bucket = new MemoryR2Bucket(); + const stage = await createOperationStage(database); + const candidates = [ + bucket.putBackup({ + platformId: "550e8400-e29b-41d4-a716-446655440002", + stagingId: stage.id, + }), + bucket.putBackup({ + platformId: "550e8400-e29b-41d4-a716-446655440003", + stagingId: stage.id, + }), + ]; + + await Promise.all( + candidates.map((actualBackupId) => + claimSandboxBackupStageActual(database, { + actualBackupId, + dir: DIR, + sandboxIncarnation: 1, + stagingId: stage.id, + }), + ), + ); + const winner = (await getSandboxBackupStage(database, stage.id))?.actualBackupId; + if (winner === null || winner === undefined) { + throw new Error("Concurrent claims produced no winner."); + } + await finalizeSandboxBackupStage(database, { actualBackupId: winner, stagingId: stage.id }); + await reconcileSandboxBackupPage(createBindings(database, bucket), { + cursor: null, + }); + + expect(await readyRows(database)).toEqual([{ id: winner, staging_id: stage.id }]); + const loser = candidates.find((candidate) => candidate !== winner)!; + expect(getSandboxBackupObjectKeys(winner).every((key) => bucket.objects.has(key))).toBe(true); + expect(getSandboxBackupObjectKeys(loser).every((key) => !bucket.objects.has(key))).toBe(true); + }); + + test("claim keeps its winner when another worker finalizes before the caller resumes", async () => { + const database = createDatabase(); + const stage = await createOperationStage(database); + const actualId = encodeSandboxBackupIdForStorage("550e8400-e29b-41d4-a716-446655440009"); + let intercepted = false; + const interceptClaim = (statement: D1PreparedStatement): D1PreparedStatement => + new Proxy(statement, { + get(target, property, receiver) { + if (property === "bind") { + return (...values: unknown[]) => + interceptClaim(Reflect.apply(target.bind, target, values)); + } + if (property === "first") { + return async (...args: unknown[]) => { + const result = await Reflect.apply(target.first, target, args); + intercepted = true; + await finalizeSandboxBackupStage(database, { + actualBackupId: actualId, + stagingId: stage.id, + }); + return result; + }; + } + return Reflect.get(target, property, receiver); + }, + }); + const racingDatabase = new Proxy(database, { + get(target, property) { + if (property === "prepare") { + return (query: string) => { + const statement = target.prepare(query); + return query.includes("UPDATE sandbox_backup_staging AS stage") && + query.includes("RETURNING actual_backup_id") + ? interceptClaim(statement) + : statement; + }; + } + const value = Reflect.get(target, property, target) as unknown; + return typeof value === "function" ? value.bind(target) : value; + }, + }); + + await expect( + claimSandboxBackupStageActual(racingDatabase, { + actualBackupId: actualId, + dir: DIR, + sandboxIncarnation: 1, + stagingId: stage.id, + }), + ).resolves.toEqual({ actualBackupId: actualId }); + expect(intercepted).toBe(true); + expect(await readyRows(database)).toEqual([{ id: actualId, staging_id: stage.id }]); + expect(await getSandboxBackupStage(database, stage.id)).toBeNull(); + }); + + test("a stale collector cannot tombstone an object after finalization wins", async () => { + const database = createDatabase(); + const bucket = new MemoryR2Bucket(); + const stage = await createOperationStage(database); + const actualId = bucket.putBackup({ + platformId: "550e8400-e29b-41d4-a716-44665544000a", + stagingId: stage.id, + }); + expect( + await claimSandboxBackupStageActual(database, { + actualBackupId: actualId, + dir: DIR, + sandboxIncarnation: 1, + stagingId: stage.id, + }), + ).toEqual({ actualBackupId: actualId }); + + // The collector observed no finalized row, then the authoritative finalizer won D1. + expect(await readyRows(database)).toEqual([]); + await finalizeSandboxBackupStage(database, { + actualBackupId: actualId, + stagingId: stage.id, + }); + expect( + await authorizeSandboxBackupDeletion(database, { + authority: { kind: "runtime_invalid", stagingId: stage.id }, + backupId: actualId, + }), + ).toBe(false); + + expect(await readyRows(database)).toEqual([{ id: actualId, staging_id: stage.id }]); + expect(getSandboxBackupObjectKeys(actualId).every((key) => bucket.objects.has(key))).toBe(true); + }); + + test("an old lease owner cannot borrow a successor lease to finalize its backup", async () => { + const database = createDatabase(); + const bucket = new MemoryR2Bucket(); + const stageA = await createOperationStage(database); + const delayedA = bucket.putBackup({ + platformId: "550e8400-e29b-41d4-a716-446655440012", + stagingId: stageA.id, + }); + await database + .prepare( + `UPDATE sandbox + SET claim_owner = 'successor-owner', + claim_expires_at = CAST(unixepoch('subsec') * 1000 AS INTEGER) + 60000 + WHERE id = ?`, + ) + .bind(SANDBOX_ID) + .run(); + + await expect( + claimSandboxBackupStageActual(database, { + actualBackupId: delayedA, + dir: DIR, + sandboxIncarnation: 1, + stagingId: stageA.id, + }), + ).resolves.toBeNull(); + const [writeB] = await stageSandboxBackupWrites(database, { + admission: { + kind: "operation", + lease: { + claimExpiresAt: NOW + 60_000, + claimOwner: "successor-owner", + incarnation: 1, + kind: "hibernate", + operationId: OPERATION_ID, + status: "backing_up", + }, + }, + sandboxId: SANDBOX_ID, + targets: [{ dir: DIR, updateSandboxLastBackup: false, workspaceSessionId: null }], + ttlSeconds: 100, + }); + expect(writeB?.kind).toBe("staged"); + if (writeB?.kind !== "staged") { + throw new Error("Successor lease did not create a replacement backup stage."); + } + expect(writeB.stage).toMatchObject({ claimOwner: "successor-owner" }); + expect(writeB.stage.id).not.toBe(stageA.id); + + await reconcileSandboxBackupPage(createBindings(database, bucket), { cursor: null }); + expect(getSandboxBackupObjectKeys(delayedA).every((key) => !bucket.objects.has(key))).toBe( + true, + ); + expect(await getSandboxBackupStage(database, writeB.stage.id)).not.toBeNull(); + expect(await readyRows(database)).toEqual([]); + }); + + test("a handoff race never returns a third owner's backup stage", async () => { + const database = createDatabase(); + const stageA = await createOperationStage(database); + await database + .prepare( + `UPDATE sandbox + SET claim_owner = 'owner-b', + claim_expires_at = CAST(unixepoch('subsec') * 1000 AS INTEGER) + 60000 + WHERE id = ?`, + ) + .bind(SANDBOX_ID) + .run(); + let stageC: Awaited> = null; + let raced = false; + const interceptDelete = (statement: D1PreparedStatement): D1PreparedStatement => + new Proxy(statement, { + get(target, property, receiver) { + if (property === "bind") { + return (...values: unknown[]) => + interceptDelete(Reflect.apply(target.bind, target, values)); + } + if (property === "first") { + return async (...args: unknown[]) => { + if (!raced) { + raced = true; + await database + .prepare( + `UPDATE sandbox + SET claim_owner = 'owner-c', + claim_expires_at = CAST(unixepoch('subsec') * 1000 AS INTEGER) + 60000 + WHERE id = ?`, + ) + .bind(SANDBOX_ID) + .run(); + await revokeSandboxBackupStage(database, { + onlyIfStale: true, + stagingId: stageA.id, + }); + const [writeC] = await stageSandboxBackupWrites(database, { + admission: { + kind: "operation", + lease: { + claimExpiresAt: NOW + 60_000, + claimOwner: "owner-c", + incarnation: 1, + kind: "hibernate", + operationId: OPERATION_ID, + status: "backing_up", + }, + }, + sandboxId: SANDBOX_ID, + targets: [{ dir: DIR, updateSandboxLastBackup: false, workspaceSessionId: null }], + ttlSeconds: 100, + }); + stageC = writeC?.kind === "staged" ? writeC.stage : null; + } + return Reflect.apply(target.first, target, args); + }; + } + return Reflect.get(target, property, receiver); + }, + }); + const racingDatabase = new Proxy(database, { + get(target, property) { + if (property === "prepare") { + return (query: string) => { + const statement = target.prepare(query); + return query.includes("DELETE FROM sandbox_backup_staging AS stage") && + query.includes("AND NOT") + ? interceptDelete(statement) + : statement; + }; + } + const value = Reflect.get(target, property, target) as unknown; + return typeof value === "function" ? value.bind(target) : value; + }, + }); + + await expect( + stageSandboxBackupWrites(racingDatabase, { + admission: { + kind: "operation", + lease: { + claimExpiresAt: NOW + 60_000, + claimOwner: "owner-b", + incarnation: 1, + kind: "hibernate", + operationId: OPERATION_ID, + status: "backing_up", + }, + }, + sandboxId: SANDBOX_ID, + targets: [{ dir: DIR, updateSandboxLastBackup: false, workspaceSessionId: null }], + ttlSeconds: 100, + }), + ).rejects.toThrow("another admission authority"); + expect(raced).toBe(true); + expect(stageC).toMatchObject({ claimOwner: "owner-c" }); + }); + + test("an expired D1 lease cannot claim a backup stage", async () => { + const database = createDatabase(); + const stage = await createOperationStage(database); + await database + .prepare( + `UPDATE sandbox + SET claim_expires_at = CAST(unixepoch('subsec') * 1000 AS INTEGER) - 1 + WHERE id = ?`, + ) + .bind(SANDBOX_ID) + .run(); + + await expect( + claimSandboxBackupStageActual(database, { + actualBackupId: encodeSandboxBackupIdForStorage("550e8400-e29b-41d4-a716-446655440013"), + dir: DIR, + sandboxIncarnation: 1, + stagingId: stage.id, + }), + ).resolves.toBeNull(); + }); + + test("a partial finalization cannot borrow a successor owner to update the subject", async () => { + const database = createDatabase(); + const stage = await createOperationStage(database, { updateSubjectBackup: true }); + const actualId = encodeSandboxBackupIdForStorage("550e8400-e29b-41d4-a716-446655440017"); + await claimSandboxBackupStageActual(database, { + actualBackupId: actualId, + dir: DIR, + sandboxIncarnation: 1, + stagingId: stage.id, + }); + await expect( + database + .prepare( + `INSERT OR REPLACE INTO sandbox_backup ( + created_at, dir, id, keep, operation_id, sandbox_id, sandbox_incarnation, + staging_id, status, ttl_seconds, updated_at + ) SELECT created_at, dir, actual_backup_id, 0, operation_id, sandbox_id, + sandbox_incarnation, id, 'pruned', ttl_seconds, + CAST(unixepoch('subsec') * 1000 AS INTEGER) + FROM sandbox_backup_staging WHERE id = ?`, + ) + .bind(stage.id) + .run(), + ).rejects.toThrow("already referenced"); + await database + .prepare( + `INSERT INTO sandbox_backup ( + created_at, dir, id, keep, operation_id, sandbox_id, sandbox_incarnation, + staging_id, status, ttl_seconds, updated_at + ) SELECT created_at, dir, actual_backup_id, 0, operation_id, sandbox_id, + sandbox_incarnation, id, 'ready', ttl_seconds, + CAST(unixepoch('subsec') * 1000 AS INTEGER) + FROM sandbox_backup_staging WHERE id = ?`, + ) + .bind(stage.id) + .run(); + await expect( + database + .prepare("UPDATE sandbox_backup_staging SET actual_backup_id = NULL WHERE id = ?") + .bind(stage.id) + .run(), + ).rejects.toThrow("already owned"); + await expect( + database + .prepare("UPDATE sandbox_backup SET status = 'pruned' WHERE id = ?") + .bind(actualId) + .run(), + ).rejects.toThrow("already referenced"); + await database + .prepare( + `UPDATE sandbox + SET claim_owner = 'successor-owner', + claim_expires_at = CAST(unixepoch('subsec') * 1000 AS INTEGER) + 60000 + WHERE id = ?`, + ) + .bind(SANDBOX_ID) + .run(); + + await expect( + finalizeSandboxBackupStage(database, { + actualBackupId: actualId, + stagingId: stage.id, + }), + ).resolves.toMatchObject({ candidateAccepted: true, complete: false }); + expect( + await database + .prepare("SELECT last_backup_id FROM sandbox WHERE id = ?") + .bind(SANDBOX_ID) + .first(), + ).toEqual({ last_backup_id: null }); + expect(await getSandboxBackupStage(database, stage.id)).not.toBeNull(); + }); + + test("the tombstone guard independently rejects a ready row ID change", async () => { + const database = createDatabase(); + const stage = await createOperationStage(database); + const readyId = encodeSandboxBackupIdForStorage("550e8400-e29b-41d4-a716-44665544000d"); + const tombstonedId = encodeSandboxBackupIdForStorage("550e8400-e29b-41d4-a716-44665544000e"); + expect( + await authorizeSandboxBackupDeletion(database, { + authority: { kind: "unattributed" }, + backupId: tombstonedId, + }), + ).toBe(true); + await claimSandboxBackupStageActual(database, { + actualBackupId: readyId, + dir: DIR, + sandboxIncarnation: 1, + stagingId: stage.id, + }); + await finalizeSandboxBackupStage(database, { + actualBackupId: readyId, + stagingId: stage.id, + }); + database.execute("DROP TRIGGER sandbox_backup_identity_immutable"); + + await expect( + database + .prepare("UPDATE sandbox_backup SET id = ? WHERE id = ?") + .bind(tombstonedId, readyId) + .run(), + ).rejects.toThrow("sandbox backup object is tombstoned or already referenced"); + expect(await readyRows(database)).toEqual([{ id: readyId, staging_id: stage.id }]); + }); + + test("runtime final identity cannot use OR REPLACE to delete another owner", async () => { + const database = createDatabase(); + const actualA = encodeSandboxBackupIdForStorage("550e8400-e29b-41d4-a716-446655440020"); + const actualB = encodeSandboxBackupIdForStorage("550e8400-e29b-41d4-a716-446655440021"); + const actualC = encodeSandboxBackupIdForStorage("550e8400-e29b-41d4-a716-446655440022"); + const actualD = encodeSandboxBackupIdForStorage("550e8400-e29b-41d4-a716-446655440027"); + const stageA = encodeSandboxBackupIdForStorage("550e8400-e29b-41d4-a716-446655440023"); + const stageB = encodeSandboxBackupIdForStorage("550e8400-e29b-41d4-a716-446655440024"); + const stageC = encodeSandboxBackupIdForStorage("550e8400-e29b-41d4-a716-446655440028"); + const insert = (actualId: SandboxBackupId, stagingId: SandboxBackupId, dir: string) => + database + .prepare( + `INSERT INTO sandbox_backup ( + created_at, dir, id, keep, operation_id, sandbox_id, sandbox_incarnation, + session_run_id, staging_id, status, ttl_seconds, updated_at, workspace_session_id + ) VALUES (?, ?, ?, 0, ?, ?, 1, NULL, ?, 'ready', 100, ?, NULL)`, + ) + .bind(NOW, dir, actualId, OPERATION_ID, SANDBOX_ID, stagingId, NOW) + .run(); + await insert(actualA, stageA, "/workspace/a"); + await insert(actualB, stageB, "/workspace/b"); + await database.prepare("PRAGMA recursive_triggers = OFF").run(); + + for (const [actualId, stagingId, dir] of [ + [actualC, stageA, "/workspace/c"], + [actualD, stageC, "/workspace/b"], + ] as const) { + await expect( + database + .prepare( + `INSERT OR REPLACE INTO sandbox_backup ( + created_at, dir, id, keep, operation_id, sandbox_id, sandbox_incarnation, + session_run_id, staging_id, status, ttl_seconds, updated_at, workspace_session_id + ) VALUES (?, ?, ?, 0, ?, ?, 1, NULL, ?, 'ready', 100, ?, NULL)`, + ) + .bind(NOW, dir, actualId, OPERATION_ID, SANDBOX_ID, stagingId, NOW) + .run(), + ).rejects.toThrow("already referenced"); + } + await expect( + database + .prepare("UPDATE OR REPLACE sandbox_backup SET dir = '/workspace/b' WHERE id = ?") + .bind(actualA) + .run(), + ).rejects.toThrow("identity is immutable"); + await expect( + database.prepare("DELETE FROM sandbox_backup WHERE id = ?").bind(actualA).run(), + ).rejects.toThrow("record is permanent"); + expect( + (await database.prepare("SELECT id, staging_id FROM sandbox_backup ORDER BY id").all()) + .results, + ).toEqual([ + { id: actualA, staging_id: stageA }, + { id: actualB, staging_id: stageB }, + ]); + }); + + test("ready and pruned runtime records prevent a second stage from claiming their ID", async () => { + const database = createDatabase(); + const stageA = await createOperationStage(database); + const actualId = encodeSandboxBackupIdForStorage("550e8400-e29b-41d4-a716-446655440025"); + await claimSandboxBackupStageActual(database, { + actualBackupId: actualId, + dir: DIR, + sandboxIncarnation: 1, + stagingId: stageA.id, + }); + await finalizeSandboxBackupStage(database, { + actualBackupId: actualId, + stagingId: stageA.id, + }); + await expect( + database + .prepare( + `INSERT OR REPLACE INTO sandbox_backup_staging ( + actual_backup_id, claim_owner, created_at, dir, driver_generation, + driver_instance_id, id, operation_id, sandbox_id, sandbox_incarnation, + session_run_id, ttl_seconds, updated_at, updates_subject_backup, + workspace_session_id + ) VALUES ( + NULL, 'owner', ?, '/workspace/resurrected', NULL, NULL, ?, ?, ?, 1, + NULL, 100, ?, 0, NULL + )`, + ) + .bind(NOW, stageA.id, OPERATION_ID, SANDBOX_ID, NOW) + .run(), + ).rejects.toThrow("already owned"); + const [write] = await stageSandboxBackupWrites(database, { + admission: { + kind: "operation", + lease: { + claimExpiresAt: NOW + 60_000, + claimOwner: "owner", + incarnation: 1, + kind: "hibernate", + operationId: OPERATION_ID, + status: "backing_up", + }, + }, + sandboxId: SANDBOX_ID, + targets: [ + { + dir: "/workspace/other", + updateSandboxLastBackup: false, + workspaceSessionId: null, + }, + ], + ttlSeconds: 100, + }); + if (write?.kind !== "staged") { + throw new Error("Second runtime backup stage was not created."); + } + + await expect( + claimSandboxBackupStageActual(database, { + actualBackupId: actualId, + dir: write.stage.dir, + sandboxIncarnation: 1, + stagingId: write.stage.id, + }), + ).resolves.toBeNull(); + await database + .prepare("UPDATE sandbox_backup SET status = 'pruned' WHERE id = ?") + .bind(actualId) + .run(); + await expect( + database + .prepare("UPDATE sandbox_backup SET status = 'ready' WHERE id = ?") + .bind(actualId) + .run(), + ).rejects.toThrow("cannot become ready"); + await expect( + claimSandboxBackupStageActual(database, { + actualBackupId: actualId, + dir: write.stage.dir, + sandboxIncarnation: 1, + stagingId: write.stage.id, + }), + ).resolves.toBeNull(); + await expect( + database + .prepare("UPDATE OR REPLACE sandbox_backup_staging SET actual_backup_id = ? WHERE id = ?") + .bind(actualId, write.stage.id) + .run(), + ).rejects.toThrow("already owned"); + await expect( + database + .prepare( + `INSERT OR REPLACE INTO sandbox_backup_staging ( + actual_backup_id, claim_owner, created_at, dir, driver_generation, + driver_instance_id, id, operation_id, sandbox_id, sandbox_incarnation, + session_run_id, ttl_seconds, updated_at, updates_subject_backup, + workspace_session_id + ) VALUES ( + ?, 'owner', ?, '/workspace/third', NULL, NULL, ?, ?, ?, 1, + NULL, 100, ?, 0, NULL + )`, + ) + .bind( + actualId, + NOW, + encodeSandboxBackupIdForStorage("550e8400-e29b-41d4-a716-446655440026"), + OPERATION_ID, + SANDBOX_ID, + NOW, + ) + .run(), + ).rejects.toThrow("already owned"); + expect( + await authorizeSandboxBackupDeletion(database, { + authority: { kind: "pruned" }, + backupId: actualId, + }), + ).toBe(true); + expect(await getSandboxBackupStage(database, write.stage.id)).toMatchObject({ + actualBackupId: null, + }); + }); + + test("a crash after delete intent is retried to physical completion", async () => { + const database = createDatabase(); + const bucket = new MemoryR2Bucket(); + const actualId = bucket.putBackup({ + platformId: "550e8400-e29b-41d4-a716-44665544000b", + stagingId: parsePlatformId("01J0000000000000000000000C"), + }); + expect( + await authorizeSandboxBackupDeletion(database, { + authority: { kind: "unattributed" }, + backupId: actualId, + }), + ).toBe(true); + expect( + await database + .prepare("SELECT deleted_at FROM sandbox_backup_delete_intent WHERE backup_id = ?") + .bind(actualId) + .first(), + ).toEqual({ deleted_at: null }); + + await reconcileSandboxBackupPage(createBindings(database, bucket), { + cursor: null, + }); + + expect(getSandboxBackupObjectKeys(actualId).every((key) => !bucket.objects.has(key))).toBe( + true, + ); + expect( + await database + .prepare("SELECT deleted_at FROM sandbox_backup_delete_intent WHERE backup_id = ?") + .bind(actualId) + .first<{ deleted_at: number | null }>(), + ).toMatchObject({ deleted_at: expect.any(Number) }); + await expect( + database + .prepare("DELETE FROM sandbox_backup_delete_intent WHERE backup_id = ?") + .bind(actualId) + .run(), + ).rejects.toThrow("deletion intent is permanent"); + await database.prepare("PRAGMA recursive_triggers = OFF").run(); + await expect( + database + .prepare( + `INSERT OR REPLACE INTO sandbox_backup_delete_intent ( + attempted_at, backup_id, created_at, delete_after, deleted_at + ) VALUES ( + NULL, ?, CAST(unixepoch('subsec') * 1000 AS INTEGER), + CAST(unixepoch('subsec') * 1000 AS INTEGER), NULL + )`, + ) + .bind(actualId) + .run(), + ).rejects.toThrow("deletion lacks D1 authority"); + bucket.putBackup({ + platformId: "550e8400-e29b-41d4-a716-44665544000b", + stagingId: parsePlatformId("01J0000000000000000000000C"), + }); + expect( + await authorizeSandboxBackupDeletion(database, { + authority: { kind: "unattributed" }, + backupId: actualId, + }), + ).toBe(true); + await deleteAuthorizedSandboxBackupObjects(createBindings(database, bucket), [actualId]); + expect(getSandboxBackupObjectKeys(actualId).every((key) => !bucket.objects.has(key))).toBe( + true, + ); + }); + + test.each([ + ["data", "550e8400-e29b-41d4-a716-446655440019"], + ["metadata", "550e8400-e29b-41d4-a716-44665544001c"], + ] as const)( + "reconciliation resumes after deleting %s when the R2 ACK is lost", + async (kind, id) => { + const database = createDatabase(); + const bucket = new MemoryR2Bucket(); + const actualId = bucket.putBackup({ + platformId: id, + stagingId: parsePlatformId("01J0000000000000000000000C"), + uploaded: new Date(), + }); + await authorizeSandboxBackupDeletion(database, { + authority: { kind: "unattributed" }, + backupId: actualId, + }); + const [dataKey, metadataKey] = getSandboxBackupObjectKeys(actualId); + bucket.failAfterDeletingKey = kind === "data" ? dataKey : metadataKey; + + await reconcileSandboxBackupPage(createBindings(database, bucket), { cursor: null }); + + expect(bucket.objects.has(dataKey)).toBe(false); + expect(bucket.objects.has(metadataKey)).toBe(kind === "data"); + expect( + await database + .prepare( + `SELECT attempted_at, deleted_at + FROM sandbox_backup_delete_intent WHERE backup_id = ?`, + ) + .bind(actualId) + .first(), + ).toEqual({ attempted_at: expect.any(Number), deleted_at: null }); + + await reconcileSandboxBackupPage(createBindings(database, bucket), { cursor: null }); + + expect(bucket.objects.has(metadataKey)).toBe(false); + expect( + await database + .prepare("SELECT deleted_at FROM sandbox_backup_delete_intent WHERE backup_id = ?") + .bind(actualId) + .first(), + ).toEqual({ deleted_at: expect.any(Number) }); + }, + ); + + test("a lost deletion completion acknowledgement remains idempotently complete", async () => { + const database = createDatabase(); + const bucket = new MemoryR2Bucket(); + const actualId = bucket.putBackup({ + platformId: "550e8400-e29b-41d4-a716-44665544001a", + stagingId: parsePlatformId("01J0000000000000000000000C"), + }); + await authorizeSandboxBackupDeletion(database, { + authority: { kind: "unattributed" }, + backupId: actualId, + }); + const fault = loseRunAcknowledgementOnce(database, "SET deleted_at = coalesce"); + + await reconcileSandboxBackupPage(createBindings(fault.database, bucket), { cursor: null }); + + expect(fault.wasLost()).toBe(true); + expect(getSandboxBackupObjectKeys(actualId).every((key) => !bucket.objects.has(key))).toBe( + true, + ); + expect( + await database + .prepare("SELECT deleted_at FROM sandbox_backup_delete_intent WHERE backup_id = ?") + .bind(actualId) + .first(), + ).toEqual({ deleted_at: expect.any(Number) }); + + await reconcileSandboxBackupPage(createBindings(database, bucket), { cursor: null }); + expect(await listPendingSandboxBackupDeletions(database, 1)).toEqual([]); + }); + + test("a deletion cannot be completed before its first physical attempt", async () => { + const database = createDatabase(); + const backupId = encodeSandboxBackupIdForStorage("550e8400-e29b-41d4-a716-44665544001b"); + await authorizeSandboxBackupDeletion(database, { + authority: { kind: "unattributed" }, + backupId, + }); + + await expect( + database + .prepare( + `UPDATE sandbox_backup_delete_intent + SET deleted_at = CAST(unixepoch('subsec') * 1000 AS INTEGER) + WHERE backup_id = ?`, + ) + .bind(backupId) + .run(), + ).rejects.toThrow("completion must use D1 time and is irreversible"); + }); + + test("an unattempted deletion wins an otherwise exact retry ordering tie", async () => { + const database = createDatabase(); + const attempted = encodeSandboxBackupIdForStorage("550e8400-e29b-41d4-a716-44665544001d"); + const unattempted = encodeSandboxBackupIdForStorage("550e8400-e29b-41d4-a716-44665544001e"); + database.execute("DROP TRIGGER sandbox_backup_delete_intent_authority"); + await database + .prepare( + `INSERT INTO sandbox_backup_delete_intent ( + attempted_at, backup_id, created_at, delete_after, deleted_at + ) VALUES (1, ?, 1, 1, NULL), (NULL, ?, 1, 1, NULL)`, + ) + .bind(attempted, unattempted) + .run(); + + expect(await listPendingSandboxBackupDeletions(database, 2)).toEqual([unattempted, attempted]); + }); + + test("cleanup admitted between SDK creation and D1 claim leaves no ready row", async () => { + const database = createDatabase(); + const bucket = new MemoryR2Bucket(); + const stage = await createOperationStage(database, { workspace: true }); + const actualId = bucket.putBackup({ + platformId: "550e8400-e29b-41d4-a716-446655440004", + stagingId: stage.id, + }); + await database + .prepare( + `UPDATE session + SET archived_at = ?, cleanup_operation_kind = 'delete', + status = 'RESCHEDULING', status_operation_id = ? + WHERE id = ?`, + ) + .bind(NOW, CLEANUP_OPERATION_ID, SESSION_ID) + .run(); + + const claim = await claimSandboxBackupStageActual(database, { + actualBackupId: actualId, + dir: DIR, + sandboxIncarnation: 1, + stagingId: stage.id, + }); + expect(claim).toBeNull(); + expect( + await revokeSandboxBackupsForSessionDelete(database, { + cwd: DIR, + operationId: CLEANUP_OPERATION_ID, + sandboxId: SANDBOX_ID, + sessionId: SESSION_ID, + }), + ).toEqual([]); + await reconcileSandboxBackupPage(createBindings(database, bucket), { + cursor: null, + }); + + expect(await readyRows(database)).toEqual([]); + expect(await getSandboxBackupStage(database, stage.id)).toBeNull(); + expect(getSandboxBackupObjectKeys(actualId).every((key) => !bucket.objects.has(key))).toBe( + true, + ); + }); + + for (const mismatch of ["dir", "id", "scope"] as const) { + test(`scanner rejects ${mismatch} metadata that does not match its stage`, async () => { + const database = createDatabase(); + const bucket = new MemoryR2Bucket(); + const stage = await createOperationStage(database); + const platformId = "550e8400-e29b-41d4-a716-446655440005"; + const actualId = bucket.putBackup({ + ...(mismatch === "dir" ? { dir: "/workspace/wrong" } : {}), + ...(mismatch === "id" ? { metadataId: "550e8400-e29b-41d4-a716-446655440006" } : {}), + ...(mismatch === "scope" + ? { + name: createRuntimeSandboxBackupName( + parsePlatformId("01J0000000000000000000000C"), + ), + } + : {}), + platformId, + stagingId: stage.id, + }); + + await reconcileSandboxBackupPage(createBindings(database, bucket), { + cursor: null, + }); + + expect(await readyRows(database)).toEqual([]); + expect((await getSandboxBackupStage(database, stage.id))?.actualBackupId).toBeNull(); + expect(getSandboxBackupObjectKeys(actualId).every((key) => !bucket.objects.has(key))).toBe( + true, + ); + }); + } + + test("reconciliation remains bounded and resumes with an opaque page cursor", async () => { + const database = createDatabase(); + const bucket = new MemoryR2Bucket(); + for (let index = 0; index < 65; index += 1) { + const backupId = encodeSandboxBackupIdForStorage( + `550e8400-e29b-4000-8000-${index.toString(16).padStart(12, "0")}`, + ); + const [dataKey] = getSandboxBackupObjectKeys(backupId); + await bucket.put(dataKey, "orphan"); + } + + const first = await reconcileSandboxBackupPage(createBindings(database, bucket), { + cursor: null, + }); + const second = await reconcileSandboxBackupPage(createBindings(database, bucket), { + cursor: first.nextCursor, + }); + + expect(first).toEqual({ hasMore: true, nextCursor: "opaque-1", processed: 64 }); + expect(second).toEqual({ hasMore: false, nextCursor: null, processed: 1 }); + expect(bucket.objects.size).toBe(0); + }); + + test("database-backed orphan grace retains fresh uploads", async () => { + const database = createDatabase(); + const bucket = new MemoryR2Bucket(); + const backupId = encodeSandboxBackupIdForStorage("550e8400-e29b-41d4-a716-44665544000c"); + const [dataKey] = getSandboxBackupObjectKeys(backupId); + await bucket.put(dataKey, "in-flight", new Date()); + + await reconcileSandboxBackupPage(createBindings(database, bucket), { + cursor: null, + }); + + expect(bucket.objects.has(dataKey)).toBe(true); + }); +}); + +describe("environment artifact backup lifecycle", () => { + test("a pruned runtime backup ID cannot be claimed before its tombstone exists", async () => { + const database = createDatabase(); + const runtimeStage = await createOperationStage(database); + const { authority } = await createEnvironmentArtifactStage(database); + const sharedId = encodeSandboxBackupIdForStorage("650e8400-e29b-41d4-a716-446655440040"); + + expect( + await claimSandboxBackupStageActual(database, { + actualBackupId: sharedId, + dir: DIR, + sandboxIncarnation: 1, + stagingId: runtimeStage.id, + }), + ).toEqual({ actualBackupId: sharedId }); + await finalizeSandboxBackupStage(database, { + actualBackupId: sharedId, + stagingId: runtimeStage.id, + }); + await database + .prepare("UPDATE sandbox_backup SET status = 'pruned' WHERE id = ?") + .bind(sharedId) + .run(); + + await expect( + claimEnvironmentPackageArtifactBackupActual(database, { + actualBackupId: sharedId, + authority, + commandId: ENVIRONMENT_COMMAND_ID, + dir: ENVIRONMENT_DIR, + }), + ).resolves.toBeNull(); + await expect( + database + .prepare( + `UPDATE OR REPLACE environment_package_artifact_backup_staging + SET actual_backup_id = ? WHERE command_id = ?`, + ) + .bind(sharedId, ENVIRONMENT_COMMAND_ID) + .run(), + ).rejects.toThrow("already owned"); + expect( + await database + .prepare("SELECT id, status FROM sandbox_backup WHERE id = ?") + .bind(sharedId) + .first(), + ).toEqual({ id: sharedId, status: "pruned" }); + expect( + await getEnvironmentPackageArtifactBackupStage(database, ENVIRONMENT_COMMAND_ID), + ).toMatchObject({ actualBackupId: null }); + }); + + test("an environment artifact backup ID cannot be claimed by runtime", async () => { + const database = createDatabase(); + const runtimeStage = await createOperationStage(database); + const { authority } = await createEnvironmentArtifactStage(database); + const sharedId = encodeSandboxBackupIdForStorage("650e8400-e29b-41d4-a716-446655440041"); + + expect( + await claimEnvironmentPackageArtifactBackupActual(database, { + actualBackupId: sharedId, + authority, + commandId: ENVIRONMENT_COMMAND_ID, + dir: ENVIRONMENT_DIR, + }), + ).toEqual({ actualBackupId: sharedId }); + expect( + await commitEnvironmentPackageArtifactBackup(database, { + actualBackupId: sharedId, + ...authority, + expiresAt: Date.now() + ENVIRONMENT_PACKAGE_ARTIFACT_BACKUP_TTL_SECONDS * 1_000 - 1_000, + key: ENVIRONMENT_KEY, + paths: ENVIRONMENT_PATHS, + }), + ).toBe(true); + + await expect( + claimSandboxBackupStageActual(database, { + actualBackupId: sharedId, + dir: DIR, + sandboxIncarnation: 1, + stagingId: runtimeStage.id, + }), + ).resolves.toBeNull(); + await expect( + database + .prepare( + `UPDATE OR REPLACE sandbox_backup_staging + SET actual_backup_id = ? WHERE id = ?`, + ) + .bind(sharedId, runtimeStage.id) + .run(), + ).rejects.toThrow("already owned"); + expect(await getSandboxBackupStage(database, runtimeStage.id)).toMatchObject({ + actualBackupId: null, + }); + expect( + await getEnvironmentPackageArtifactBackupManifest(database, ENVIRONMENT_KEY), + ).toMatchObject({ backupId: sharedId }); + await database + .prepare("DELETE FROM environment_package_artifact_backup_staging WHERE command_id = ?") + .bind(ENVIRONMENT_COMMAND_ID) + .run(); + const successor = await createEnvironmentArtifactStage(database, { + attemptCount: 2, + claimOwner: "successor-owner", + deliveryGeneration: 2, + }); + await expect( + claimEnvironmentPackageArtifactBackupActual(database, { + actualBackupId: sharedId, + authority: successor.authority, + commandId: ENVIRONMENT_COMMAND_ID, + dir: ENVIRONMENT_DIR, + }), + ).resolves.toBeNull(); + await expect( + database + .prepare( + `UPDATE OR REPLACE environment_package_artifact_backup_staging + SET actual_backup_id = ? WHERE command_id = ?`, + ) + .bind(sharedId, ENVIRONMENT_COMMAND_ID) + .run(), + ).rejects.toThrow("already owned"); + }); + + test("bounded reconciliation removes a terminal stage before any R2 object exists", async () => { + const database = createDatabase(); + const bucket = new MemoryR2Bucket(); + await createEnvironmentArtifactStage(database); + database.execute( + `UPDATE api_command SET status = 'failed' WHERE id = '${ENVIRONMENT_COMMAND_ID}'`, + ); + + await reconcileSandboxBackupPage(createBindings(database, bucket), { + cursor: null, + }); + + expect( + await getEnvironmentPackageArtifactBackupStage(database, ENVIRONMENT_COMMAND_ID), + ).toBeNull(); + }); + + test("an empty R2 page continues until every terminal stage is revoked", async () => { + const database = createDatabase(); + const bucket = new MemoryR2Bucket(); + for (let index = 0; index < 65; index += 1) { + const commandId = createPlatformId(); + const inputDigest = index.toString(16).padStart(64, "0"); + const dir = `/workspace/.mosoo/environment-artifacts/${inputDigest}`; + await database + .prepare( + `INSERT INTO api_command ( + attempt_count, claim_expires_at, claim_owner, created_at, dedupe_key, + delivery_generation, id, kind, payload_json, status, updated_at + ) VALUES (1, ?, 'owner', ?, ?, 1, ?, 'environment_package_artifact_build', ?, + 'running', ?)`, + ) + .bind( + Date.now() + 60_000, + NOW, + `terminal-environment-stage:${commandId}`, + commandId, + JSON.stringify({ appId: ENVIRONMENT_APP_ID, inputDigest }), + NOW, + ) + .run(); + await database + .prepare( + `INSERT INTO environment_package_artifact_backup_staging ( + actual_backup_id, app_id, attempt_count, claim_owner, command_id, created_at, + delivery_generation, dir, input_digest, paths_json, updated_at + ) VALUES (NULL, ?, 1, 'owner', ?, ?, 1, ?, ?, ?, ?)`, + ) + .bind( + ENVIRONMENT_APP_ID, + commandId, + NOW, + dir, + inputDigest, + JSON.stringify({ executable: [`${dir}/bin/tool`], node: [], python: [] }), + NOW, + ) + .run(); + } + await database + .prepare( + "UPDATE api_command SET status = 'failed' WHERE dedupe_key LIKE 'terminal-environment-stage:%'", + ) + .run(); + + const first = await reconcileSandboxBackupPage(createBindings(database, bucket), { + cursor: null, + }); + expect(first).toEqual({ hasMore: true, nextCursor: null, processed: 0 }); + await expect( + database + .prepare("SELECT count(*) AS count FROM environment_package_artifact_backup_staging") + .first(), + ).resolves.toEqual({ count: 1 }); + + const second = await reconcileSandboxBackupPage(createBindings(database, bucket), { + cursor: null, + }); + expect(second).toEqual({ hasMore: false, nextCursor: null, processed: 0 }); + await expect( + database + .prepare("SELECT count(*) AS count FROM environment_package_artifact_backup_staging") + .first(), + ).resolves.toEqual({ count: 0 }); + }); + + test("scanner commits a complete backup to D1 without writing a projection", async () => { + const database = createDatabase(); + const bucket = new MemoryR2Bucket(); + const { authority } = await createEnvironmentArtifactStage(database); + const actualId = bucket.putBackup({ + dir: ENVIRONMENT_DIR, + name: createEnvironmentPackageArtifactBackupName(authority), + platformId: "650e8400-e29b-41d4-a716-446655440001", + }); + await reconcileSandboxBackupPage(createBindings(database, bucket), { + cursor: null, + }); + expect( + await getEnvironmentPackageArtifactBackupStage(database, ENVIRONMENT_COMMAND_ID), + ).toBeNull(); + expect( + await getEnvironmentPackageArtifactBackupManifest(database, ENVIRONMENT_KEY), + ).toMatchObject({ backupId: actualId, manifestGeneration: 1, paths: ENVIRONMENT_PATHS }); + expect(bucket.objects.has(environmentPackageArtifactMetadataKey(ENVIRONMENT_KEY))).toBe(false); + expect(getSandboxBackupObjectKeys(actualId).every((key) => bucket.objects.has(key))).toBe(true); + }); + + test("a D1 manifest resolves without an R2 projection", async () => { + const database = createDatabase(); + const bucket = new MemoryR2Bucket(); + const { authority } = await createEnvironmentArtifactStage(database); + const actualId = bucket.putBackup({ + dir: ENVIRONMENT_DIR, + name: createEnvironmentPackageArtifactBackupName(authority), + platformId: "650e8400-e29b-41d4-a716-446655440009", + }); + await claimEnvironmentPackageArtifactBackupActual(database, { + actualBackupId: actualId, + authority, + commandId: authority.commandId, + dir: ENVIRONMENT_DIR, + }); + expect( + await commitEnvironmentPackageArtifactBackup(database, { + actualBackupId: actualId, + ...authority, + expiresAt: bucket.backupExpiresAt(actualId), + key: ENVIRONMENT_KEY, + paths: ENVIRONMENT_PATHS, + }), + ).toBe(true); + expect(bucket.objects.has(environmentPackageArtifactMetadataKey(ENVIRONMENT_KEY))).toBe(false); + const manifest = await getEnvironmentPackageArtifactBackupManifest(database, ENVIRONMENT_KEY); + expect(manifest?.expiresAt).toBe(bucket.backupExpiresAt(actualId)); + expect(manifest?.expiresAt).not.toBe( + OLD_UPLOAD.getTime() + ENVIRONMENT_PACKAGE_ARTIFACT_BACKUP_TTL_SECONDS * 1_000, + ); + + await expect( + resolveEnvironmentPackageArtifactBackup(createBindings(database, bucket), ENVIRONMENT_KEY), + ).resolves.toMatchObject({ + backupId: "650e8400-e29b-41d4-a716-446655440009", + paths: ENVIRONMENT_PATHS, + }); + }); + + test.each([ + ["data", 0], + ["metadata", 1], + ] as const)( + "a current manifest resolves null when its %s object is missing", + async (_kind, keyIndex) => { + const database = createDatabase(); + const bucket = new MemoryR2Bucket(); + const { authority } = await createEnvironmentArtifactStage(database); + const actualId = bucket.putBackup({ + dir: ENVIRONMENT_DIR, + name: createEnvironmentPackageArtifactBackupName(authority), + platformId: "650e8400-e29b-41d4-a716-446655440019", + }); + await claimEnvironmentPackageArtifactBackupActual(database, { + actualBackupId: actualId, + authority, + commandId: authority.commandId, + dir: ENVIRONMENT_DIR, + }); + await commitEnvironmentPackageArtifactBackup(database, { + actualBackupId: actualId, + ...authority, + expiresAt: bucket.backupExpiresAt(actualId), + key: ENVIRONMENT_KEY, + paths: ENVIRONMENT_PATHS, + }); + await bucket.delete(getSandboxBackupObjectKeys(actualId)[keyIndex]); + + await expect( + resolveEnvironmentPackageArtifactBackup(createBindings(database, bucket), ENVIRONMENT_KEY), + ).resolves.toBeNull(); + expect( + await getEnvironmentPackageArtifactBackupManifest(database, ENVIRONMENT_KEY), + ).toMatchObject({ backupId: actualId, manifestGeneration: 1 }); + }, + ); + + test("a near-expiry current manifest resolves null without reading R2", async () => { + const database = createDatabase(); + const bucket = new MemoryR2Bucket(); + const { authority } = await createEnvironmentArtifactStage(database); + const actualId = bucket.putBackup({ + createdAt: new Date( + Date.now() - + ENVIRONMENT_PACKAGE_ARTIFACT_BACKUP_TTL_SECONDS * 1_000 + + ENVIRONMENT_PACKAGE_ARTIFACT_BACKUP_REFRESH_WINDOW_MS / 2, + ), + dir: ENVIRONMENT_DIR, + name: createEnvironmentPackageArtifactBackupName(authority), + platformId: "650e8400-e29b-41d4-a716-44665544001a", + }); + const expiresAt = bucket.backupExpiresAt(actualId); + database.execute("DROP TRIGGER environment_package_artifact_backup_authority"); + await database + .prepare( + `INSERT INTO environment_package_artifact_backup ( + app_id, attempt_count, backup_id, command_id, committed_at, + delivery_generation, expires_at, input_digest, manifest_generation, paths_json + ) VALUES (?, 1, ?, ?, ?, 1, ?, ?, 1, ?)`, + ) + .bind( + ENVIRONMENT_APP_ID, + actualId, + ENVIRONMENT_COMMAND_ID, + expiresAt - 25 * 60 * 60_000, + expiresAt, + ENVIRONMENT_KEY.inputDigest, + JSON.stringify(ENVIRONMENT_PATHS), + ) + .run(); + + await expect( + resolveEnvironmentPackageArtifactBackup(createBindings(database, bucket), ENVIRONMENT_KEY), + ).resolves.toBeNull(); + expect(bucket.gets).not.toContain(getSandboxBackupObjectKeys(actualId)[1]); + }); + + test("default resolution automatically requeues a succeeded near-expiry artifact", async () => { + const database = createDatabase(); + const bucket = new MemoryR2Bucket(); + const buildQueue = createApiCommandQueueStub(); + const bindings = { + ...createBindings(database, bucket), + API_COMMAND_QUEUE: createApiCommandQueueStub(), + ENVIRONMENT_ARTIFACT_BUILD_QUEUE: buildQueue, + } as ApiBindings; + const packages = [{ manager: "pip", packages: ["requests==2.32.4"] }] as const; + const initial = await resolveEnvironmentPackageArtifact(bindings, ENVIRONMENT_APP_ID, packages); + if (initial === null) { + throw new Error("Environment package artifact resolution returned no key."); + } + const command = await database + .prepare( + `SELECT id FROM api_command + WHERE kind = 'environment_package_artifact_build'`, + ) + .first<{ id: ApiCommandId }>(); + if (command === null) { + throw new Error("Environment package artifact command was not enqueued."); + } + await database + .prepare( + `UPDATE api_command + SET attempt_count = 1, claim_expires_at = NULL, claim_owner = NULL, + completed_at = CAST(unixepoch('subsec') * 1000 AS INTEGER), status = 'succeeded' + WHERE id = ?`, + ) + .bind(command.id) + .run(); + const dir = environmentPackageArtifactDir(initial.key); + const paths: EnvironmentPackageArtifactPaths = { + executable: [`${dir}/python/bin`], + node: [`${dir}/npm/node_modules`], + python: [`${dir}/python/site-packages`], + }; + const backupId = encodeSandboxBackupIdForStorage("650e8400-e29b-41d4-a716-44665544002b"); + await database + .prepare( + `INSERT INTO environment_package_artifact_backup ( + app_id, attempt_count, backup_id, command_id, committed_at, + delivery_generation, expires_at, input_digest, manifest_generation, paths_json + ) VALUES ( + ?, 1, ?, ?, CAST(unixepoch('subsec') * 1000 AS INTEGER), 1, + CAST(unixepoch('subsec') * 1000 AS INTEGER) + 315359999000, ?, 1, ? + )`, + ) + .bind(initial.key.appId, backupId, command.id, initial.key.inputDigest, JSON.stringify(paths)) + .run(); + await expireEnvironmentArtifactManifest( + database, + Date.now() + ENVIRONMENT_PACKAGE_ARTIFACT_BACKUP_REFRESH_WINDOW_MS / 2, + initial.key, + ); + + await expect( + resolveEnvironmentPackageArtifact(bindings, ENVIRONMENT_APP_ID, packages), + ).resolves.toMatchObject({ key: initial.key, metadata: null }); + expect(buildQueue.sent).toHaveLength(2); + expect( + await database + .prepare("SELECT delivery_generation, status FROM api_command WHERE id = ?") + .bind(command.id) + .first(), + ).toEqual({ delivery_generation: 2, status: "queued" }); + }); + + test("a delete intent that wins D1 prevents a late environment projection", async () => { + const database = createDatabase(); + const bucket = new MemoryR2Bucket(); + const { authority } = await createEnvironmentArtifactStage(database); + const actualId = bucket.putBackup({ + dir: ENVIRONMENT_DIR, + name: createEnvironmentPackageArtifactBackupName(authority), + platformId: "650e8400-e29b-41d4-a716-44665544000a", + }); + await claimEnvironmentPackageArtifactBackupActual(database, { + actualBackupId: actualId, + authority, + commandId: authority.commandId, + dir: ENVIRONMENT_DIR, + }); + database.execute("DROP TRIGGER sandbox_backup_delete_intent_authority"); + await database + .prepare( + `INSERT INTO sandbox_backup_delete_intent ( + attempted_at, backup_id, created_at, delete_after, deleted_at + ) VALUES ( + NULL, ?, CAST(unixepoch('subsec') * 1000 AS INTEGER), + CAST(unixepoch('subsec') * 1000 AS INTEGER), NULL + )`, + ) + .bind(actualId) + .run(); + database.execute("DROP TRIGGER environment_package_artifact_backup_authority"); + + await expect( + database + .prepare( + `INSERT INTO environment_package_artifact_backup ( + app_id, attempt_count, backup_id, command_id, committed_at, + delivery_generation, expires_at, input_digest, manifest_generation, paths_json + ) VALUES ( + ?, 1, ?, ?, CAST(unixepoch('subsec') * 1000 AS INTEGER), 1, + ?, ?, 1, ? + )`, + ) + .bind( + ENVIRONMENT_APP_ID, + actualId, + ENVIRONMENT_COMMAND_ID, + bucket.backupExpiresAt(actualId), + ENVIRONMENT_KEY.inputDigest, + JSON.stringify(ENVIRONMENT_PATHS), + ) + .run(), + ).rejects.toThrow("sandbox backup object is tombstoned or already referenced"); + expect(await getEnvironmentPackageArtifactBackupStage(database, authority.commandId)).toEqual( + expect.objectContaining({ actualBackupId: actualId }), + ); + await deleteAuthorizedSandboxBackupObjects(createBindings(database, bucket), [actualId]); + expect(bucket.objects.has(environmentPackageArtifactMetadataKey(ENVIRONMENT_KEY))).toBe(false); + expect(getSandboxBackupObjectKeys(actualId).every((key) => !bucket.objects.has(key))).toBe( + true, + ); + }); + + test("a verified named R2 projection is atomically adopted into D1", async () => { + const database = createDatabase(); + const bucket = new MemoryR2Bucket(); + const attemptA = await createEnvironmentArtifactStage(database); + const actualId = bucket.putBackup({ + dir: ENVIRONMENT_DIR, + name: createEnvironmentPackageArtifactBackupName(attemptA.authority), + platformId: "650e8400-e29b-41d4-a716-44665544000b", + }); + const legacyKey = environmentPackageArtifactMetadataKey(ENVIRONMENT_KEY); + await bucket.put( + legacyKey, + JSON.stringify({ + backupId: "650e8400-e29b-41d4-a716-44665544000b", + paths: ENVIRONMENT_PATHS, + }), + ); + await succeedEnvironmentArtifactCommand(database); + + await expect( + resolveEnvironmentPackageArtifactBackup(createBindings(database, bucket), ENVIRONMENT_KEY), + ).resolves.toMatchObject({ paths: ENVIRONMENT_PATHS }); + expect( + await getEnvironmentPackageArtifactBackupManifest(database, ENVIRONMENT_KEY), + ).toMatchObject({ backupId: actualId, paths: ENVIRONMENT_PATHS }); + await bucket.delete(legacyKey); + await expect( + resolveEnvironmentPackageArtifactBackup(createBindings(database, bucket), ENVIRONMENT_KEY), + ).resolves.toMatchObject({ paths: ENVIRONMENT_PATHS }); + expect(bucket.gets.filter((key) => key === legacyKey)).toHaveLength(1); + expect( + await authorizeSandboxBackupDeletion(database, { + authority: { kind: "unattributed" }, + backupId: actualId, + }), + ).toBe(false); + }); + + test("a real legacy nameless R2 projection is atomically adopted into D1", async () => { + const database = createDatabase(); + const bucket = new MemoryR2Bucket(); + await createEnvironmentArtifactStage(database); + const actualId = bucket.putBackup({ + dir: ENVIRONMENT_DIR, + name: null, + platformId: "650e8400-e29b-41d4-a716-446655440010", + }); + await bucket.put( + environmentPackageArtifactMetadataKey(ENVIRONMENT_KEY), + JSON.stringify({ + backupId: "650e8400-e29b-41d4-a716-446655440010", + paths: ENVIRONMENT_PATHS, + }), + ); + await succeedEnvironmentArtifactCommand(database); + + await expect( + resolveEnvironmentPackageArtifactBackup(createBindings(database, bucket), ENVIRONMENT_KEY), + ).resolves.toMatchObject({ paths: ENVIRONMENT_PATHS }); + expect( + await getEnvironmentPackageArtifactBackupManifest(database, ENVIRONMENT_KEY), + ).toMatchObject({ + attemptCount: 1, + backupId: actualId, + commandId: ENVIRONMENT_COMMAND_ID, + deliveryGeneration: 1, + paths: ENVIRONMENT_PATHS, + }); + await createEnvironmentArtifactStage(database, { + attemptCount: 2, + claimOwner: "successor-owner", + deliveryGeneration: 2, + }); + await expect( + resolveEnvironmentPackageArtifactBackup(createBindings(database, bucket), ENVIRONMENT_KEY), + ).resolves.toMatchObject({ paths: ENVIRONMENT_PATHS }); + }); + + test("a D1 manifest ignores a stale legacy R2 projection", async () => { + const database = createDatabase(); + const bucket = new MemoryR2Bucket(); + const { authority } = await createEnvironmentArtifactStage(database); + const actualId = bucket.putBackup({ + dir: ENVIRONMENT_DIR, + name: createEnvironmentPackageArtifactBackupName(authority), + platformId: "650e8400-e29b-41d4-a716-44665544000c", + }); + await claimEnvironmentPackageArtifactBackupActual(database, { + actualBackupId: actualId, + authority, + commandId: authority.commandId, + dir: ENVIRONMENT_DIR, + }); + await commitEnvironmentPackageArtifactBackup(database, { + actualBackupId: actualId, + ...authority, + expiresAt: bucket.backupExpiresAt(actualId), + key: ENVIRONMENT_KEY, + paths: ENVIRONMENT_PATHS, + }); + await bucket.put( + environmentPackageArtifactMetadataKey(ENVIRONMENT_KEY), + JSON.stringify({ + backupId: "650e8400-e29b-41d4-a716-44665544000d", + paths: ENVIRONMENT_PATHS, + }), + ); + + await expect( + resolveEnvironmentPackageArtifactBackup(createBindings(database, bucket), ENVIRONMENT_KEY), + ).resolves.toEqual({ + backupId: "650e8400-e29b-41d4-a716-44665544000c", + paths: ENVIRONMENT_PATHS, + }); + expect(getSandboxBackupObjectKeys(actualId).every((key) => bucket.objects.has(key))).toBe(true); + }); + + test("a D1 manifest rejects mismatched object authority metadata", async () => { + const database = createDatabase(); + const bucket = new MemoryR2Bucket(); + const { authority } = await createEnvironmentArtifactStage(database); + const actualId = bucket.putBackup({ + dir: ENVIRONMENT_DIR, + name: createEnvironmentPackageArtifactBackupName(authority), + platformId: "650e8400-e29b-41d4-a716-44665544000e", + }); + await claimEnvironmentPackageArtifactBackupActual(database, { + actualBackupId: actualId, + authority, + commandId: authority.commandId, + dir: ENVIRONMENT_DIR, + }); + await commitEnvironmentPackageArtifactBackup(database, { + actualBackupId: actualId, + ...authority, + expiresAt: bucket.backupExpiresAt(actualId), + key: ENVIRONMENT_KEY, + paths: ENVIRONMENT_PATHS, + }); + await bucket.put( + environmentPackageArtifactMetadataKey(ENVIRONMENT_KEY), + JSON.stringify({ + backupId: "650e8400-e29b-41d4-a716-44665544000e", + paths: ENVIRONMENT_PATHS, + }), + ); + const [, backupMetadataKey] = getSandboxBackupObjectKeys(actualId); + const backupMetadata = JSON.parse(bucket.objects.get(backupMetadataKey)!.body) as Record< + string, + unknown + >; + for (const tamperedAuthority of [ + { ...authority, commandId: "01J0000000000000000000000L" }, + { ...authority, deliveryGeneration: authority.deliveryGeneration + 1 }, + { ...authority, attemptCount: authority.attemptCount + 1 }, + ]) { + await bucket.put( + backupMetadataKey, + JSON.stringify({ + ...backupMetadata, + name: createEnvironmentPackageArtifactBackupName(tamperedAuthority), + }), + ); + await expect( + resolveEnvironmentPackageArtifactBackup(createBindings(database, bucket), ENVIRONMENT_KEY), + ).resolves.toBeNull(); + } + for (const tamperedMetadata of [ + { ...backupMetadata, ttl: ENVIRONMENT_PACKAGE_ARTIFACT_BACKUP_TTL_SECONDS - 1 }, + { ...backupMetadata, createdAt: "2026-01-01" }, + ]) { + await bucket.put(backupMetadataKey, JSON.stringify(tamperedMetadata)); + await expect( + resolveEnvironmentPackageArtifactBackup(createBindings(database, bucket), ENVIRONMENT_KEY), + ).resolves.toBeNull(); + } + }); + + test("a current manifest rejects direct, replacement, and malformed writes", async () => { + const database = createDatabase(); + const bucket = new MemoryR2Bucket(); + const { authority } = await createEnvironmentArtifactStage(database); + const actualId = bucket.putBackup({ + dir: ENVIRONMENT_DIR, + name: createEnvironmentPackageArtifactBackupName(authority), + platformId: "650e8400-e29b-41d4-a716-446655440014", + }); + await claimEnvironmentPackageArtifactBackupActual(database, { + actualBackupId: actualId, + authority, + commandId: authority.commandId, + dir: ENVIRONMENT_DIR, + }); + await commitEnvironmentPackageArtifactBackup(database, { + actualBackupId: actualId, + ...authority, + expiresAt: bucket.backupExpiresAt(actualId), + key: ENVIRONMENT_KEY, + paths: ENVIRONMENT_PATHS, + }); + await database.prepare("PRAGMA recursive_triggers = OFF").run(); + await expect( + database + .prepare( + `INSERT OR REPLACE INTO environment_package_artifact_backup ( + app_id, attempt_count, backup_id, command_id, committed_at, + delivery_generation, expires_at, input_digest, manifest_generation, paths_json + ) VALUES (?, ?, ?, ?, CAST(unixepoch('subsec') * 1000 AS INTEGER), ?, ?, ?, 1, ?)`, + ) + .bind( + ENVIRONMENT_APP_ID, + authority.attemptCount, + actualId, + authority.commandId, + authority.deliveryGeneration, + bucket.backupExpiresAt(actualId), + ENVIRONMENT_KEY.inputDigest, + JSON.stringify(ENVIRONMENT_PATHS), + ) + .run(), + ).rejects.toThrow("backup lacks D1 authority"); + + const malformedDatabase = createDatabase(); + const malformedStage = await createEnvironmentArtifactStage(malformedDatabase); + const malformedId = encodeSandboxBackupIdForStorage("650e8400-e29b-41d4-a716-446655440015"); + await claimEnvironmentPackageArtifactBackupActual(malformedDatabase, { + actualBackupId: malformedId, + authority: malformedStage.authority, + commandId: ENVIRONMENT_COMMAND_ID, + dir: ENVIRONMENT_DIR, + }); + const duplicatePaths = `{"executable":[],"executable":[],"node":[],"python":[]}`; + malformedDatabase.execute("DROP TRIGGER environment_package_artifact_backup_staging_immutable"); + await malformedDatabase + .prepare( + "UPDATE environment_package_artifact_backup_staging SET paths_json = ? WHERE command_id = ?", + ) + .bind(duplicatePaths, ENVIRONMENT_COMMAND_ID) + .run(); + await expect( + malformedDatabase + .prepare( + `INSERT INTO environment_package_artifact_backup ( + app_id, attempt_count, backup_id, command_id, committed_at, + delivery_generation, expires_at, input_digest, manifest_generation, paths_json + ) VALUES (?, 1, ?, ?, CAST(unixepoch('subsec') * 1000 AS INTEGER), 1, + CAST(unixepoch('subsec') * 1000 AS INTEGER) + 315360000000, ?, 1, ?)`, + ) + .bind( + ENVIRONMENT_APP_ID, + malformedId, + ENVIRONMENT_COMMAND_ID, + ENVIRONMENT_KEY.inputDigest, + duplicatePaths, + ) + .run(), + ).rejects.toThrow("backup lacks D1 authority"); + + const invalidClockDatabase = createDatabase(); + const invalidClockStage = await createEnvironmentArtifactStage(invalidClockDatabase); + const invalidClockId = encodeSandboxBackupIdForStorage("650e8400-e29b-41d4-a716-446655440016"); + await claimEnvironmentPackageArtifactBackupActual(invalidClockDatabase, { + actualBackupId: invalidClockId, + authority: invalidClockStage.authority, + commandId: ENVIRONMENT_COMMAND_ID, + dir: ENVIRONMENT_DIR, + }); + await expect( + invalidClockDatabase + .prepare( + `INSERT INTO environment_package_artifact_backup ( + app_id, attempt_count, backup_id, command_id, committed_at, + delivery_generation, expires_at, input_digest, manifest_generation, paths_json + ) VALUES (?, 1, ?, ?, 0, 1, 315360000000, ?, 1, ?)`, + ) + .bind( + ENVIRONMENT_APP_ID, + invalidClockId, + ENVIRONMENT_COMMAND_ID, + ENVIRONMENT_KEY.inputDigest, + JSON.stringify(ENVIRONMENT_PATHS), + ) + .run(), + ).rejects.toThrow("backup lacks D1 authority"); + }); + + test("a raw manifest insert accepts an exact live staged command", async () => { + const database = createDatabase(); + const { authority } = await createEnvironmentArtifactStage(database); + const backupId = encodeSandboxBackupIdForStorage("650e8400-e29b-41d4-a716-446655440017"); + await claimEnvironmentPackageArtifactBackupActual(database, { + actualBackupId: backupId, + authority, + commandId: ENVIRONMENT_COMMAND_ID, + dir: ENVIRONMENT_DIR, + }); + + await insertRawEnvironmentArtifactManifest(database, backupId); + + expect( + await getEnvironmentPackageArtifactBackupManifest(database, ENVIRONMENT_KEY), + ).toMatchObject({ backupId }); + }); + + test.each(["queued", "failed", "dead_lettered"] as const)( + "an exact live stage cannot borrow a %s command", + async (status) => { + const database = createDatabase(); + const { authority } = await createEnvironmentArtifactStage(database); + const backupId = encodeSandboxBackupIdForStorage( + `650e8400-e29b-4000-8000-${status.length.toString(16).padStart(12, "0")}`, + ); + await claimEnvironmentPackageArtifactBackupActual(database, { + actualBackupId: backupId, + authority, + commandId: ENVIRONMENT_COMMAND_ID, + dir: ENVIRONMENT_DIR, + }); + await database + .prepare("UPDATE api_command SET status = ? WHERE id = ?") + .bind(status, ENVIRONMENT_COMMAND_ID) + .run(); + + await expect(insertRawEnvironmentArtifactManifest(database, backupId)).rejects.toThrow( + "backup lacks D1 authority", + ); + expect( + await getEnvironmentPackageArtifactBackupManifest(database, ENVIRONMENT_KEY), + ).toBeNull(); + }, + ); + + test("an exact staged running command requires a live lease", async () => { + const database = createDatabase(); + const { authority } = await createEnvironmentArtifactStage(database); + const backupId = encodeSandboxBackupIdForStorage("650e8400-e29b-41d4-a716-446655440018"); + await claimEnvironmentPackageArtifactBackupActual(database, { + actualBackupId: backupId, + authority, + commandId: ENVIRONMENT_COMMAND_ID, + dir: ENVIRONMENT_DIR, + }); + await database + .prepare("UPDATE api_command SET claim_expires_at = 0 WHERE id = ?") + .bind(ENVIRONMENT_COMMAND_ID) + .run(); + + await expect(insertRawEnvironmentArtifactManifest(database, backupId)).rejects.toThrow( + "backup lacks D1 authority", + ); + }); + + test("a succeeded command can authorize an unstaged legacy manifest", async () => { + const database = createDatabase(); + await setEnvironmentArtifactCommand(database, { + attemptCount: 1, + claimOwner: "environment-owner", + deliveryGeneration: 1, + }); + await succeedEnvironmentArtifactCommand(database); + const backupId = encodeSandboxBackupIdForStorage("650e8400-e29b-41d4-a716-44665544001f"); + + await insertRawEnvironmentArtifactManifest(database, backupId); + + expect( + await getEnvironmentPackageArtifactBackupManifest(database, ENVIRONMENT_KEY), + ).toMatchObject({ backupId }); + }); + + test.each([ + ["terminal status", "UPDATE api_command SET status = 'failed' WHERE id = ?"], + ["completion clock", "UPDATE api_command SET completed_at = NULL WHERE id = ?"], + ["claim owner", "UPDATE api_command SET claim_owner = 'stale-owner' WHERE id = ?"], + ["claim lease", "UPDATE api_command SET claim_expires_at = 9007199254740991 WHERE id = ?"], + ] as const)( + "an unstaged legacy manifest requires an exact succeeded command %s", + async (_, sql) => { + const database = createDatabase(); + await setEnvironmentArtifactCommand(database, { + attemptCount: 1, + claimOwner: "environment-owner", + deliveryGeneration: 1, + }); + await succeedEnvironmentArtifactCommand(database); + await database.prepare(sql).bind(ENVIRONMENT_COMMAND_ID).run(); + const backupId = encodeSandboxBackupIdForStorage("650e8400-e29b-41d4-a716-446655440047"); + + await expect(insertRawEnvironmentArtifactManifest(database, backupId)).rejects.toThrow( + "backup lacks D1 authority", + ); + expect( + await getEnvironmentPackageArtifactBackupManifest(database, ENVIRONMENT_KEY), + ).toBeNull(); + }, + ); + + test("OR REPLACE cannot move a live environment candidate to another key", async () => { + const database = createDatabase(); + const { authority } = await createEnvironmentArtifactStage(database); + const actualId = encodeSandboxBackupIdForStorage("650e8400-e29b-41d4-a716-446655440042"); + await claimEnvironmentPackageArtifactBackupActual(database, { + actualBackupId: actualId, + authority, + commandId: ENVIRONMENT_COMMAND_ID, + dir: ENVIRONMENT_DIR, + }); + + const appB = parsePlatformId("01J0000000000000000000000M"); + const commandB = parsePlatformId("01J0000000000000000000000N"); + const keyB: EnvironmentPackageArtifactKey = { appId: appB, inputDigest: "b".repeat(64) }; + const dirB = environmentPackageArtifactDir(keyB); + const pathsB: EnvironmentPackageArtifactPaths = { + executable: [`${dirB}/python/bin`], + node: [`${dirB}/npm/node_modules`], + python: [`${dirB}/python/site-packages`], + }; + await database + .prepare( + `INSERT INTO api_command ( + attempt_count, claim_expires_at, claim_owner, created_at, dedupe_key, + delivery_generation, id, kind, payload_json, status, updated_at + ) VALUES ( + 1, CAST(unixepoch('subsec') * 1000 AS INTEGER) + 60000, 'owner-b', ?, ?, 1, ?, + 'environment_package_artifact_build', ?, 'running', ? + )`, + ) + .bind( + NOW, + `environment-artifact:${appB}:${keyB.inputDigest}`, + commandB, + JSON.stringify(keyB), + NOW, + ) + .run(); + await database.prepare("PRAGMA recursive_triggers = OFF").run(); + + await expect( + database + .prepare( + `INSERT OR REPLACE INTO environment_package_artifact_backup_staging ( + actual_backup_id, app_id, attempt_count, claim_owner, command_id, created_at, + delivery_generation, dir, input_digest, paths_json, updated_at + ) VALUES ( + ?, ?, 1, 'owner-b', ?, CAST(unixepoch('subsec') * 1000 AS INTEGER), + 1, ?, ?, ?, CAST(unixepoch('subsec') * 1000 AS INTEGER) + )`, + ) + .bind(actualId, appB, commandB, dirB, keyB.inputDigest, JSON.stringify(pathsB)) + .run(), + ).rejects.toThrow("already owned"); + + await database + .prepare( + `UPDATE api_command + SET claim_expires_at = NULL, claim_owner = NULL, + completed_at = CAST(unixepoch('subsec') * 1000 AS INTEGER), status = 'succeeded' + WHERE id = ?`, + ) + .bind(commandB) + .run(); + await expect( + database + .prepare( + `INSERT OR REPLACE INTO environment_package_artifact_backup ( + app_id, attempt_count, backup_id, command_id, committed_at, + delivery_generation, expires_at, input_digest, manifest_generation, paths_json + ) VALUES ( + ?, 1, ?, ?, CAST(unixepoch('subsec') * 1000 AS INTEGER), 1, + CAST(unixepoch('subsec') * 1000 AS INTEGER) + 315359999000, ?, 1, ? + )`, + ) + .bind(appB, actualId, commandB, keyB.inputDigest, JSON.stringify(pathsB)) + .run(), + ).rejects.toThrow("backup lacks D1 authority"); + + expect( + await getEnvironmentPackageArtifactBackupStage(database, ENVIRONMENT_COMMAND_ID), + ).toMatchObject({ actualBackupId: actualId }); + expect(await getEnvironmentPackageArtifactBackupManifest(database, keyB)).toBeNull(); + }); + + test("OR REPLACE cannot replace an occupied environment stage key with a new backup", async () => { + const database = createDatabase(); + await createEnvironmentArtifactStage(database); + const commandB = parsePlatformId("01J0000000000000000000000P"); + const actualB = encodeSandboxBackupIdForStorage("650e8400-e29b-41d4-a716-446655440048"); + await setEnvironmentArtifactCommand(database, { + attemptCount: 1, + claimOwner: "owner-b", + commandId: commandB, + dedupeKey: `environment-artifact-replacement:${commandB}`, + deliveryGeneration: 1, + key: ENVIRONMENT_KEY, + }); + await database.prepare("PRAGMA recursive_triggers = OFF").run(); + + await expect( + database + .prepare( + `INSERT OR REPLACE INTO environment_package_artifact_backup_staging ( + actual_backup_id, app_id, attempt_count, claim_owner, command_id, created_at, + delivery_generation, dir, input_digest, paths_json, updated_at + ) VALUES ( + ?, ?, 1, 'owner-b', ?, CAST(unixepoch('subsec') * 1000 AS INTEGER), + 1, ?, ?, ?, CAST(unixepoch('subsec') * 1000 AS INTEGER) + )`, + ) + .bind( + actualB, + ENVIRONMENT_APP_ID, + commandB, + ENVIRONMENT_DIR, + ENVIRONMENT_KEY.inputDigest, + JSON.stringify(ENVIRONMENT_PATHS), + ) + .run(), + ).rejects.toThrow("already owned"); + + expect( + await getEnvironmentPackageArtifactBackupStage(database, ENVIRONMENT_COMMAND_ID), + ).not.toBeNull(); + expect(await getEnvironmentPackageArtifactBackupStage(database, commandB)).toBeNull(); + }); + + test("OR REPLACE cannot replace an occupied environment manifest key with a new backup", async () => { + const database = createDatabase(); + const { authority } = await createEnvironmentArtifactStage(database); + const actualA = encodeSandboxBackupIdForStorage("650e8400-e29b-41d4-a716-446655440049"); + const actualB = encodeSandboxBackupIdForStorage("650e8400-e29b-41d4-a716-44665544004a"); + await claimEnvironmentPackageArtifactBackupActual(database, { + actualBackupId: actualA, + authority, + commandId: ENVIRONMENT_COMMAND_ID, + dir: ENVIRONMENT_DIR, + }); + await insertRawEnvironmentArtifactManifest(database, actualA); + const commandB = parsePlatformId("01J0000000000000000000000Q"); + await setEnvironmentArtifactCommand(database, { + attemptCount: 1, + claimOwner: "owner-b", + commandId: commandB, + dedupeKey: `environment-artifact-replacement:${commandB}`, + deliveryGeneration: 1, + key: ENVIRONMENT_KEY, + }); + await succeedEnvironmentArtifactCommand(database, commandB); + await database.prepare("PRAGMA recursive_triggers = OFF").run(); + + await expect( + database + .prepare( + `INSERT OR REPLACE INTO environment_package_artifact_backup ( + app_id, attempt_count, backup_id, command_id, committed_at, + delivery_generation, expires_at, input_digest, manifest_generation, paths_json + ) VALUES ( + ?, 1, ?, ?, CAST(unixepoch('subsec') * 1000 AS INTEGER), 1, + CAST(unixepoch('subsec') * 1000 AS INTEGER) + 315359999000, ?, 1, ? + )`, + ) + .bind( + ENVIRONMENT_APP_ID, + actualB, + commandB, + ENVIRONMENT_KEY.inputDigest, + JSON.stringify(ENVIRONMENT_PATHS), + ) + .run(), + ).rejects.toThrow("backup lacks D1 authority"); + + expect( + await getEnvironmentPackageArtifactBackupManifest(database, ENVIRONMENT_KEY), + ).toMatchObject({ backupId: actualA }); + }); + + test("OR REPLACE cannot move a live runtime candidate between stages", async () => { + const database = createDatabase(); + const stageA = await createOperationStage(database); + const actualA = encodeSandboxBackupIdForStorage("650e8400-e29b-41d4-a716-446655440043"); + const actualB = encodeSandboxBackupIdForStorage("650e8400-e29b-41d4-a716-446655440044"); + const stageB = encodeSandboxBackupIdForStorage("650e8400-e29b-41d4-a716-446655440045"); + const stageC = encodeSandboxBackupIdForStorage("650e8400-e29b-41d4-a716-446655440046"); + await claimSandboxBackupStageActual(database, { + actualBackupId: actualA, + dir: DIR, + sandboxIncarnation: 1, + stagingId: stageA.id, + }); + const insertStage = (stagingId: SandboxBackupId, actualBackupId: SandboxBackupId) => + database + .prepare( + `INSERT OR REPLACE INTO sandbox_backup_staging ( + actual_backup_id, claim_owner, created_at, dir, driver_generation, + driver_instance_id, id, operation_id, sandbox_id, sandbox_incarnation, + session_run_id, ttl_seconds, updated_at, updates_subject_backup, + workspace_session_id + ) VALUES ( + ?, 'owner', CAST(unixepoch('subsec') * 1000 AS INTEGER), '/workspace/other', + NULL, NULL, ?, ?, ?, 1, NULL, 100, + CAST(unixepoch('subsec') * 1000 AS INTEGER), 0, NULL + )`, + ) + .bind(actualBackupId, stagingId, OPERATION_ID, SANDBOX_ID) + .run(); + await database.prepare("PRAGMA recursive_triggers = OFF").run(); + + await expect(insertStage(stageC, actualA)).rejects.toThrow("already owned"); + await insertStage(stageB, actualB); + await expect( + database + .prepare("UPDATE OR REPLACE sandbox_backup_staging SET actual_backup_id = ? WHERE id = ?") + .bind(actualA, stageB) + .run(), + ).rejects.toThrow("already owned"); + + expect(await getSandboxBackupStage(database, stageA.id)).toMatchObject({ + actualBackupId: actualA, + }); + expect(await getSandboxBackupStage(database, stageB)).toMatchObject({ + actualBackupId: actualB, + }); + }); + + test("a wrong-dir stage cannot authorize a manifest", async () => { + const database = createDatabase(); + await setEnvironmentArtifactCommand(database, { + attemptCount: 1, + claimOwner: "environment-owner", + deliveryGeneration: 1, + }); + const backupId = encodeSandboxBackupIdForStorage("650e8400-e29b-41d4-a716-44665544002c"); + await database + .prepare( + `INSERT INTO environment_package_artifact_backup_staging ( + actual_backup_id, app_id, attempt_count, claim_owner, command_id, created_at, + delivery_generation, dir, input_digest, paths_json, updated_at + ) VALUES ( + ?, ?, 1, 'environment-owner', ?, CAST(unixepoch('subsec') * 1000 AS INTEGER), + 1, '/workspace/wrong', ?, ?, CAST(unixepoch('subsec') * 1000 AS INTEGER) + )`, + ) + .bind( + backupId, + ENVIRONMENT_APP_ID, + ENVIRONMENT_COMMAND_ID, + ENVIRONMENT_KEY.inputDigest, + JSON.stringify(ENVIRONMENT_PATHS), + ) + .run(); + const expiresAt = Date.now() + ENVIRONMENT_PACKAGE_ARTIFACT_BACKUP_TTL_SECONDS * 1_000 - 1_000; + + await expect( + commitEnvironmentPackageArtifactBackup(database, { + actualBackupId: backupId, + attemptCount: 1, + claimOwner: "environment-owner", + commandId: ENVIRONMENT_COMMAND_ID, + deliveryGeneration: 1, + expiresAt, + key: ENVIRONMENT_KEY, + paths: ENVIRONMENT_PATHS, + }), + ).resolves.toBe(false); + await expect( + database + .prepare( + `INSERT INTO environment_package_artifact_backup ( + app_id, attempt_count, backup_id, command_id, committed_at, + delivery_generation, expires_at, input_digest, manifest_generation, paths_json + ) VALUES ( + ?, 1, ?, ?, CAST(unixepoch('subsec') * 1000 AS INTEGER), 1, ?, ?, 1, ? + )`, + ) + .bind( + ENVIRONMENT_APP_ID, + backupId, + ENVIRONMENT_COMMAND_ID, + expiresAt, + ENVIRONMENT_KEY.inputDigest, + JSON.stringify(ENVIRONMENT_PATHS), + ) + .run(), + ).rejects.toThrow("backup lacks D1 authority"); + }); + + test("UPDATE OR REPLACE cannot steal another environment key's backup", async () => { + const database = createDatabase(); + const bucket = new MemoryR2Bucket(); + const { authority } = await createEnvironmentArtifactStage(database); + const actualA = bucket.putBackup({ + createdAt: new Date(Date.now() - 60_000), + dir: ENVIRONMENT_DIR, + name: createEnvironmentPackageArtifactBackupName(authority), + platformId: "650e8400-e29b-41d4-a716-446655440027", + }); + await claimEnvironmentPackageArtifactBackupActual(database, { + actualBackupId: actualA, + authority, + commandId: ENVIRONMENT_COMMAND_ID, + dir: ENVIRONMENT_DIR, + }); + await publishEnvironmentPackageArtifactBackup(createBindings(database, bucket), { + ...authority, + backupId: actualA, + key: ENVIRONMENT_KEY, + paths: ENVIRONMENT_PATHS, + }); + + const appB = parsePlatformId("01J0000000000000000000000M"); + const commandB = parsePlatformId("01J0000000000000000000000N"); + const keyB: EnvironmentPackageArtifactKey = { appId: appB, inputDigest: "b".repeat(64) }; + const dirB = environmentPackageArtifactDir(keyB); + const pathsB: EnvironmentPackageArtifactPaths = { + executable: [`${dirB}/python/bin`], + node: [`${dirB}/npm/node_modules`], + python: [`${dirB}/python/site-packages`], + }; + const actualB = encodeSandboxBackupIdForStorage("650e8400-e29b-41d4-a716-446655440028"); + await database + .prepare( + `INSERT INTO api_command ( + attempt_count, claim_expires_at, claim_owner, completed_at, created_at, + dedupe_key, delivery_generation, id, kind, payload_json, status, updated_at + ) VALUES ( + 1, NULL, NULL, CAST(unixepoch('subsec') * 1000 AS INTEGER), ?, + ?, 1, ?, 'environment_package_artifact_build', ?, 'succeeded', ? + )`, + ) + .bind( + NOW, + `environment-artifact:${appB}:${keyB.inputDigest}`, + commandB, + JSON.stringify(keyB), + NOW, + ) + .run(); + await database + .prepare( + `INSERT INTO environment_package_artifact_backup ( + app_id, attempt_count, backup_id, command_id, committed_at, + delivery_generation, expires_at, input_digest, manifest_generation, paths_json + ) VALUES ( + ?, 1, ?, ?, CAST(unixepoch('subsec') * 1000 AS INTEGER), 1, + CAST(unixepoch('subsec') * 1000 AS INTEGER) + 315359999000, ?, 1, ? + )`, + ) + .bind(appB, actualB, commandB, keyB.inputDigest, JSON.stringify(pathsB)) + .run(); + await createEnvironmentArtifactStage(database, { + attemptCount: 2, + claimOwner: "replacement-owner", + deliveryGeneration: 2, + }); + const stageGuard = await database + .prepare( + `SELECT sql FROM sqlite_schema + WHERE type = 'trigger' + AND name = 'sandbox_backup_delete_intent_blocks_environment_stage_update'`, + ) + .first<{ sql: string }>(); + if (stageGuard === null) { + throw new Error("Environment backup stage ownership trigger is missing."); + } + database.execute("DROP TRIGGER sandbox_backup_delete_intent_blocks_environment_stage_update"); + await database + .prepare( + `UPDATE environment_package_artifact_backup_staging + SET actual_backup_id = ? WHERE command_id = ?`, + ) + .bind(actualB, ENVIRONMENT_COMMAND_ID) + .run(); + database.execute(stageGuard.sql); + await database.prepare("PRAGMA recursive_triggers = OFF").run(); + + await expect( + database + .prepare( + `UPDATE OR REPLACE environment_package_artifact_backup + SET attempt_count = 2, backup_id = ?, command_id = ?, + committed_at = CAST(unixepoch('subsec') * 1000 AS INTEGER), + delivery_generation = 2, expires_at = ?, manifest_generation = 2, + paths_json = ? + WHERE app_id = ? AND input_digest = ?`, + ) + .bind( + actualB, + ENVIRONMENT_COMMAND_ID, + bucket.backupExpiresAt(actualA) + 30_000, + JSON.stringify(ENVIRONMENT_PATHS), + ENVIRONMENT_APP_ID, + ENVIRONMENT_KEY.inputDigest, + ) + .run(), + ).rejects.toThrow("rotation lacks D1 authority"); + expect( + ( + await database + .prepare( + `SELECT app_id, backup_id, input_digest + FROM environment_package_artifact_backup ORDER BY app_id`, + ) + .all() + ).results, + ).toEqual([ + { app_id: ENVIRONMENT_APP_ID, backup_id: actualA, input_digest: ENVIRONMENT_KEY.inputDigest }, + { app_id: appB, backup_id: actualB, input_digest: keyB.inputDigest }, + ]); + }); + + test("raw rotation requires a new backup, exact next generation, and longer expiry", async () => { + const database = createDatabase(); + const bucket = new MemoryR2Bucket(); + const attemptA = await createEnvironmentArtifactStage(database); + const actualA = bucket.putBackup({ + createdAt: new Date(Date.now() - 60_000), + dir: ENVIRONMENT_DIR, + name: createEnvironmentPackageArtifactBackupName(attemptA.authority), + platformId: "650e8400-e29b-41d4-a716-44665544002d", + }); + await claimEnvironmentPackageArtifactBackupActual(database, { + actualBackupId: actualA, + authority: attemptA.authority, + commandId: ENVIRONMENT_COMMAND_ID, + dir: ENVIRONMENT_DIR, + }); + await publishEnvironmentPackageArtifactBackup(createBindings(database, bucket), { + ...attemptA.authority, + backupId: actualA, + key: ENVIRONMENT_KEY, + paths: ENVIRONMENT_PATHS, + }); + const attemptB = await createEnvironmentArtifactStage(database, { + attemptCount: 2, + claimOwner: "successor-owner", + deliveryGeneration: 2, + }); + const actualB = bucket.putBackup({ + dir: ENVIRONMENT_DIR, + name: createEnvironmentPackageArtifactBackupName(attemptB.authority), + platformId: "650e8400-e29b-41d4-a716-44665544002e", + }); + await claimEnvironmentPackageArtifactBackupActual(database, { + actualBackupId: actualB, + authority: attemptB.authority, + commandId: ENVIRONMENT_COMMAND_ID, + dir: ENVIRONMENT_DIR, + }); + + for (const [backupId, manifestGeneration, expiresAt] of [ + [actualB, 3, bucket.backupExpiresAt(actualB)], + [actualB, 2, bucket.backupExpiresAt(actualA)], + ] as const) { + await expect( + database + .prepare( + `UPDATE environment_package_artifact_backup + SET attempt_count = 2, backup_id = ?, command_id = ?, + committed_at = CAST(unixepoch('subsec') * 1000 AS INTEGER), + delivery_generation = 2, expires_at = ?, manifest_generation = ?, + paths_json = ? + WHERE app_id = ? AND input_digest = ?`, + ) + .bind( + backupId, + ENVIRONMENT_COMMAND_ID, + expiresAt, + manifestGeneration, + JSON.stringify(ENVIRONMENT_PATHS), + ENVIRONMENT_APP_ID, + ENVIRONMENT_KEY.inputDigest, + ) + .run(), + ).rejects.toThrow("rotation lacks D1 authority"); + } + database.execute("DROP TRIGGER sandbox_backup_delete_intent_blocks_environment_stage_update"); + await database + .prepare( + `UPDATE environment_package_artifact_backup_staging + SET actual_backup_id = ? WHERE command_id = ?`, + ) + .bind(actualA, ENVIRONMENT_COMMAND_ID) + .run(); + await expect( + database + .prepare( + `UPDATE environment_package_artifact_backup + SET attempt_count = 2, backup_id = ?, command_id = ?, + committed_at = CAST(unixepoch('subsec') * 1000 AS INTEGER), + delivery_generation = 2, expires_at = ?, manifest_generation = 2, + paths_json = ? + WHERE app_id = ? AND input_digest = ?`, + ) + .bind( + actualA, + ENVIRONMENT_COMMAND_ID, + bucket.backupExpiresAt(actualB), + JSON.stringify(ENVIRONMENT_PATHS), + ENVIRONMENT_APP_ID, + ENVIRONMENT_KEY.inputDigest, + ) + .run(), + ).rejects.toThrow("rotation lacks D1 authority"); + expect( + await getEnvironmentPackageArtifactBackupManifest(database, ENVIRONMENT_KEY), + ).toMatchObject({ backupId: actualA, manifestGeneration: 1 }); + }); + + test("a failed old-backup tombstone rolls back the whole manifest swap", async () => { + const database = createDatabase(); + const bucket = new MemoryR2Bucket(); + const attemptA = await createEnvironmentArtifactStage(database); + const actualA = bucket.putBackup({ + createdAt: new Date(Date.now() - 60_000), + dir: ENVIRONMENT_DIR, + name: createEnvironmentPackageArtifactBackupName(attemptA.authority), + platformId: "650e8400-e29b-41d4-a716-44665544002f", + }); + await claimEnvironmentPackageArtifactBackupActual(database, { + actualBackupId: actualA, + authority: attemptA.authority, + commandId: ENVIRONMENT_COMMAND_ID, + dir: ENVIRONMENT_DIR, + }); + await publishEnvironmentPackageArtifactBackup(createBindings(database, bucket), { + ...attemptA.authority, + backupId: actualA, + key: ENVIRONMENT_KEY, + paths: ENVIRONMENT_PATHS, + }); + const attemptB = await createEnvironmentArtifactStage(database, { + attemptCount: 2, + claimOwner: "successor-owner", + deliveryGeneration: 2, + }); + const actualB = bucket.putBackup({ + dir: ENVIRONMENT_DIR, + name: createEnvironmentPackageArtifactBackupName(attemptB.authority), + platformId: "650e8400-e29b-41d4-a716-446655440030", + }); + await claimEnvironmentPackageArtifactBackupActual(database, { + actualBackupId: actualB, + authority: attemptB.authority, + commandId: ENVIRONMENT_COMMAND_ID, + dir: ENVIRONMENT_DIR, + }); + database.execute("DROP TRIGGER sandbox_backup_delete_intent_authority"); + await database + .prepare( + `INSERT INTO sandbox_backup_delete_intent ( + attempted_at, backup_id, created_at, delete_after, deleted_at + ) VALUES ( + NULL, ?, CAST(unixepoch('subsec') * 1000 AS INTEGER), + CAST(unixepoch('subsec') * 1000 AS INTEGER), NULL + )`, + ) + .bind(actualA) + .run(); + + await expect( + publishEnvironmentPackageArtifactBackup(createBindings(database, bucket), { + ...attemptB.authority, + backupId: actualB, + key: ENVIRONMENT_KEY, + paths: ENVIRONMENT_PATHS, + }), + ).rejects.toThrow(); + expect( + await getEnvironmentPackageArtifactBackupManifest(database, ENVIRONMENT_KEY), + ).toMatchObject({ backupId: actualA, manifestGeneration: 1 }); + expect( + await database + .prepare("SELECT count(*) AS count FROM sandbox_backup_delete_intent WHERE backup_id = ?") + .bind(actualA) + .first(), + ).toEqual({ count: 1 }); + }); + + test.each([ + ["traversal", { ...ENVIRONMENT_PATHS, python: [`${ENVIRONMENT_DIR}/python/../escape`] }], + ["outside", { ...ENVIRONMENT_PATHS, python: ["/workspace/escape"] }], + ["relative", { ...ENVIRONMENT_PATHS, python: ["python/site-packages"] }], + ["root itself", { ...ENVIRONMENT_PATHS, python: [ENVIRONMENT_DIR] }], + ["prefix collision", { ...ENVIRONMENT_PATHS, python: [`${ENVIRONMENT_DIR}-evil/path`] }], + ["empty segment", { ...ENVIRONMENT_PATHS, python: [`${ENVIRONMENT_DIR}//python`] }], + ["dot segment", { ...ENVIRONMENT_PATHS, python: [`${ENVIRONMENT_DIR}/./python`] }], + ["extra key", { ...ENVIRONMENT_PATHS, extra: [`${ENVIRONMENT_DIR}/extra`] }], + ["duplicate", { ...ENVIRONMENT_PATHS, python: [ENVIRONMENT_PATHS.executable[0]] }], + ["NUL", { ...ENVIRONMENT_PATHS, python: [`${ENVIRONMENT_DIR}/python/\0escape`] }], + ["colon", { ...ENVIRONMENT_PATHS, python: [`${ENVIRONMENT_DIR}/python:escape`] }], + ])("a legacy projection rejects %s artifact paths", async (_case, paths) => { + const database = createDatabase(); + const bucket = new MemoryR2Bucket(); + const { authority } = await createEnvironmentArtifactStage(database); + bucket.putBackup({ + dir: ENVIRONMENT_DIR, + name: createEnvironmentPackageArtifactBackupName(authority), + platformId: "650e8400-e29b-41d4-a716-44665544000f", + }); + await bucket.put( + environmentPackageArtifactMetadataKey(ENVIRONMENT_KEY), + JSON.stringify({ + backupId: "650e8400-e29b-41d4-a716-44665544000f", + paths, + }), + ); + + await expect( + resolveEnvironmentPackageArtifactBackup(createBindings(database, bucket), ENVIRONMENT_KEY), + ).resolves.toBeNull(); + expect(await getEnvironmentPackageArtifactBackupManifest(database, ENVIRONMENT_KEY)).toBeNull(); + }); + + test("a legacy projection rejects extra metadata keys", async () => { + const database = createDatabase(); + const bucket = new MemoryR2Bucket(); + const { authority } = await createEnvironmentArtifactStage(database); + bucket.putBackup({ + dir: ENVIRONMENT_DIR, + name: createEnvironmentPackageArtifactBackupName(authority), + platformId: "650e8400-e29b-41d4-a716-446655440011", + }); + await bucket.put( + environmentPackageArtifactMetadataKey(ENVIRONMENT_KEY), + JSON.stringify({ + backupId: "650e8400-e29b-41d4-a716-446655440011", + extra: true, + paths: ENVIRONMENT_PATHS, + }), + ); + + await expect( + resolveEnvironmentPackageArtifactBackup(createBindings(database, bucket), ENVIRONMENT_KEY), + ).resolves.toBeNull(); + expect(await getEnvironmentPackageArtifactBackupManifest(database, ENVIRONMENT_KEY)).toBeNull(); + }); + + test("an existing D1 manifest cannot complete a successor stage", async () => { + const database = createDatabase(); + const bucket = new MemoryR2Bucket(); + const attemptA = await createEnvironmentArtifactStage(database); + const actualA = bucket.putBackup({ + dir: ENVIRONMENT_DIR, + name: createEnvironmentPackageArtifactBackupName(attemptA.authority), + platformId: "650e8400-e29b-41d4-a716-446655440002", + }); + expect( + await claimEnvironmentPackageArtifactBackupActual(database, { + actualBackupId: actualA, + authority: attemptA.authority, + commandId: ENVIRONMENT_COMMAND_ID, + dir: ENVIRONMENT_DIR, + }), + ).toEqual({ actualBackupId: actualA }); + expect( + await commitEnvironmentPackageArtifactBackup(database, { + actualBackupId: actualA, + ...attemptA.authority, + expiresAt: bucket.backupExpiresAt(actualA), + key: ENVIRONMENT_KEY, + paths: ENVIRONMENT_PATHS, + }), + ).toBe(true); + + const attemptB = await createEnvironmentArtifactStage(database, { + attemptCount: 2, + claimOwner: "successor-owner", + deliveryGeneration: 2, + }); + await publishEnvironmentPackageArtifactBackup(createBindings(database, bucket), { + attemptCount: attemptA.authority.attemptCount, + backupId: actualA, + claimOwner: attemptA.authority.claimOwner, + commandId: attemptA.authority.commandId, + deliveryGeneration: attemptA.authority.deliveryGeneration, + key: ENVIRONMENT_KEY, + paths: ENVIRONMENT_PATHS, + }); + await reconcileSandboxBackupPage(createBindings(database, bucket), { + cursor: null, + }); + + expect( + await getEnvironmentPackageArtifactBackupStage(database, ENVIRONMENT_COMMAND_ID), + ).toEqual(attemptB.stage); + expect(getSandboxBackupObjectKeys(actualA).every((key) => bucket.objects.has(key))).toBe(true); + expect(bucket.objects.has(environmentPackageArtifactMetadataKey(ENVIRONMENT_KEY))).toBe(false); + }); + + test("rotation keeps old objects until every issued reference has expired", async () => { + const database = createDatabase(); + const bucket = new MemoryR2Bucket(); + const attemptA = await createEnvironmentArtifactStage(database); + const actualA = bucket.putBackup({ + createdAt: new Date(Date.now() - 60_000), + dir: ENVIRONMENT_DIR, + name: createEnvironmentPackageArtifactBackupName(attemptA.authority), + platformId: "650e8400-e29b-41d4-a716-446655440008", + }); + await claimEnvironmentPackageArtifactBackupActual(database, { + actualBackupId: actualA, + authority: attemptA.authority, + commandId: ENVIRONMENT_COMMAND_ID, + dir: ENVIRONMENT_DIR, + }); + await publishEnvironmentPackageArtifactBackup(createBindings(database, bucket), { + ...attemptA.authority, + backupId: actualA, + key: ENVIRONMENT_KEY, + paths: ENVIRONMENT_PATHS, + }); + + const attemptB = await createEnvironmentArtifactStage(database, { + attemptCount: 2, + claimOwner: "successor-owner", + deliveryGeneration: 2, + }); + const actualB = bucket.putBackup({ + dir: ENVIRONMENT_DIR, + name: createEnvironmentPackageArtifactBackupName(attemptB.authority), + platformId: "650e8400-e29b-41d4-a716-446655440018", + }); + await claimEnvironmentPackageArtifactBackupActual(database, { + actualBackupId: actualB, + authority: attemptB.authority, + commandId: ENVIRONMENT_COMMAND_ID, + dir: ENVIRONMENT_DIR, + }); + await publishEnvironmentPackageArtifactBackup(createBindings(database, bucket), { + ...attemptB.authority, + backupId: actualB, + key: ENVIRONMENT_KEY, + paths: ENVIRONMENT_PATHS, + }); + + expect( + await getEnvironmentPackageArtifactBackupManifest(database, ENVIRONMENT_KEY), + ).toMatchObject({ backupId: actualB, manifestGeneration: 2 }); + expect( + await database + .prepare( + `SELECT attempted_at, delete_after, deleted_at + FROM sandbox_backup_delete_intent WHERE backup_id = ?`, + ) + .bind(actualA) + .first(), + ).toEqual({ + attempted_at: null, + delete_after: bucket.backupExpiresAt(actualA), + deleted_at: null, + }); + expect(await listPendingSandboxBackupDeletions(database, 64)).not.toContain(actualA); + + await reconcileSandboxBackupPage(createBindings(database, bucket), { + cursor: null, + }); + + expect(getSandboxBackupObjectKeys(actualA).every((key) => bucket.objects.has(key))).toBe(true); + expect(getSandboxBackupObjectKeys(actualB).every((key) => bucket.objects.has(key))).toBe(true); + }); + + test("an expired manifest rotates atomically and makes its old objects immediately due", async () => { + const database = createDatabase(); + const bucket = new MemoryR2Bucket(); + const attemptA = await createEnvironmentArtifactStage(database); + const actualA = bucket.putBackup({ + dir: ENVIRONMENT_DIR, + name: createEnvironmentPackageArtifactBackupName(attemptA.authority), + platformId: "650e8400-e29b-41d4-a716-446655440020", + }); + await claimEnvironmentPackageArtifactBackupActual(database, { + actualBackupId: actualA, + authority: attemptA.authority, + commandId: ENVIRONMENT_COMMAND_ID, + dir: ENVIRONMENT_DIR, + }); + await publishEnvironmentPackageArtifactBackup(createBindings(database, bucket), { + ...attemptA.authority, + backupId: actualA, + key: ENVIRONMENT_KEY, + paths: ENVIRONMENT_PATHS, + }); + + await expireEnvironmentArtifactManifest(database); + const [, oldMetadataKey] = getSandboxBackupObjectKeys(actualA); + const oldMetadata = JSON.parse(bucket.objects.get(oldMetadataKey)!.body) as Record< + string, + unknown + >; + await bucket.put(oldMetadataKey, JSON.stringify({ ...oldMetadata, name: null })); + + const attemptB = await createEnvironmentArtifactStage(database, { + attemptCount: 2, + claimOwner: "successor-owner", + deliveryGeneration: 2, + }); + const actualB = bucket.putBackup({ + dir: ENVIRONMENT_DIR, + name: createEnvironmentPackageArtifactBackupName(attemptB.authority), + platformId: "650e8400-e29b-41d4-a716-446655440021", + }); + await claimEnvironmentPackageArtifactBackupActual(database, { + actualBackupId: actualB, + authority: attemptB.authority, + commandId: ENVIRONMENT_COMMAND_ID, + dir: ENVIRONMENT_DIR, + }); + await publishEnvironmentPackageArtifactBackup(createBindings(database, bucket), { + ...attemptB.authority, + backupId: actualB, + key: ENVIRONMENT_KEY, + paths: ENVIRONMENT_PATHS, + }); + + expect( + await getEnvironmentPackageArtifactBackupManifest(database, ENVIRONMENT_KEY), + ).toMatchObject({ backupId: actualB, manifestGeneration: 2 }); + expect(await listPendingSandboxBackupDeletions(database, 64)).toContain(actualA); + + await reconcileSandboxBackupPage(createBindings(database, bucket), { cursor: null }); + expect(getSandboxBackupObjectKeys(actualA).every((key) => !bucket.objects.has(key))).toBe(true); + expect(getSandboxBackupObjectKeys(actualB).every((key) => bucket.objects.has(key))).toBe(true); + }); + + test("reconciliation retires an unused expired manifest and its R2 objects", async () => { + const database = createDatabase(); + const bucket = new MemoryR2Bucket(); + const { authority } = await createEnvironmentArtifactStage(database); + const platformId = "650e8400-e29b-41d4-a716-446655440029"; + const actualId = bucket.putBackup({ + dir: ENVIRONMENT_DIR, + name: createEnvironmentPackageArtifactBackupName(authority), + platformId, + }); + await claimEnvironmentPackageArtifactBackupActual(database, { + actualBackupId: actualId, + authority, + commandId: ENVIRONMENT_COMMAND_ID, + dir: ENVIRONMENT_DIR, + }); + await publishEnvironmentPackageArtifactBackup(createBindings(database, bucket), { + ...authority, + backupId: actualId, + key: ENVIRONMENT_KEY, + paths: ENVIRONMENT_PATHS, + }); + await reconcileSandboxBackupPage(createBindings(database, bucket), { cursor: null }); + + await expect( + database + .prepare( + `DELETE FROM environment_package_artifact_backup + WHERE app_id = ? AND input_digest = ?`, + ) + .bind(ENVIRONMENT_APP_ID, ENVIRONMENT_KEY.inputDigest) + .run(), + ).rejects.toThrow("manifest has not expired"); + await expireEnvironmentArtifactManifest(database); + + await reconcileSandboxBackupPage(createBindings(database, bucket), { cursor: null }); + + expect(await getEnvironmentPackageArtifactBackupManifest(database, ENVIRONMENT_KEY)).toBeNull(); + expect(getSandboxBackupObjectKeys(actualId).every((key) => !bucket.objects.has(key))).toBe( + true, + ); + expect( + await database + .prepare( + `SELECT attempted_at, deleted_at + FROM sandbox_backup_delete_intent WHERE backup_id = ?`, + ) + .bind(actualId) + .first(), + ).toEqual({ attempted_at: expect.any(Number), deleted_at: expect.any(Number) }); + + bucket.putBackup({ + dir: ENVIRONMENT_DIR, + name: createEnvironmentPackageArtifactBackupName(authority), + platformId, + }); + await bucket.put( + environmentPackageArtifactMetadataKey(ENVIRONMENT_KEY), + JSON.stringify({ backupId: platformId, paths: ENVIRONMENT_PATHS }), + ); + await succeedEnvironmentArtifactCommand(database); + await expect( + resolveEnvironmentPackageArtifactBackup(createBindings(database, bucket), ENVIRONMENT_KEY), + ).resolves.toBeNull(); + expect(await getEnvironmentPackageArtifactBackupManifest(database, ENVIRONMENT_KEY)).toBeNull(); + + const successor = await createEnvironmentArtifactStage(database, { + attemptCount: 2, + claimOwner: "successor-owner", + deliveryGeneration: 2, + }); + const successorId = bucket.putBackup({ + dir: ENVIRONMENT_DIR, + name: createEnvironmentPackageArtifactBackupName(successor.authority), + platformId: "650e8400-e29b-41d4-a716-44665544002a", + }); + await claimEnvironmentPackageArtifactBackupActual(database, { + actualBackupId: successorId, + authority: successor.authority, + commandId: ENVIRONMENT_COMMAND_ID, + dir: ENVIRONMENT_DIR, + }); + await publishEnvironmentPackageArtifactBackup(createBindings(database, bucket), { + ...successor.authority, + backupId: successorId, + key: ENVIRONMENT_KEY, + paths: ENVIRONMENT_PATHS, + }); + expect( + await getEnvironmentPackageArtifactBackupManifest(database, ENVIRONMENT_KEY), + ).toMatchObject({ backupId: successorId, manifestGeneration: 1 }); + }); + + test("a rotation retries as generation one when retirement wins its CAS", async () => { + const database = createDatabase(); + const bucket = new MemoryR2Bucket(); + const attemptA = await createEnvironmentArtifactStage(database); + const actualA = bucket.putBackup({ + dir: ENVIRONMENT_DIR, + name: createEnvironmentPackageArtifactBackupName(attemptA.authority), + platformId: "650e8400-e29b-41d4-a716-446655440031", + }); + await claimEnvironmentPackageArtifactBackupActual(database, { + actualBackupId: actualA, + authority: attemptA.authority, + commandId: ENVIRONMENT_COMMAND_ID, + dir: ENVIRONMENT_DIR, + }); + await publishEnvironmentPackageArtifactBackup(createBindings(database, bucket), { + ...attemptA.authority, + backupId: actualA, + key: ENVIRONMENT_KEY, + paths: ENVIRONMENT_PATHS, + }); + await reconcileSandboxBackupPage(createBindings(database, bucket), { cursor: null }); + await expireEnvironmentArtifactManifest(database); + + const attemptB = await createEnvironmentArtifactStage(database, { + attemptCount: 2, + claimOwner: "successor-owner", + deliveryGeneration: 2, + }); + const actualB = bucket.putBackup({ + dir: ENVIRONMENT_DIR, + name: createEnvironmentPackageArtifactBackupName(attemptB.authority), + platformId: "650e8400-e29b-41d4-a716-446655440032", + }); + await claimEnvironmentPackageArtifactBackupActual(database, { + actualBackupId: actualB, + authority: attemptB.authority, + commandId: ENVIRONMENT_COMMAND_ID, + dir: ENVIRONMENT_DIR, + }); + let retired = false; + const intercept = (statement: D1PreparedStatement): D1PreparedStatement => + new Proxy(statement, { + get(target, property, receiver) { + if (property === "bind") { + return (...values: unknown[]) => intercept(Reflect.apply(target.bind, target, values)); + } + if (property === "run") { + return async (...args: unknown[]) => { + if (!retired) { + retired = true; + await retireExpiredEnvironmentPackageArtifactBackups(database, 64); + } + return Reflect.apply(target.run, target, args); + }; + } + return Reflect.get(target, property, receiver); + }, + }); + const racingDatabase = new Proxy(database, { + get(target, property) { + if (property === "prepare") { + return (query: string) => { + const statement = target.prepare(query); + return query.includes("UPDATE environment_package_artifact_backup\n SET") + ? intercept(statement) + : statement; + }; + } + const value = Reflect.get(target, property, target) as unknown; + return typeof value === "function" ? value.bind(target) : value; + }, + }); + + await publishEnvironmentPackageArtifactBackup(createBindings(racingDatabase, bucket), { + ...attemptB.authority, + backupId: actualB, + key: ENVIRONMENT_KEY, + paths: ENVIRONMENT_PATHS, + }); + + expect(retired).toBe(true); + expect( + await getEnvironmentPackageArtifactBackupManifest(database, ENVIRONMENT_KEY), + ).toMatchObject({ backupId: actualB, manifestGeneration: 1 }); + expect(await listPendingSandboxBackupDeletions(database, 64)).toContain(actualA); + }); + + test("an initial manifest survives a lost D1 acknowledgement", async () => { + const database = createDatabase(); + const bucket = new MemoryR2Bucket(); + const { authority } = await createEnvironmentArtifactStage(database); + const actualId = bucket.putBackup({ + dir: ENVIRONMENT_DIR, + name: createEnvironmentPackageArtifactBackupName(authority), + platformId: "650e8400-e29b-41d4-a716-446655440043", + }); + await claimEnvironmentPackageArtifactBackupActual(database, { + actualBackupId: actualId, + authority, + commandId: ENVIRONMENT_COMMAND_ID, + dir: ENVIRONMENT_DIR, + }); + const fault = loseRunAcknowledgementOnce( + database, + "INSERT INTO environment_package_artifact_backup", + ); + + await expect( + publishEnvironmentPackageArtifactBackup(createBindings(fault.database, bucket), { + ...authority, + backupId: actualId, + key: ENVIRONMENT_KEY, + paths: ENVIRONMENT_PATHS, + }), + ).rejects.toThrow("Simulated D1 write acknowledgement loss"); + expect(fault.wasLost()).toBe(true); + expect( + await getEnvironmentPackageArtifactBackupManifest(database, ENVIRONMENT_KEY), + ).toMatchObject({ backupId: actualId, manifestGeneration: 1 }); + + await expect( + publishEnvironmentPackageArtifactBackup(createBindings(database, bucket), { + ...authority, + backupId: actualId, + key: ENVIRONMENT_KEY, + paths: ENVIRONMENT_PATHS, + }), + ).resolves.toBeUndefined(); + }); + + test("a committed manifest swap survives a lost D1 acknowledgement", async () => { + const database = createDatabase(); + const bucket = new MemoryR2Bucket(); + const attemptA = await createEnvironmentArtifactStage(database); + const actualA = bucket.putBackup({ + createdAt: new Date(Date.now() - 60_000), + dir: ENVIRONMENT_DIR, + name: createEnvironmentPackageArtifactBackupName(attemptA.authority), + platformId: "650e8400-e29b-41d4-a716-446655440022", + }); + await claimEnvironmentPackageArtifactBackupActual(database, { + actualBackupId: actualA, + authority: attemptA.authority, + commandId: ENVIRONMENT_COMMAND_ID, + dir: ENVIRONMENT_DIR, + }); + await publishEnvironmentPackageArtifactBackup(createBindings(database, bucket), { + ...attemptA.authority, + backupId: actualA, + key: ENVIRONMENT_KEY, + paths: ENVIRONMENT_PATHS, + }); + + const attemptB = await createEnvironmentArtifactStage(database, { + attemptCount: 2, + claimOwner: "successor-owner", + deliveryGeneration: 2, + }); + const actualB = bucket.putBackup({ + dir: ENVIRONMENT_DIR, + name: createEnvironmentPackageArtifactBackupName(attemptB.authority), + platformId: "650e8400-e29b-41d4-a716-446655440023", + }); + await claimEnvironmentPackageArtifactBackupActual(database, { + actualBackupId: actualB, + authority: attemptB.authority, + commandId: ENVIRONMENT_COMMAND_ID, + dir: ENVIRONMENT_DIR, + }); + const fault = loseRunAcknowledgementOnce( + database, + "UPDATE environment_package_artifact_backup\n SET", + ); + + await expect( + publishEnvironmentPackageArtifactBackup(createBindings(fault.database, bucket), { + ...attemptB.authority, + backupId: actualB, + key: ENVIRONMENT_KEY, + paths: ENVIRONMENT_PATHS, + }), + ).rejects.toThrow("Simulated D1 write acknowledgement loss"); + + expect(fault.wasLost()).toBe(true); + expect( + await getEnvironmentPackageArtifactBackupManifest(database, ENVIRONMENT_KEY), + ).toMatchObject({ backupId: actualB, manifestGeneration: 2 }); + expect( + await database + .prepare("SELECT count(*) AS count FROM sandbox_backup_delete_intent WHERE backup_id = ?") + .bind(actualA) + .first(), + ).toEqual({ count: 1 }); + await expect( + publishEnvironmentPackageArtifactBackup(createBindings(database, bucket), { + ...attemptB.authority, + backupId: actualB, + key: ENVIRONMENT_KEY, + paths: ENVIRONMENT_PATHS, + }), + ).resolves.toBeUndefined(); + expect( + await getEnvironmentPackageArtifactBackupManifest(database, ENVIRONMENT_KEY), + ).toMatchObject({ backupId: actualB, manifestGeneration: 2 }); + }); + + test("a stale rotation CAS loses without disturbing its winner", async () => { + const database = createDatabase(); + const bucket = new MemoryR2Bucket(); + const attemptA = await createEnvironmentArtifactStage(database); + const actualA = bucket.putBackup({ + createdAt: new Date(Date.now() - 120_000), + dir: ENVIRONMENT_DIR, + name: createEnvironmentPackageArtifactBackupName(attemptA.authority), + platformId: "650e8400-e29b-41d4-a716-446655440024", + }); + await claimEnvironmentPackageArtifactBackupActual(database, { + actualBackupId: actualA, + authority: attemptA.authority, + commandId: ENVIRONMENT_COMMAND_ID, + dir: ENVIRONMENT_DIR, + }); + await publishEnvironmentPackageArtifactBackup(createBindings(database, bucket), { + ...attemptA.authority, + backupId: actualA, + key: ENVIRONMENT_KEY, + paths: ENVIRONMENT_PATHS, + }); + + const attemptB = await createEnvironmentArtifactStage(database, { + attemptCount: 2, + claimOwner: "second-owner", + deliveryGeneration: 2, + }); + const actualB = bucket.putBackup({ + createdAt: new Date(Date.now() - 60_000), + dir: ENVIRONMENT_DIR, + name: createEnvironmentPackageArtifactBackupName(attemptB.authority), + platformId: "650e8400-e29b-41d4-a716-446655440025", + }); + await claimEnvironmentPackageArtifactBackupActual(database, { + actualBackupId: actualB, + authority: attemptB.authority, + commandId: ENVIRONMENT_COMMAND_ID, + dir: ENVIRONMENT_DIR, + }); + + let actualC: SandboxBackupId | null = null; + let intercepted = false; + const intercept = (statement: D1PreparedStatement): D1PreparedStatement => + new Proxy(statement, { + get(target, property, receiver) { + if (property === "bind") { + return (...values: unknown[]) => intercept(Reflect.apply(target.bind, target, values)); + } + if (property === "run") { + return async (...args: unknown[]) => { + if (!intercepted) { + intercepted = true; + const attemptC = await createEnvironmentArtifactStage(database, { + attemptCount: 3, + claimOwner: "third-owner", + deliveryGeneration: 3, + }); + actualC = bucket.putBackup({ + dir: ENVIRONMENT_DIR, + name: createEnvironmentPackageArtifactBackupName(attemptC.authority), + platformId: "650e8400-e29b-41d4-a716-446655440026", + }); + await claimEnvironmentPackageArtifactBackupActual(database, { + actualBackupId: actualC, + authority: attemptC.authority, + commandId: ENVIRONMENT_COMMAND_ID, + dir: ENVIRONMENT_DIR, + }); + await publishEnvironmentPackageArtifactBackup(createBindings(database, bucket), { + ...attemptC.authority, + backupId: actualC, + key: ENVIRONMENT_KEY, + paths: ENVIRONMENT_PATHS, + }); + } + return Reflect.apply(target.run, target, args); + }; + } + return Reflect.get(target, property, receiver); + }, + }); + const racingDatabase = new Proxy(database, { + get(target, property) { + if (property === "prepare") { + return (query: string) => { + const statement = target.prepare(query); + return query.includes("UPDATE environment_package_artifact_backup\n SET") + ? intercept(statement) + : statement; + }; + } + const value = Reflect.get(target, property, target) as unknown; + return typeof value === "function" ? value.bind(target) : value; + }, + }); + + await publishEnvironmentPackageArtifactBackup(createBindings(racingDatabase, bucket), { + ...attemptB.authority, + backupId: actualB, + key: ENVIRONMENT_KEY, + paths: ENVIRONMENT_PATHS, + }); + + expect(intercepted).toBe(true); + expect(actualC).not.toBeNull(); + expect( + await getEnvironmentPackageArtifactBackupManifest(database, ENVIRONMENT_KEY), + ).toMatchObject({ backupId: actualC, manifestGeneration: 2 }); + expect(await listPendingSandboxBackupDeletions(database, 64)).toContain(actualB); + expect(getSandboxBackupObjectKeys(actualB).every((key) => bucket.objects.has(key))).toBe(true); + + bucket.failAfterDeletingKey = getSandboxBackupObjectKeys(actualB)[0]; + await reconcileSandboxBackupPage(createBindings(database, bucket), { cursor: null }); + await reconcileSandboxBackupPage(createBindings(database, bucket), { cursor: null }); + expect(getSandboxBackupObjectKeys(actualB).every((key) => !bucket.objects.has(key))).toBe(true); + expect(getSandboxBackupObjectKeys(actualA).every((key) => bucket.objects.has(key))).toBe(true); + expect( + actualC !== null && + getSandboxBackupObjectKeys(actualC).every((key) => bucket.objects.has(key)), + ).toBe(true); + }); + + test("scanner cannot claim an old attempt candidate into a rotated stage", async () => { + const database = createDatabase(); + const attemptA = await createEnvironmentArtifactStage(database); + const candidateA = encodeSandboxBackupIdForStorage("650e8400-e29b-41d4-a716-446655440003"); + + const attemptB = await createEnvironmentArtifactStage(database, { + attemptCount: 2, + claimOwner: "successor-owner", + deliveryGeneration: 2, + }); + expect( + await claimEnvironmentPackageArtifactBackupActual(database, { + actualBackupId: candidateA, + authority: attemptA.authority, + commandId: ENVIRONMENT_COMMAND_ID, + dir: ENVIRONMENT_DIR, + }), + ).toBeNull(); + expect( + await getEnvironmentPackageArtifactBackupStage(database, ENVIRONMENT_COMMAND_ID), + ).toEqual(attemptB.stage); + }); + + test("old attempt objects are collected after grace without disturbing the current stage", async () => { + const database = createDatabase(); + const bucket = new MemoryR2Bucket(); + const attemptA = await createEnvironmentArtifactStage(database); + const actualA = bucket.putBackup({ + dir: ENVIRONMENT_DIR, + name: createEnvironmentPackageArtifactBackupName(attemptA.authority), + platformId: "650e8400-e29b-41d4-a716-446655440004", + }); + const attemptB = await createEnvironmentArtifactStage(database, { + attemptCount: 2, + claimOwner: "successor-owner", + deliveryGeneration: 2, + }); + + await reconcileSandboxBackupPage(createBindings(database, bucket), { + cursor: null, + }); + + expect(getSandboxBackupObjectKeys(actualA).every((key) => !bucket.objects.has(key))).toBe(true); + expect( + await getEnvironmentPackageArtifactBackupStage(database, ENVIRONMENT_COMMAND_ID), + ).toEqual(attemptB.stage); + }); + + test("partial objects use bounded grace even while another artifact writer is active", async () => { + const database = createDatabase(); + const bucket = new MemoryR2Bucket(); + const { authority } = await createEnvironmentArtifactStage(database); + const metaOnly = bucket.putBackup({ + dir: ENVIRONMENT_DIR, + name: createEnvironmentPackageArtifactBackupName(authority), + platformId: "650e8400-e29b-41d4-a716-446655440005", + }); + expect( + await claimEnvironmentPackageArtifactBackupActual(database, { + actualBackupId: metaOnly, + authority, + commandId: ENVIRONMENT_COMMAND_ID, + dir: ENVIRONMENT_DIR, + }), + ).toEqual({ actualBackupId: metaOnly }); + const [metaOnlyDataKey, metaOnlyMetadataKey] = getSandboxBackupObjectKeys(metaOnly); + await bucket.delete(metaOnlyDataKey); + + const oldDataOnly = encodeSandboxBackupIdForStorage("650e8400-e29b-41d4-a716-446655440006"); + const freshDataOnly = encodeSandboxBackupIdForStorage("650e8400-e29b-41d4-a716-446655440007"); + const [oldDataKey] = getSandboxBackupObjectKeys(oldDataOnly); + const [freshDataKey] = getSandboxBackupObjectKeys(freshDataOnly); + await bucket.put(oldDataKey, "old-partial"); + await bucket.put(freshDataKey, "fresh-partial", new Date(NOW)); + + await reconcileSandboxBackupPage(createBindings(database, bucket), { + cursor: null, + }); + + expect(bucket.objects.has(metaOnlyMetadataKey)).toBe(false); + expect(bucket.objects.has(oldDataKey)).toBe(false); + expect(bucket.objects.has(freshDataKey)).toBe(true); + expect( + await getEnvironmentPackageArtifactBackupStage(database, ENVIRONMENT_COMMAND_ID), + ).toMatchObject({ actualBackupId: null }); + }); + + test("opaque pagination converges many complete candidates on one D1 manifest", async () => { + const database = createDatabase(); + const bucket = new MemoryR2Bucket(); + const { authority } = await createEnvironmentArtifactStage(database); + const candidates: SandboxBackupId[] = []; + for (let index = 0; index < 65; index += 1) { + candidates.push( + bucket.putBackup({ + dir: ENVIRONMENT_DIR, + name: createEnvironmentPackageArtifactBackupName(authority), + platformId: `650e8400-e29b-4000-8000-${index.toString(16).padStart(12, "0")}`, + }), + ); + } + + let cursor: string | null = null; + let pageCount = 0; + do { + const page = await reconcileSandboxBackupPage(createBindings(database, bucket), { + cursor, + }); + cursor = page.nextCursor; + pageCount += 1; + if (!page.hasMore) { + break; + } + } while (pageCount < 10); + + const winner = (await getEnvironmentPackageArtifactBackupManifest(database, ENVIRONMENT_KEY)) + ?.backupId; + if (winner === undefined) { + throw new Error("Environment artifact candidates produced no D1 winner."); + } + expect(candidates).toContain(winner); + expect(pageCount).toBeGreaterThan(1); + expect( + [...bucket.objects.keys()].filter((key) => key.startsWith("backups/")).toSorted(), + ).toEqual([...getSandboxBackupObjectKeys(winner)].toSorted()); + }); + + test("an empty R2 page continues until every expired D1 manifest is retired", async () => { + const database = createDatabase(); + const bucket = new MemoryR2Bucket(); + await createEnvironmentArtifactStage(database); + database.execute("DROP TRIGGER environment_package_artifact_backup_authority"); + const committedAt = Date.now() - 2 * 86_400_000; + const expiresAt = committedAt + 86_400_001; + for (let index = 0; index < 65; index += 1) { + const inputDigest = index.toString(16).padStart(64, "0"); + const dir = `/workspace/.mosoo/environment-artifacts/${inputDigest}`; + const backupId = encodeSandboxBackupIdForStorage( + `650e8400-e29b-4000-8001-${index.toString(16).padStart(12, "0")}`, + ); + await database + .prepare( + `INSERT INTO environment_package_artifact_backup ( + app_id, attempt_count, backup_id, command_id, committed_at, + delivery_generation, expires_at, input_digest, manifest_generation, paths_json + ) VALUES (?, 1, ?, ?, ?, 1, ?, ?, 1, ?)`, + ) + .bind( + ENVIRONMENT_APP_ID, + backupId, + ENVIRONMENT_COMMAND_ID, + committedAt, + expiresAt, + inputDigest, + JSON.stringify({ executable: [`${dir}/bin/tool`], node: [], python: [] }), + ) + .run(); + } + + const first = await reconcileSandboxBackupPage(createBindings(database, bucket), { + cursor: null, + }); + expect(first).toEqual({ hasMore: true, nextCursor: null, processed: 0 }); + expect( + await database + .prepare("SELECT count(*) AS count FROM environment_package_artifact_backup") + .first(), + ).toEqual({ count: 1 }); + + const second = await reconcileSandboxBackupPage(createBindings(database, bucket), { + cursor: null, + }); + expect(second).toEqual({ hasMore: false, nextCursor: null, processed: 0 }); + expect( + await database + .prepare("SELECT count(*) AS count FROM environment_package_artifact_backup") + .first(), + ).toEqual({ count: 0 }); + }); + + test("hidden rowid cannot replace permanent backup authority", async () => { + const database = createDatabase(); + const stage = await createOperationStage(database); + const readyId = encodeSandboxBackupIdForStorage("550e8400-e29b-41d4-a716-446655440030"); + const replacementId = encodeSandboxBackupIdForStorage("550e8400-e29b-41d4-a716-446655440031"); + const replacementStageId = encodeSandboxBackupIdForStorage( + "550e8400-e29b-41d4-a716-446655440032", + ); + const intentId = encodeSandboxBackupIdForStorage("550e8400-e29b-41d4-a716-446655440033"); + const replacementIntentId = encodeSandboxBackupIdForStorage( + "550e8400-e29b-41d4-a716-446655440034", + ); + + await claimSandboxBackupStageActual(database, { + actualBackupId: readyId, + dir: DIR, + sandboxIncarnation: 1, + stagingId: stage.id, + }); + await finalizeSandboxBackupStage(database, { + actualBackupId: readyId, + stagingId: stage.id, + }); + expect( + await authorizeSandboxBackupDeletion(database, { + authority: { kind: "unattributed" }, + backupId: intentId, + }), + ).toBe(true); + + await database.prepare("PRAGMA recursive_triggers = OFF").run(); + + expect( + ( + await database + .prepare( + `SELECT name, wr FROM pragma_table_list + WHERE name IN ( + 'environment_package_artifact_backup', + 'environment_package_artifact_backup_staging', + 'sandbox_backup', + 'sandbox_backup_delete_intent', + 'sandbox_backup_staging' + ) + ORDER BY name`, + ) + .all() + ).results, + ).toEqual([ + { name: "environment_package_artifact_backup", wr: 1 }, + { name: "environment_package_artifact_backup_staging", wr: 1 }, + { name: "sandbox_backup", wr: 1 }, + { name: "sandbox_backup_delete_intent", wr: 1 }, + { name: "sandbox_backup_staging", wr: 1 }, + ]); + + await expect( + database + .prepare( + `INSERT OR REPLACE INTO sandbox_backup ( + rowid, created_at, dir, id, keep, operation_id, sandbox_id, + sandbox_incarnation, session_run_id, staging_id, status, + ttl_seconds, updated_at, workspace_session_id + ) VALUES ( + 1, ?, '/workspace/rowid-replacement', ?, 0, ?, ?, 1, + NULL, ?, 'ready', 100, ?, NULL + )`, + ) + .bind(NOW, replacementId, OPERATION_ID, SANDBOX_ID, replacementStageId, NOW) + .run(), + ).rejects.toThrow(/rowid/i); + + await expect( + database + .prepare( + `INSERT OR REPLACE INTO sandbox_backup_delete_intent ( + rowid, attempted_at, backup_id, created_at, delete_after, deleted_at + ) VALUES ( + 1, NULL, ?, CAST(unixepoch('subsec') * 1000 AS INTEGER), + CAST(unixepoch('subsec') * 1000 AS INTEGER), NULL + )`, + ) + .bind(replacementIntentId) + .run(), + ).rejects.toThrow(/rowid/i); + + expect(await readyRows(database)).toEqual([{ id: readyId, staging_id: stage.id }]); + expect( + ( + await database + .prepare("SELECT backup_id FROM sandbox_backup_delete_intent ORDER BY backup_id") + .all() + ).results, + ).toEqual([{ backup_id: intentId }]); + }); +}); diff --git a/apps/api/tests/sandbox-backup-pruning.test.ts b/apps/api/tests/sandbox-backup-pruning.test.ts index 56016445..f8da52f8 100644 --- a/apps/api/tests/sandbox-backup-pruning.test.ts +++ b/apps/api/tests/sandbox-backup-pruning.test.ts @@ -1,216 +1,183 @@ import { describe, expect, test } from "bun:test"; +import { createPlatformId } from "@mosoo/id"; +import type { RuntimeOperationId, SandboxBackupId } from "@mosoo/id"; + import { selectSandboxBackupPruneIds } from "../src/modules/runtime/infrastructure/sandbox-backup-pruning"; import { listReadySandboxBackupsForPruning, markSandboxBackupsPruned, - recordCreatedSandboxBackups, } from "../src/modules/runtime/infrastructure/sandbox-backup-store"; +import { applyDrizzleMigrationsThrough } from "./helpers/drizzle-migrations"; import { SqliteD1Database } from "./helpers/sqlite-d1"; -const BACKUP_ID_1 = "01J000000000000000000000H1"; -const BACKUP_ID_2 = "01J000000000000000000000H2"; -const BACKUP_ID_3 = "01J000000000000000000000H3"; -const BACKUP_ID_4 = "01J000000000000000000000H4"; -const BACKUP_ID_5 = "01J000000000000000000000H5"; -const KEEP_BACKUP_ID = "01J000000000000000000000HZ"; -const MEMORY_NEW_BACKUP_ID = "01J000000000000000000000H6"; -const MEMORY_OLD_BACKUP_ID = "01J000000000000000000000H7"; -const SESSION_BACKUP_ID = "01J000000000000000000000H8"; - -function createSandboxBackupDatabase(input: { maxBoundParams?: number } = {}): SqliteD1Database { - const database = new SqliteD1Database(input); - - database.execute(` - CREATE TABLE sandbox ( - id text PRIMARY KEY NOT NULL, - last_backup_id text, - status text NOT NULL, - status_changed_at integer DEFAULT 0 NOT NULL, - status_event text DEFAULT 'runtime_subject.cold' NOT NULL, - status_operation_id text, - status_seq integer DEFAULT 0 NOT NULL, - status_source text DEFAULT 'system' NOT NULL, - updated_at integer NOT NULL - ); - - CREATE TABLE sandbox_backup ( - created_at integer NOT NULL, - dir text NOT NULL, - error_message text, - id text PRIMARY KEY NOT NULL, - keep integer DEFAULT 0 NOT NULL, - sandbox_id text NOT NULL, - session_run_id text, - status text NOT NULL, - ttl_seconds integer NOT NULL, - updated_at integer NOT NULL - ); - `); +const SANDBOX_ID = "01J0000000000000000000000D"; +const BACKUP_IDS = [..."12345Z"].map((suffix) => `01J000000000000000000000H${suffix}`); +function createDatabase(): SqliteD1Database { + const database = new SqliteD1Database(); + applyDrizzleMigrationsThrough(database, "0020_sandbox-backup-object-authority"); return database; } -async function insertSandbox(database: D1Database): Promise { +async function insertActiveSandbox(database: D1Database): Promise { + await database + .prepare( + `INSERT INTO sandbox ( + agent_id, app_id, created_at, id, incarnation, kind, network_constraints_hash, + owner_account_id, status, subject_id, subject_kind, updated_at + ) VALUES (?, ?, 1, ?, 1, 'pet', ?, ?, 'active', ?, 'agent', 1)`, + ) + .bind( + "01J0000000000000000000000E", + "01J0000000000000000000000F", + SANDBOX_ID, + "0".repeat(64), + "01J0000000000000000000000G", + "01J0000000000000000000000E", + ) + .run(); +} + +async function insertWorkspaceSession(database: D1Database, sessionId: string): Promise { await database - .prepare("INSERT INTO sandbox (id, last_backup_id, status, updated_at) VALUES (?, ?, ?, ?)") - .bind("01J0000000000000000000000D", null, "backing_up", 1) + .prepare( + `INSERT INTO session ( + agent_id, app_id, created_at, creator_account_id, id, kind, model, + provider, renamed, runtime_id, status, updated_at + ) VALUES (?, ?, 1, ?, ?, 'pet', 'gpt-5.4', 'openai', 0, 'openai-runtime', 'IDLE', 1)`, + ) + .bind( + "01J0000000000000000000000E", + "01J0000000000000000000000F", + "01J0000000000000000000000G", + sessionId, + ) .run(); } async function insertBackup( database: D1Database, - input: { - createdAt: number; - id: string; - keep?: boolean; - }, + input: { createdAt: number; id: string; keep?: boolean; workspaceSessionId?: string }, ): Promise { await database .prepare( - ` - INSERT INTO sandbox_backup ( - created_at, - dir, - error_message, - id, - keep, - sandbox_id, - status, - ttl_seconds, - updated_at - ) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) - `, + `INSERT INTO sandbox_backup ( + created_at, dir, id, keep, operation_id, sandbox_id, sandbox_incarnation, + session_run_id, staging_id, status, ttl_seconds, updated_at, workspace_session_id + ) VALUES (?, '/workspace', ?, ?, ?, ?, 1, NULL, ?, 'ready', 100, ?, ?)`, ) .bind( input.createdAt, - "/workspace", - null, input.id, input.keep === true ? 1 : 0, - "01J0000000000000000000000D", - "ready", - 100, + createPlatformId(), + SANDBOX_ID, + createPlatformId(), input.createdAt, + input.workspaceSessionId ?? null, ) .run(); } describe("sandbox backup pruning", () => { - test("selects pruned backup ids and marks records", async () => { - const database = createSandboxBackupDatabase(); - - await insertBackup(database, { createdAt: 1, id: BACKUP_ID_1 }); - await insertBackup(database, { createdAt: 2, id: BACKUP_ID_2 }); - await insertBackup(database, { createdAt: 3, id: BACKUP_ID_3 }); - await insertBackup(database, { createdAt: 4, id: BACKUP_ID_4 }); - await insertBackup(database, { createdAt: 5, id: BACKUP_ID_5 }); - await insertBackup(database, { createdAt: 0, id: KEEP_BACKUP_ID, keep: true }); - - const backups = await listReadySandboxBackupsForPruning(database, "01J0000000000000000000000D"); - const pruneIds = selectSandboxBackupPruneIds(backups); - - await markSandboxBackupsPruned(database, pruneIds); - - expect(pruneIds).toEqual([BACKUP_ID_2, BACKUP_ID_1]); + test("marks only stable overflow after D1 protection is rechecked", async () => { + const database = createDatabase(); + await insertActiveSandbox(database); + for (const [index, id] of BACKUP_IDS.entries()) { + await insertBackup(database, { + createdAt: index + 1, + id, + keep: index === BACKUP_IDS.length - 1, + }); + } + await database + .prepare("UPDATE sandbox SET last_backup_id = ? WHERE id = ?") + .bind(BACKUP_IDS[0], SANDBOX_ID) + .run(); + + const backups = await listReadySandboxBackupsForPruning(database, SANDBOX_ID); + const candidates = selectSandboxBackupPruneIds(backups); + const pruned = await markSandboxBackupsPruned(database, candidates); + + expect(candidates).toEqual([BACKUP_IDS[1]]); + expect(pruned).toEqual([BACKUP_IDS[1]]); const rows = await database - .prepare("SELECT id, status FROM sandbox_backup ORDER BY id") + .prepare("SELECT id, status FROM sandbox_backup ORDER BY created_at") .all<{ id: string; status: string }>(); - expect(rows.results).toEqual([ - { id: BACKUP_ID_1, status: "pruned" }, - { id: BACKUP_ID_2, status: "pruned" }, - { id: BACKUP_ID_3, status: "ready" }, - { id: BACKUP_ID_4, status: "ready" }, - { id: BACKUP_ID_5, status: "ready" }, - { id: KEEP_BACKUP_ID, status: "ready" }, - ]); - }); - - test("records checkpoint backup batch and stores latest subject checkpoint", async () => { - const database = createSandboxBackupDatabase(); - await insertSandbox(database); - - await recordCreatedSandboxBackups(database, { - backups: [ - { - backup: { dir: "/workspace/one", id: SESSION_BACKUP_ID }, - updateSandboxLastBackup: false, - }, - { - backup: { dir: "/memory", id: MEMORY_OLD_BACKUP_ID }, - updateSandboxLastBackup: true, - }, - { - backup: { dir: "/memory", id: MEMORY_NEW_BACKUP_ID }, - updateSandboxLastBackup: true, - }, - ], - sandboxId: "01J0000000000000000000000D", - ttlSeconds: 100, - }); - - const backupRows = await database - .prepare("SELECT id, dir, status, ttl_seconds FROM sandbox_backup ORDER BY id") - .all<{ dir: string; id: string; status: string; ttl_seconds: number }>(); - expect(backupRows.results).toEqual([ - { - dir: "/memory", - id: MEMORY_NEW_BACKUP_ID, - status: "ready", - ttl_seconds: 100, - }, - { - dir: "/memory", - id: MEMORY_OLD_BACKUP_ID, - status: "ready", - ttl_seconds: 100, - }, - { - dir: "/workspace/one", - id: SESSION_BACKUP_ID, - status: "ready", - ttl_seconds: 100, - }, + expect(rows.results.map((row) => row.status)).toEqual([ + "ready", + "pruned", + "ready", + "ready", + "ready", + "ready", ]); - - const sandbox = await database - .prepare("SELECT last_backup_id, status, status_seq FROM sandbox WHERE id = ?") - .bind("01J0000000000000000000000D") - .first<{ last_backup_id: string; status: string; status_seq: number }>(); - expect(sandbox).toEqual({ - last_backup_id: MEMORY_NEW_BACKUP_ID, - status: "backing_up", - status_seq: 0, - }); }); - test("records more backups than fit in one D1 statement", async () => { - const database = createSandboxBackupDatabase({ maxBoundParams: 100 }); - await insertSandbox(database); - const suffixes = [..."123456789ABCDEFG"]; - - await recordCreatedSandboxBackups(database, { - backups: suffixes.map((suffix, index) => ({ - backup: { - dir: index === suffixes.length - 1 ? "/memory" : `/workspace/${index}`, - id: `01J000000000000000000000H${suffix}`, - }, - updateSandboxLastBackup: index === suffixes.length - 1, - })), - sandboxId: "01J0000000000000000000000D", - ttlSeconds: 100, + for (const protection of ["last_backup", "restoring", "provisioning"] as const) { + test(`rechecks ${protection} protection after pruning candidates were listed`, async () => { + const database = createDatabase(); + const sessionId = "01J0000000000000000000000S"; + await insertActiveSandbox(database); + if (protection === "provisioning") { + await insertWorkspaceSession(database, sessionId); + } + for (const [index, id] of BACKUP_IDS.slice(0, 5).entries()) { + await insertBackup(database, { + createdAt: index + 1, + id, + workspaceSessionId: protection === "provisioning" && index === 1 ? sessionId : undefined, + }); + } + + const listed = await listReadySandboxBackupsForPruning(database, SANDBOX_ID); + const candidates = selectSandboxBackupPruneIds(listed); + expect(candidates).toEqual([BACKUP_IDS[1], BACKUP_IDS[0]]); + const protectedId = candidates[0]; + switch (protection) { + case "last_backup": { + await database + .prepare("UPDATE sandbox SET last_backup_id = ? WHERE id = ?") + .bind(protectedId, SANDBOX_ID) + .run(); + break; + } + case "restoring": { + await database + .prepare( + `UPDATE sandbox + SET claim_expires_at = ?, claim_owner = 'restore-owner', + last_restore_backup_id = ?, operation_kind = 'activate', + status = 'restoring', status_operation_id = ? + WHERE id = ?`, + ) + .bind(Date.now() + 60_000, protectedId, "01J0000000000000000000000A", SANDBOX_ID) + .run(); + break; + } + case "provisioning": { + await database + .prepare( + `UPDATE session + SET runtime_provisioning_heartbeat_at = 1, + runtime_provisioning_operation_id = ?, + runtime_provisioning_sandbox_id = ? + WHERE id = ?`, + ) + .bind("01J0000000000000000000000A", SANDBOX_ID, sessionId) + .run(); + break; + } + } + + expect(await markSandboxBackupsPruned(database, candidates)).toEqual([candidates[1]]); + await expect( + database + .prepare("SELECT status FROM sandbox_backup WHERE id = ?") + .bind(protectedId) + .first(), + ).resolves.toEqual({ status: "ready" }); }); - - const backupCount = await database - .prepare("SELECT COUNT(*) AS count FROM sandbox_backup") - .first<{ count: number }>(); - const sandbox = await database - .prepare("SELECT last_backup_id FROM sandbox WHERE id = ?") - .bind("01J0000000000000000000000D") - .first<{ last_backup_id: string }>(); - - expect(backupCount?.count).toBe(16); - expect(sandbox?.last_backup_id).toBe("01J000000000000000000000HG"); - }); + } }); diff --git a/apps/api/tests/sandbox-backup-staging.test.ts b/apps/api/tests/sandbox-backup-staging.test.ts new file mode 100644 index 00000000..2bd2adea --- /dev/null +++ b/apps/api/tests/sandbox-backup-staging.test.ts @@ -0,0 +1,365 @@ +import { expect, test } from "bun:test"; + +import { createPlatformId, parsePlatformId } from "@mosoo/id"; +import type { RuntimeOperationId, SandboxBackupId, SandboxId, SessionId } from "@mosoo/id"; + +import { + deferSandboxBackupStageRepair, + listSandboxBackupStages, + listSandboxSessionBackupCandidates, + revokeSandboxBackupsForSessionDelete, + stageSandboxBackupWrites, +} from "../src/modules/runtime/infrastructure/sandbox-backup-store"; +import { applyDrizzleMigrationsThrough } from "./helpers/drizzle-migrations"; +import { SqliteD1Database } from "./helpers/sqlite-d1"; + +const SANDBOX_ID = parsePlatformId("01J0000000000000000000000D"); +const SESSION_ID = parsePlatformId("01J0000000000000000000000S"); +const NEXT_SESSION_ID = parsePlatformId("01J0000000000000000000000T"); +const OPERATION_ID = parsePlatformId("01J0000000000000000000000A"); + +function createDatabase(): SqliteD1Database { + const database = new SqliteD1Database(); + applyDrizzleMigrationsThrough(database, "0020_sandbox-backup-object-authority"); + return database; +} + +async function insertBackingUpSandbox( + database: D1Database, + sandboxId: SandboxId = SANDBOX_ID, +): Promise { + const now = Date.now(); + await database + .prepare( + `INSERT INTO sandbox ( + agent_id, app_id, claim_expires_at, claim_owner, created_at, id, incarnation, + kind, network_constraints_hash, operation_kind, owner_account_id, status, status_operation_id, + subject_id, subject_kind, updated_at + ) VALUES (?, ?, ?, 'owner', ?, ?, 1, 'pet', ?, 'hibernate', ?, 'backing_up', ?, ?, 'agent', ?)`, + ) + .bind( + sandboxId, + "01J0000000000000000000000F", + now + 60_000, + now, + sandboxId, + "0".repeat(64), + "01J0000000000000000000000G", + OPERATION_ID, + sandboxId, + now, + ) + .run(); +} + +async function insertSession( + database: D1Database, + input: { + readonly cleanup?: boolean; + readonly id: SessionId; + readonly lastMessageAt: number; + }, +): Promise { + const now = Date.now(); + await database + .prepare( + `INSERT INTO session ( + agent_id, app_id, archived_at, cleanup_operation_kind, created_at, + creator_account_id, id, kind, last_message_at, model, provider, renamed, + runtime_id, status, status_operation_id, updated_at + ) VALUES (?, ?, ?, ?, ?, ?, ?, 'pet', ?, 'gpt-5.4', 'openai', 0, + 'openai-runtime', 'IDLE', ?, ?)`, + ) + .bind( + "01J0000000000000000000000E", + "01J0000000000000000000000F", + input.cleanup === true ? now : null, + input.cleanup === true ? "delete" : null, + now, + "01J0000000000000000000000G", + input.id, + input.lastMessageAt, + input.cleanup === true ? OPERATION_ID : null, + now, + ) + .run(); +} + +async function insertWorkspace( + database: D1Database, + input: { + readonly cwd: string; + readonly incarnation: number; + readonly sandboxId?: SandboxId; + readonly sessionId: SessionId; + readonly status: "active" | "closed"; + }, +): Promise { + const now = Date.now(); + await database + .prepare( + `INSERT INTO sandbox_session ( + cloudflare_session_id, created_at, cwd, origin_json, sandbox_id, + sandbox_incarnation, session_id, status, updated_at + ) VALUES (?, ?, ?, '{}', ?, ?, ?, ?, ?)`, + ) + .bind( + input.sessionId, + now, + input.cwd, + input.sandboxId ?? SANDBOX_ID, + input.incarnation, + input.sessionId, + input.status, + now, + ) + .run(); +} + +test("workspace staging binds the exact session scope in SQLite", async () => { + const database = createDatabase(); + await insertBackingUpSandbox(database); + await insertSession(database, { id: SESSION_ID, lastMessageAt: 1 }); + await insertWorkspace(database, { + cwd: "/workspace", + incarnation: 1, + sessionId: SESSION_ID, + status: "active", + }); + + const [write] = await stageSandboxBackupWrites(database, { + admission: { + kind: "operation", + lease: { + claimExpiresAt: Date.now() + 60_000, + claimOwner: "owner", + incarnation: 1, + kind: "hibernate", + operationId: OPERATION_ID, + status: "backing_up", + }, + }, + sandboxId: SANDBOX_ID, + targets: [ + { + dir: "/workspace", + updateSandboxLastBackup: false, + workspaceSessionId: SESSION_ID, + }, + ], + ttlSeconds: 100, + }); + + expect(write?.kind).toBe("staged"); + if (write?.kind === "staged") { + expect(write.stage.workspaceSessionId).toBe(SESSION_ID); + expect(write.stage.sandboxIncarnation).toBe(1); + } +}); + +test("session cleanup fences new workspace staging", async () => { + const database = createDatabase(); + await insertBackingUpSandbox(database); + await insertSession(database, { cleanup: true, id: SESSION_ID, lastMessageAt: 1 }); + await insertWorkspace(database, { + cwd: "/workspace", + incarnation: 1, + sessionId: SESSION_ID, + status: "active", + }); + + await expect( + stageSandboxBackupWrites(database, { + admission: { + kind: "operation", + lease: { + claimExpiresAt: Date.now() + 60_000, + claimOwner: "owner", + incarnation: 1, + kind: "hibernate", + operationId: OPERATION_ID, + status: "backing_up", + }, + }, + sandboxId: SANDBOX_ID, + targets: [ + { + dir: "/workspace", + updateSandboxLastBackup: false, + workspaceSessionId: SESSION_ID, + }, + ], + ttlSeconds: 100, + }), + ).rejects.toThrow("lost its exact lifecycle authority"); +}); + +test("checkpoint candidates exclude a closed workspace from the prior incarnation", async () => { + const database = createDatabase(); + await insertSession(database, { id: SESSION_ID, lastMessageAt: 1 }); + await insertSession(database, { id: NEXT_SESSION_ID, lastMessageAt: 2 }); + await insertWorkspace(database, { + cwd: "/workspace/old", + incarnation: 1, + sessionId: SESSION_ID, + status: "closed", + }); + await insertWorkspace(database, { + cwd: "/workspace/current", + incarnation: 2, + sessionId: NEXT_SESSION_ID, + status: "active", + }); + + const candidates = await listSandboxSessionBackupCandidates(database, SANDBOX_ID, 2); + + expect(candidates.map(({ cwd, sessionId }) => ({ cwd, sessionId }))).toEqual([ + { cwd: "/workspace/current", sessionId: NEXT_SESSION_ID }, + ]); +}); + +test("failed repair pages rotate instead of starving the next stage", async () => { + const database = createDatabase(); + const ids = Array.from({ length: 65 }, () => createPlatformId()); + for (const [index, id] of ids.entries()) { + await database + .prepare( + `INSERT INTO sandbox_backup_staging ( + claim_owner, created_at, dir, id, operation_id, sandbox_id, sandbox_incarnation, + ttl_seconds, updated_at, updates_subject_backup + ) VALUES ('stale-owner', 1, ?, ?, ?, ?, 1, 100, 1, 0)`, + ) + .bind(`/workspace/${index.toString().padStart(2, "0")}`, id, OPERATION_ID, SANDBOX_ID) + .run(); + } + + const firstPage = await listSandboxBackupStages(database, 64); + expect(firstPage).toHaveLength(64); + expect(firstPage.map(({ id }) => id)).not.toContain(ids[64]); + for (const stage of firstPage) { + expect(await deferSandboxBackupStageRepair(database, stage)).toBe(true); + } + + const secondPage = await listSandboxBackupStages(database, 64); + expect(secondPage[0]?.id).toBe(ids[64]); +}); + +test("session delete revokes every exact workspace incarnation and preserves other owners", async () => { + const database = createDatabase(); + const otherSandboxId = parsePlatformId("01J0000000000000000000000E"); + const sameSandboxOtherSessionId = parsePlatformId("01J0000000000000000000000V"); + await insertBackingUpSandbox(database); + await insertBackingUpSandbox(database, otherSandboxId); + await insertSession(database, { cleanup: true, id: SESSION_ID, lastMessageAt: 1 }); + await insertSession(database, { id: NEXT_SESSION_ID, lastMessageAt: 2 }); + await insertSession(database, { id: sameSandboxOtherSessionId, lastMessageAt: 3 }); + await insertWorkspace(database, { + cwd: "/workspace/shared", + incarnation: 2, + sessionId: SESSION_ID, + status: "closed", + }); + await insertWorkspace(database, { + cwd: "/workspace/shared", + incarnation: 1, + sandboxId: otherSandboxId, + sessionId: NEXT_SESSION_ID, + status: "closed", + }); + await insertWorkspace(database, { + cwd: "/workspace/shared", + incarnation: 2, + sessionId: sameSandboxOtherSessionId, + status: "closed", + }); + + const exactDeletedPriorId = createPlatformId(); + const exactDeletedCurrentId = createPlatformId(); + const sameSandboxOtherSessionBackupId = createPlatformId(); + const otherSandboxBackupId = createPlatformId(); + const legacyDeletedSandboxId = createPlatformId(); + const legacyOtherSandboxId = createPlatformId(); + for (const [backupId, sandboxId, workspaceSessionId, incarnation] of [ + [exactDeletedPriorId, SANDBOX_ID, SESSION_ID, 1], + [exactDeletedCurrentId, SANDBOX_ID, SESSION_ID, 2], + [sameSandboxOtherSessionBackupId, SANDBOX_ID, sameSandboxOtherSessionId, 2], + [otherSandboxBackupId, otherSandboxId, NEXT_SESSION_ID, 1], + [legacyDeletedSandboxId, SANDBOX_ID, null, 0], + [legacyOtherSandboxId, otherSandboxId, null, 0], + ] as const) { + await database + .prepare( + `INSERT INTO sandbox_backup ( + created_at, dir, id, keep, operation_id, sandbox_id, sandbox_incarnation, + staging_id, status, ttl_seconds, updated_at, workspace_session_id + ) VALUES (1, '/workspace/shared', ?, 0, ?, ?, ?, ?, 'ready', 100, 1, ?)`, + ) + .bind( + backupId, + incarnation === 0 ? null : createPlatformId(), + sandboxId, + incarnation, + incarnation === 0 ? backupId : createPlatformId(), + workspaceSessionId, + ) + .run(); + } + + const stagedActualIds = [ + createPlatformId(), + createPlatformId(), + ]; + for (const [incarnation, actualBackupId] of [ + [1, stagedActualIds[0]], + [2, stagedActualIds[1]], + ] as const) { + await database + .prepare( + `INSERT INTO sandbox_backup_staging ( + actual_backup_id, claim_owner, created_at, dir, id, operation_id, sandbox_id, + sandbox_incarnation, ttl_seconds, updated_at, updates_subject_backup, + workspace_session_id + ) VALUES (?, 'owner', 1, '/workspace/shared', ?, ?, ?, ?, 100, 1, 0, ?)`, + ) + .bind( + actualBackupId, + createPlatformId(), + createPlatformId(), + SANDBOX_ID, + incarnation, + SESSION_ID, + ) + .run(); + } + + const revoked = await revokeSandboxBackupsForSessionDelete(database, { + cwd: "/workspace/shared", + operationId: OPERATION_ID, + sandboxId: SANDBOX_ID, + sessionId: SESSION_ID, + }); + expect(new Set(revoked)).toEqual( + new Set([exactDeletedPriorId, exactDeletedCurrentId, stagedActualIds[0], stagedActualIds[1]]), + ); + await expect( + database + .prepare( + "SELECT COUNT(*) AS count FROM sandbox_backup_staging WHERE workspace_session_id = ?", + ) + .bind(SESSION_ID) + .first(), + ).resolves.toEqual({ count: 0 }); + const rows = await database + .prepare("SELECT id, status FROM sandbox_backup ORDER BY id") + .all<{ id: string; status: string }>(); + expect(rows.results).toEqual( + [ + { id: exactDeletedCurrentId, status: "pruned" }, + { id: exactDeletedPriorId, status: "pruned" }, + { id: legacyDeletedSandboxId, status: "ready" }, + { id: legacyOtherSandboxId, status: "ready" }, + { id: otherSandboxBackupId, status: "ready" }, + { id: sameSandboxOtherSessionBackupId, status: "ready" }, + ].toSorted((left, right) => left.id.localeCompare(right.id)), + ); +}); diff --git a/apps/api/tests/sandbox-conversation-session.test.ts b/apps/api/tests/sandbox-conversation-session.test.ts index cfda4c5d..cadce3f1 100644 --- a/apps/api/tests/sandbox-conversation-session.test.ts +++ b/apps/api/tests/sandbox-conversation-session.test.ts @@ -11,8 +11,14 @@ import type { SandboxHandle, } from "../src/modules/runtime/infrastructure/sandbox-handles"; import type { ApiBindings } from "../src/platform/cloudflare/worker-types"; -import { PublicApiMemoryFileBucket } from "./helpers/public-api-http-test-fixture"; -import { SqliteD1Database } from "./helpers/sqlite-d1"; +import { + PUBLIC_API_TEST_IDS, + PublicApiMemoryFileBucket, + createPublicHttpContractDatabase, + insertActiveSandboxSessionFixture, + insertOwnerSession, +} from "./helpers/public-api-http-test-fixture"; +import type { SqliteD1Database } from "./helpers/sqlite-d1"; mock.module("@cloudflare/sandbox", () => ({ getSandbox: () => { @@ -20,18 +26,25 @@ mock.module("@cloudflare/sandbox", () => ({ }, })); -const { closeIdleCattleConversationSession, ensureSandboxConversationSession } = +const { + closeIdleCattleConversationSession, + ensureSandboxConversationSession, + repairPendingSandboxConversationSessionCleanups, +} = await import("../src/modules/runtime/infrastructure/sandbox-session/sandbox-conversation-session.service"); const ORIGIN = { - callerUserId: "01J00000000000000000000001", + callerUserId: PUBLIC_API_TEST_IDS.ownerAccount, entrypoint: "api", - executionOwnerUserId: "01J00000000000000000000001", + executionOwnerUserId: PUBLIC_API_TEST_IDS.ownerAccount, type: "agent", } as const; const CLOUDFLARE_BACKUP_ID = "550e8400-e29b-41d4-a716-446655440000"; const STORED_BACKUP_ID = encodeSandboxBackupIdForStorage(CLOUDFLARE_BACKUP_ID); const ARTIFACT_SHA256 = "a".repeat(64); +const SANDBOX_SESSION_ID = "01J00000000000000000000001"; +const SANDBOX_INCARNATION = 1; +const SESSION_ID = PUBLIC_API_TEST_IDS.ownerSession; function commandResult(): RuntimeCommandResultHandle { return { @@ -51,80 +64,23 @@ function failedCommandResult(): RuntimeCommandResultHandle { }; } -function createConversationSessionDatabase(kind: AgentKind = "pet"): SqliteD1Database { - const database = new SqliteD1Database(); - - database.execute(` - CREATE TABLE sandbox ( - id text PRIMARY KEY NOT NULL, - inactive_deadline_at integer, - kind text NOT NULL, - status text DEFAULT 'active' NOT NULL, - status_changed_at integer DEFAULT 0 NOT NULL, - status_event text DEFAULT 'runtime_subject.active' NOT NULL, - status_operation_id text, - status_seq integer DEFAULT 0 NOT NULL, - status_source text DEFAULT 'system' NOT NULL, - updated_at integer NOT NULL - ); - - CREATE TABLE sandbox_session ( - cloudflare_session_id text NOT NULL, - created_at integer NOT NULL, - cwd text NOT NULL, - origin_json text NOT NULL, - sandbox_id text NOT NULL, - session_id text PRIMARY KEY NOT NULL, - status text NOT NULL, - updated_at integer NOT NULL - ); - - CREATE TABLE sandbox_backup ( - created_at integer NOT NULL, - dir text NOT NULL, - id text PRIMARY KEY NOT NULL, - sandbox_id text NOT NULL, - session_run_id text, - status text NOT NULL - ); - - CREATE TABLE session ( - agent_id text, - id text PRIMARY KEY NOT NULL, - kind text DEFAULT '${kind}' NOT NULL, - last_run_id text, - workspace_checkpoint_required integer DEFAULT 0 NOT NULL - ); +async function createConversationSessionDatabase( + kind: AgentKind = "pet", +): Promise { + const database = await createPublicHttpContractDatabase(); - CREATE TABLE driver_instance ( - id text PRIMARY KEY NOT NULL, - sandbox_id text NOT NULL - ); - - CREATE TABLE session_run ( - driver_instance_id text, - id text PRIMARY KEY NOT NULL, - status text NOT NULL - ); - - CREATE TABLE file_record ( - id text PRIMARY KEY NOT NULL, - created_at integer NOT NULL, - name text NOT NULL, - object_key text NOT NULL, - parent_path text NOT NULL, - scope_id text, - scope_kind text NOT NULL, - session_kind text, - size integer NOT NULL, - status text NOT NULL - ); - `); - - database.execute(` - INSERT INTO sandbox (id, inactive_deadline_at, kind, updated_at) - VALUES ('01J0000000000000000000000D', 123, '${kind}', 1); - `); + await insertOwnerSession(database); + await database.prepare("UPDATE session SET kind = ? WHERE id = ?").bind(kind, SESSION_ID).run(); + await insertActiveSandboxSessionFixture(database, { + inactiveDeadlineAt: 123, + kind, + ownerAccountId: PUBLIC_API_TEST_IDS.ownerAccount, + sandboxId: PUBLIC_API_TEST_IDS.sandbox, + sandboxSessionId: SANDBOX_SESSION_ID, + sessionId: SESSION_ID, + timestampMs: 1, + }); + await database.prepare("DELETE FROM sandbox_session WHERE session_id = ?").bind(SESSION_ID).run(); return database; } @@ -135,10 +91,6 @@ async function insertConversationSession( readonly status: SandboxSessionStatus; }, ): Promise { - await database - .prepare("INSERT INTO session (agent_id, id) VALUES (?, ?) ON CONFLICT (id) DO NOTHING") - .bind(null, "session-1") - .run(); await database .prepare( ` @@ -148,20 +100,22 @@ async function insertConversationSession( cwd, origin_json, sandbox_id, + sandbox_incarnation, session_id, status, updated_at ) - VALUES (?, ?, ?, ?, ?, ?, ?, ?) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) `, ) .bind( - "01J00000000000000000000001", + SANDBOX_SESSION_ID, 1, - "/workspace/se/session-1", + `/workspace/se/${SESSION_ID}`, JSON.stringify(ORIGIN), - "01J0000000000000000000000D", - "session-1", + PUBLIC_API_TEST_IDS.sandbox, + SANDBOX_INCARNATION, + SESSION_ID, input.status, 1, ) @@ -170,7 +124,11 @@ async function insertConversationSession( async function insertConversationBackup( database: D1Database, - input: { createdAt?: number; dir?: string } = {}, + input: { + createdAt?: number; + dir?: string; + workspaceSessionId?: string | null; + } = {}, ): Promise { await database .prepare( @@ -179,18 +137,32 @@ async function insertConversationBackup( created_at, dir, id, + keep, + operation_id, sandbox_id, - status + sandbox_incarnation, + staging_id, + status, + ttl_seconds, + updated_at, + workspace_session_id ) - VALUES (?, ?, ?, ?, ?) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) `, ) .bind( input.createdAt ?? 1, - input.dir ?? "/workspace/se/session-1", + input.dir ?? `/workspace/se/${SESSION_ID}`, + STORED_BACKUP_ID, + 0, + PUBLIC_API_TEST_IDS.operation, + PUBLIC_API_TEST_IDS.sandbox, + SANDBOX_INCARNATION, STORED_BACKUP_ID, - "01J0000000000000000000000D", "ready", + 86_400, + input.createdAt ?? 1, + input.workspaceSessionId === undefined ? SESSION_ID : input.workspaceSessionId, ) .run(); } @@ -208,7 +180,7 @@ async function readConversationSession(database: D1Database): Promise<{ WHERE session_id = ? `, ) - .bind("session-1") + .bind(SESSION_ID) .first<{ cloudflare_session_id: string; cwd: string; @@ -225,7 +197,7 @@ async function readConversationSession(database: D1Database): Promise<{ async function readInactiveDeadline(database: D1Database): Promise { return database .prepare("SELECT inactive_deadline_at FROM sandbox WHERE id = ?") - .bind("01J0000000000000000000000D") + .bind(PUBLIC_API_TEST_IDS.sandbox) .first("inactive_deadline_at"); } @@ -325,25 +297,26 @@ async function setWorkspaceCheckpointRequired( ): Promise { await database .prepare("UPDATE session SET workspace_checkpoint_required = ? WHERE id = ?") - .bind(required ? 1 : 0, "session-1") + .bind(required ? 1 : 0, SESSION_ID) .run(); } function createInput(sandbox: SandboxHandle, kind: AgentKind = "pet") { return { - agentId: "01J00000000000000000000009", + agentId: PUBLIC_API_TEST_IDS.agent, kind, mountSessionResources: false, origin: ORIGIN, sandbox, - sandboxId: "01J0000000000000000000000D", - sessionId: "session-1", + sandboxId: PUBLIC_API_TEST_IDS.sandbox, + sandboxIncarnation: SANDBOX_INCARNATION, + sessionId: SESSION_ID, }; } describe("ensureSandboxConversationSession", () => { test("reuses an active session without preparing directories", async () => { - const database = createConversationSessionDatabase(); + const database = await createConversationSessionDatabase(); await insertConversationSession(database, { status: "active" }); const sandbox = createSandbox(); @@ -352,17 +325,17 @@ describe("ensureSandboxConversationSession", () => { createInput(sandbox), ); - expect(result.sandboxSessionId).toBe("01J00000000000000000000001"); + expect(result.sandboxSessionId).toBe(SANDBOX_SESSION_ID); await expect(readConversationSession(database)).resolves.toEqual({ - cloudflare_session_id: "01J00000000000000000000001", - cwd: "/workspace/se/session-1", + cloudflare_session_id: SANDBOX_SESSION_ID, + cwd: `/workspace/se/${SESSION_ID}`, status: "active", }); await expect(readInactiveDeadline(database)).resolves.toBe(123); }); test("creates a missing conversation session record", async () => { - const database = createConversationSessionDatabase(); + const database = await createConversationSessionDatabase(); const sandbox = createSandbox(); const result = await ensureSandboxConversationSession( @@ -379,7 +352,7 @@ describe("ensureSandboxConversationSession", () => { }); test("arms an idle deadline when a legacy Pet session has none", async () => { - const database = createConversationSessionDatabase(); + const database = await createConversationSessionDatabase(); database.execute("UPDATE sandbox SET inactive_deadline_at = NULL"); const sandbox = createSandbox(); const startedAt = Date.now(); @@ -392,7 +365,7 @@ describe("ensureSandboxConversationSession", () => { }); test("continues a warm closed cattle session with a new execution session id", async () => { - const database = createConversationSessionDatabase("cattle"); + const database = await createConversationSessionDatabase("cattle"); await insertConversationSession(database, { status: "closed" }); const sandbox = createSandbox(); @@ -401,7 +374,7 @@ describe("ensureSandboxConversationSession", () => { createInput(sandbox, "cattle"), ); - expect(result.sandboxSessionId).not.toBe("01J00000000000000000000001"); + expect(result.sandboxSessionId).not.toBe(SANDBOX_SESSION_ID); expect(isPlatformId(result.sandboxSessionId)).toBe(true); await expect(readConversationSession(database)).resolves.toMatchObject({ @@ -412,7 +385,7 @@ describe("ensureSandboxConversationSession", () => { }); test("restores a cold cattle session from a 20-day-old committed checkpoint", async () => { - const database = createConversationSessionDatabase("cattle"); + const database = await createConversationSessionDatabase("cattle"); await insertConversationSession(database, { status: "closed" }); await setWorkspaceCheckpointRequired(database, true); await insertConversationBackup(database, { @@ -432,13 +405,13 @@ describe("ensureSandboxConversationSession", () => { ); expect(restoredBackup).toEqual({ - dir: "/workspace/se/session-1", + dir: `/workspace/se/${SESSION_ID}`, id: CLOUDFLARE_BACKUP_ID, }); }); test("fails cold cattle continuation when its exact Thread checkpoint is missing", async () => { - const database = createConversationSessionDatabase("cattle"); + const database = await createConversationSessionDatabase("cattle"); await insertConversationSession(database, { status: "closed" }); await setWorkspaceCheckpointRequired(database, true); await insertConversationBackup(database, { dir: "/workspace/se/another-session" }); @@ -449,8 +422,22 @@ describe("ensureSandboxConversationSession", () => { ).rejects.toThrow("has no committed workspace checkpoint"); }); + test("does not consume a legacy checkpoint without exact Thread ownership", async () => { + const database = await createConversationSessionDatabase("cattle"); + await insertConversationSession(database, { status: "closed" }); + await setWorkspaceCheckpointRequired(database, true); + await insertConversationBackup(database, { workspaceSessionId: null }); + + await expect( + ensureSandboxConversationSession( + createBindings(database), + createInput(createSandbox({ cwdHasContent: false }), "cattle"), + ), + ).rejects.toThrow("has no committed workspace checkpoint"); + }); + test("reports an actionable error for a corrupt cattle checkpoint", async () => { - const database = createConversationSessionDatabase("cattle"); + const database = await createConversationSessionDatabase("cattle"); await insertConversationSession(database, { status: "closed" }); await setWorkspaceCheckpointRequired(database, true); await insertConversationBackup(database); @@ -464,17 +451,28 @@ describe("ensureSandboxConversationSession", () => { ).rejects.toThrow("workspace checkpoint could not be restored. Retry the continuation"); }); - test("restores recorded artifacts for a pre-rollout cattle Thread", async () => { - const database = createConversationSessionDatabase("cattle"); + test("restores backfilled artifacts for a pre-rollout cattle Thread", async () => { + const database = await createConversationSessionDatabase("cattle"); await insertConversationSession(database, { status: "closed" }); database.execute(` INSERT INTO file_record ( - id, created_at, name, object_key, parent_path, scope_id, scope_kind, - session_kind, size, status + committed, created_at, created_by_account_id, id, name, object_key, + owner_id, owner_kind, parent_path, path, purpose, runtime_event_seq, + scope_id, scope_kind, session_kind, size, status, updated_at, version ) VALUES ( - 'artifact-legacy', 1, 'legacy.txt', 'artifacts/legacy.txt', - 'runtime-output/outputs/legacy.txt/${ARTIFACT_SHA256}', 'session-1', 'session', - 'artifact', 14, 'ready' + 1, 1, '${PUBLIC_API_TEST_IDS.ownerAccount}', '${PUBLIC_API_TEST_IDS.file}', + 'legacy.txt', 'artifacts/legacy.txt', '${SESSION_ID}', 'session', + 'runtime-output/outputs/legacy.txt/${ARTIFACT_SHA256}', + 'session-artifacts/${PUBLIC_API_TEST_IDS.file}/legacy.txt', 'session_artifact', 0, + '${SESSION_ID}', 'session', 'artifact', 14, 'ready', 1, 1 + ); + + INSERT INTO session_artifact_head ( + file_id, runtime_event_seq, session_id, source_event_id, source_path, updated_at + ) VALUES ( + '${PUBLIC_API_TEST_IDS.file}', 0, '${SESSION_ID}', + 'legacy-file:${PUBLIC_API_TEST_IDS.file}', + 'outputs/legacy.txt', 1 ); `); const bucket = new PublicApiMemoryFileBucket(); @@ -486,15 +484,15 @@ describe("ensureSandboxConversationSession", () => { }); await ensureSandboxConversationSession( - createBindings(database, undefined, bucket as unknown as R2Bucket), + createBindings(database, undefined, bucket), createInput(sandbox, "cattle"), ); - expect(restoredPaths).toContain("/workspace/se/session-1/outputs/legacy.txt"); + expect(restoredPaths).toContain(`/workspace/se/${SESSION_ID}/outputs/legacy.txt`); }); test("continues a closed pet session through the stable restore path", async () => { - const database = createConversationSessionDatabase(); + const database = await createConversationSessionDatabase(); await insertConversationSession(database, { status: "closed" }); await insertConversationBackup(database); let restoredBackup: { readonly dir: string; readonly id: string } | null = null; @@ -510,13 +508,32 @@ describe("ensureSandboxConversationSession", () => { createInput(sandbox, "pet"), ); - expect(result.sandboxSessionId).toBe("01J00000000000000000000001"); + expect(result.sandboxSessionId).toBe(SANDBOX_SESSION_ID); expect(restoredBackup).toEqual({ - dir: "/workspace/se/session-1", + dir: `/workspace/se/${SESSION_ID}`, id: CLOUDFLARE_BACKUP_ID, }); await expect(readConversationSession(database)).resolves.toMatchObject({ - cloudflare_session_id: "01J00000000000000000000001", + cloudflare_session_id: SANDBOX_SESSION_ID, + status: "active", + }); + }); + + test("replaces a closed pet execution session for a fenced Run provisioning", async () => { + const database = await createConversationSessionDatabase(); + await insertConversationSession(database, { status: "closed" }); + await insertConversationBackup(database); + const sandbox = createSandbox({ cwdHasContent: false }); + + const result = await ensureSandboxConversationSession(createBindings(database), { + ...createInput(sandbox, "pet"), + replaceClosedExecutionSession: true, + }); + + expect(result.sandboxSessionId).not.toBe(SANDBOX_SESSION_ID); + expect(isPlatformId(result.sandboxSessionId)).toBe(true); + await expect(readConversationSession(database)).resolves.toMatchObject({ + cloudflare_session_id: result.sandboxSessionId, status: "active", }); }); @@ -524,26 +541,66 @@ describe("ensureSandboxConversationSession", () => { describe("closeIdleCattleConversationSession", () => { test("arms subject reclamation when the remote session is already absent", async () => { - const database = createConversationSessionDatabase("cattle"); + const database = await createConversationSessionDatabase("cattle"); await insertConversationSession(database, { status: "active" }); database.execute("UPDATE sandbox SET inactive_deadline_at = NULL"); - const sandboxSessionId = "01J00000000000000000000001"; const sandbox = createSandbox({ - deleteSessionError: new Error(`Session '${sandboxSessionId}' not found`), + deleteSessionError: new Error(`Session '${SANDBOX_SESSION_ID}' not found`), }); const startedAt = Date.now(); await expect( closeIdleCattleConversationSession(createBindings(database, sandbox), { idleSinceLte: 1, - sandboxId: "01J0000000000000000000000D", - sessionId: "session-1", + sandboxId: PUBLIC_API_TEST_IDS.sandbox, + sessionId: SESSION_ID, }), - ).rejects.toThrow(`Session '${sandboxSessionId}' not found`); + ).resolves.toBe(true); await expect(readConversationSession(database)).resolves.toMatchObject({ status: "closed" }); + await expect( + database + .prepare( + `SELECT event_type, family, source, visibility + FROM session_event + WHERE session_id = ? AND event_type = 'runtime.sandbox.updated'`, + ) + .bind(SESSION_ID) + .first(), + ).resolves.toEqual({ + event_type: "runtime.sandbox.updated", + family: "sandbox", + source: "system", + visibility: "owner_debug", + }); const deadline = await readInactiveDeadline(database); expect(deadline).toBeGreaterThanOrEqual(startedAt + 5 * 60_000); expect(deadline).toBeLessThanOrEqual(Date.now() + 5 * 60_000); }); + + test("repairs cleanup against its retired physical incarnation", async () => { + const database = await createConversationSessionDatabase("cattle"); + await insertConversationSession(database, { status: "active" }); + database.execute(` + UPDATE sandbox SET incarnation = 5; + UPDATE sandbox_session + SET cleanup_operation_id = '${PUBLIC_API_TEST_IDS.operation}', + sandbox_incarnation = 4, + status = 'cleanup_pending'; + `); + const physicalIds: string[] = []; + const sandbox = createSandbox(); + const bindings = { + DB: database, + runtimeSubjectHandleFactory: (physicalId: string) => { + physicalIds.push(physicalId); + return sandbox; + }, + } as ApiBindings; + + await expect(repairPendingSandboxConversationSessionCleanups(bindings, 10)).resolves.toBe(1); + + expect(physicalIds).toEqual([`${PUBLIC_API_TEST_IDS.sandbox}-i4`]); + await expect(readConversationSession(database)).resolves.toMatchObject({ status: "closed" }); + }); }); diff --git a/apps/api/tests/sandbox-runtime-incarnation.test.ts b/apps/api/tests/sandbox-runtime-incarnation.test.ts new file mode 100644 index 00000000..102ba344 --- /dev/null +++ b/apps/api/tests/sandbox-runtime-incarnation.test.ts @@ -0,0 +1,742 @@ +import { describe, expect, mock, spyOn, test } from "bun:test"; + +import { hashSandboxNetworkConstraints } from "../src/modules/runtime/domain/sandbox-network-constraints"; + +const FULL_NETWORK_CONSTRAINTS = { allowedHosts: [], networkPolicy: "full" } as const; +const FULL_NETWORK_CONSTRAINTS_HASH = await hashSandboxNetworkConstraints(FULL_NETWORK_CONSTRAINTS); + +class SandboxDelegateMock { + alarmBarrier: Promise | null = null; + backupCalls = 0; + backupOptions: unknown[] = []; + destroyBarrier: Promise | null = null; + destroyCalls = 0; + destroyError: Error | null = null; + enableInternet = true; + envVars: Record = {}; + getPlacementError: Error | null = null; + interceptHttps = false; + lastSessionKeys: PropertyKey[] = []; + mkdirCalls = 0; + placement: string | null | undefined = "placement-1"; + readonly plainDto = { nested: { value: "unchanged" }, success: true }; + processActionCalls = 0; + readFileBarrier: Promise | null = null; + readFileError: Error | null = null; + startProcessBarrier: Promise | null = null; + terminalBarrier: Promise | null = null; + terminalCalls = 0; + terminalError: Error | null = null; + streamOpenCalls = 0; + streamReadCalls = 0; + writeFileBarrier: Promise | null = null; + onAlarmStart: (() => void) | null = null; + onMkdir: (() => void) | null = null; + onReadFileStart: (() => void) | null = null; + onStartProcessStart: (() => void) | null = null; + onTerminalStart: (() => void) | null = null; + onWriteFileStart: (() => void) | null = null; + readonly files = new Map(); + + async alarm(): Promise { + this.onAlarmStart?.(); + await this.alarmBarrier; + } + + async createBackup(options: unknown): Promise<{ dir: string; id: string }> { + this.backupCalls += 1; + this.backupOptions.push(options); + return { + dir: Reflect.get(options as object, "dir") as string, + id: "550e8400-e29b-41d4-a716-446655440001", + }; + } + + async createSession(options: { id: string }) { + void options; + const session = { + execStream: () => this.execStream(), + getProcess: () => this.getProcess(), + id: "user-session", + listProcesses: () => this.listProcesses(), + readFileStream: () => this.readFileStream(), + readFile: async (path: string) => { + const content = this.files.get(path); + if (content === undefined) { + throw new Error("missing file"); + } + return { content }; + }, + startProcess: ( + command: string, + startOptions?: Parameters[1], + ) => this.startProcess(command, startOptions), + streamProcessLogs: () => this.streamProcessLogs(), + terminal: async () => { + this.onTerminalStart?.(); + await this.terminalBarrier; + if (this.terminalError !== null) { + throw this.terminalError; + } + this.terminalCalls += 1; + return new Response("terminal"); + }, + upstreamOnlyMethod: async () => "preserved", + }; + this.lastSessionKeys = Reflect.ownKeys(session); + return session; + } + + async deleteSession(): Promise {} + + async destroy(): Promise { + this.destroyCalls += 1; + await this.destroyBarrier; + if (this.destroyError !== null) { + throw this.destroyError; + } + } + + async exec(): Promise { + return this.plainDto; + } + + async exists(path: string) { + return { + exists: this.files.has(path), + path, + success: true, + timestamp: new Date(0).toISOString(), + }; + } + + async execStream(): Promise> { + this.streamOpenCalls += 1; + return this.streamHandle(); + } + + async getContainerPlacementId(): Promise { + if (this.getPlacementError !== null) { + throw this.getPlacementError; + } + return this.placement; + } + + async getProcess(): Promise> { + return this.processHandle(); + } + + async getSession(): ReturnType { + return this.createSession({ id: "user-session" }); + } + + async mkdir(): Promise { + this.mkdirCalls += 1; + this.onMkdir?.(); + } + + async listProcesses(): Promise>> { + return [this.processHandle()]; + } + + processHandle() { + const action = async () => { + this.processActionCalls += 1; + }; + return { + command: "driver", + endTime: undefined, + exitCode: undefined, + getLogs: action, + getStatus: action, + id: "process-1", + kill: action, + pid: 1, + sessionId: "session-1", + startTime: new Date(0), + status: "running", + waitForExit: action, + waitForLog: action, + waitForPort: action, + }; + } + + async startProcess( + _command: string, + options?: { onStart?: (process: ReturnType) => void }, + ): Promise> { + const process = this.processHandle(); + options?.onStart?.(process); + this.onStartProcessStart?.(); + await this.startProcessBarrier; + return process; + } + + streamHandle() { + return { + [Symbol.asyncIterator]() { + return this; + }, + next: async () => { + this.streamReadCalls += 1; + return { done: true as const, value: undefined }; + }, + }; + } + + async readFile(path: string): Promise<{ content: string }> { + this.onReadFileStart?.(); + await this.readFileBarrier; + if (this.readFileError !== null) { + throw this.readFileError; + } + const content = this.files.get(path); + if (content === undefined) { + throw new Error("missing file"); + } + return { content }; + } + + async readFileStream(): Promise> { + this.streamOpenCalls += 1; + return this.streamHandle(); + } + + async setAllowedHosts(): Promise {} + + async setKeepAlive(): Promise {} + + async streamProcessLogs(): Promise> { + this.streamOpenCalls += 1; + return this.streamHandle(); + } + + async writeFile(path: string, content: string): Promise { + this.onWriteFileStart?.(); + await this.writeFileBarrier; + this.files.set(path, content); + } +} + +let delegate: SandboxDelegateMock; +let abortCalls = 0; +let abortRetryAlarm = false; +let alarmWrites = 0; + +function registerDelegate(value: SandboxDelegateMock): void { + delegate = value; +} + +mock.module("cloudflare:workers", () => ({ + DurableObject: class { + constructor( + protected readonly ctx: unknown, + protected readonly env: unknown, + ) {} + }, +})); + +mock.module("@cloudflare/sandbox", () => ({ + Sandbox: class extends SandboxDelegateMock { + constructor() { + super(); + registerDelegate(this); + } + }, +})); + +const { Sandbox } = await import("../src/adapters/durable-objects/sandbox.do"); + +function createBarrier(): { readonly promise: Promise; readonly release: () => void } { + let release = () => {}; + const promise = new Promise((resolve) => { + release = resolve; + }); + return { promise, release }; +} + +function createSandbox(): Sandbox { + const values = new Map(); + const ctx = { + abort: (_reason?: string, options?: { retryAlarm?: boolean }) => { + abortCalls += 1; + abortRetryAlarm = options?.retryAlarm === true; + throw new Error("durable object aborted"); + }, + blockConcurrencyWhile: (action: () => Promise) => action(), + container: { running: true }, + storage: { + get: async (key: string) => values.get(key) as T | undefined, + put: async (key: string | Record, value?: unknown) => { + if (typeof key === "string") { + values.set(key, value); + return; + } + for (const [entryKey, entryValue] of Object.entries(key)) { + values.set(entryKey, entryValue); + } + }, + setAlarm: async () => { + alarmWrites += 1; + }, + }, + }; + abortCalls = 0; + abortRetryAlarm = false; + alarmWrites = 0; + return new Sandbox(ctx, { SANDBOX_FILE_BUCKET_LOCAL: "false" }); +} + +async function createReadySandbox(): Promise { + const sandbox = createSandbox(); + await sandbox.activateRuntimeSubjectIncarnation(4, FULL_NETWORK_CONSTRAINTS_HASH); + await sandbox.markRuntimeSubjectIncarnationReady(4, FULL_NETWORK_CONSTRAINTS_HASH); + return sandbox; +} + +describe("sandbox runtime incarnation health", () => { + test("fails closed before SDK backup creation when legacy persistent auth exists", async () => { + const sandbox = await createReadySandbox(); + const authPath = "/workspace/se/session/.state/openai-runtime/auth.json"; + delegate.files.set(authPath, "secret"); + const handle = sandbox as unknown as { + createRuntimeSubjectBackup( + incarnation: number, + options: { + dir: string; + excludes: string[]; + forbiddenPaths: string[]; + name: string; + }, + ): Promise<{ dir: string; id: string }>; + }; + const options = { + dir: "/workspace/se/session", + excludes: [".mosoo-session-files-session"], + forbiddenPaths: [authPath], + name: "mosoo:runtime-backup:v1:01J0000000000000000000000B", + }; + + await expect(handle.createRuntimeSubjectBackup(4, options)).rejects.toThrow( + "legacy persistent credentials", + ); + expect(delegate.backupCalls).toBe(0); + expect(delegate.mkdirCalls).toBe(0); + + delegate.files.delete(authPath); + await expect(handle.createRuntimeSubjectBackup(4, options)).resolves.toEqual({ + dir: options.dir, + id: "550e8400-e29b-41d4-a716-446655440001", + }); + expect(delegate.mkdirCalls).toBe(1); + expect(delegate.backupCalls).toBe(1); + expect(delegate.backupOptions).toEqual([ + { + dir: options.dir, + excludes: options.excludes, + name: options.name, + }, + ]); + }); + + test("keeps incarnation and network policy as one immutable identity", async () => { + const sandbox = createSandbox(); + await sandbox.activateRuntimeSubjectIncarnation(4, FULL_NETWORK_CONSTRAINTS_HASH); + await expect( + sandbox.activateRuntimeSubjectIncarnation(4, FULL_NETWORK_CONSTRAINTS_HASH), + ).resolves.toBeUndefined(); + await expect(sandbox.activateRuntimeSubjectIncarnation(4, "0".repeat(64))).rejects.toThrow( + "identity does not match", + ); + }); + + test("rejects network mutation that differs from the admitted identity", async () => { + const sandbox = createSandbox(); + await sandbox.activateRuntimeSubjectIncarnation(4, FULL_NETWORK_CONSTRAINTS_HASH); + + await expect( + sandbox.configureNetworkConstraints({ + allowedHosts: ["example.com"], + networkPolicy: "limited", + }), + ).rejects.toThrow("do not match the admitted incarnation"); + }); + + test("keeps a ready incarnation when the placement handshake is unavailable", async () => { + const sandbox = await createReadySandbox(); + delegate.getPlacementError = new Error("control plane unavailable"); + + await expect( + (sandbox as unknown as { mkdir(path: string): Promise }).mkdir("/workspace"), + ).rejects.toThrow("health is unknown"); + expect(delegate.destroyCalls).toBe(0); + }); + + test("retires a ready incarnation after a refreshed placement mismatch", async () => { + const sandbox = await createReadySandbox(); + delegate.placement = "placement-2"; + + await expect( + (sandbox as unknown as { mkdir(path: string): Promise }).mkdir("/workspace"), + ).rejects.toThrow("replaced or stopped"); + expect(delegate.destroyCalls).toBe(1); + await expect( + sandbox.inspectRuntimeSubjectIncarnation(4, FULL_NETWORK_CONSTRAINTS_HASH), + ).resolves.toEqual({ + kind: "retired", + }); + }); + + test("refreshes cached placement before running a business mutation", async () => { + const sandbox = await createReadySandbox(); + delegate.onReadFileStart = () => { + delegate.placement = "placement-2"; + }; + + await expect( + (sandbox as unknown as { mkdir(path: string): Promise }).mkdir("/workspace"), + ).rejects.toThrow("replaced or stopped"); + expect(delegate.mkdirCalls).toBe(0); + expect(delegate.destroyCalls).toBe(1); + }); + + test("retires when a missing sentinel refreshes the cached placement", async () => { + const sandbox = await createReadySandbox(); + delegate.onReadFileStart = () => { + delegate.files.delete("/tmp/.mosoo-runtime-subject-incarnation"); + delegate.placement = "placement-2"; + }; + + await expect( + (sandbox as unknown as { mkdir(path: string): Promise }).mkdir("/workspace"), + ).rejects.toThrow("replaced or stopped"); + expect(delegate.mkdirCalls).toBe(0); + expect(delegate.destroyCalls).toBe(1); + }); + + test("retires a structured missing sentinel without placement ids", async () => { + const sandbox = createSandbox(); + await sandbox.activateRuntimeSubjectIncarnation(4, FULL_NETWORK_CONSTRAINTS_HASH); + delegate.placement = null; + await sandbox.markRuntimeSubjectIncarnationReady(4, FULL_NETWORK_CONSTRAINTS_HASH); + delegate.readFileError = Object.assign(new Error("missing sentinel"), { + code: "FILE_NOT_FOUND", + }); + + await expect( + (sandbox as unknown as { mkdir(path: string): Promise }).mkdir("/workspace"), + ).rejects.toThrow("replaced or stopped"); + expect(delegate.mkdirCalls).toBe(0); + expect(delegate.destroyCalls).toBe(1); + }); + + test("re-destroys a retired incarnation after a late local sentinel read", async () => { + const sandbox = createSandbox(); + await sandbox.activateRuntimeSubjectIncarnation(4, FULL_NETWORK_CONSTRAINTS_HASH); + await sandbox.configureNetworkConstraints(FULL_NETWORK_CONSTRAINTS); + delegate.placement = null; + await sandbox.markRuntimeSubjectIncarnationReady(4, FULL_NETWORK_CONSTRAINTS_HASH); + const read = createBarrier(); + const started = createBarrier(); + delegate.readFileBarrier = read.promise; + delegate.onReadFileStart = started.release; + + const inspection = sandbox.inspectRuntimeSubjectIncarnation(4, FULL_NETWORK_CONSTRAINTS_HASH); + await started.promise; + await sandbox.destroyRuntimeSubjectIncarnation(4); + await sandbox.alarm(); + expect(delegate.destroyCalls).toBe(2); + + read.release(); + await expect(inspection).resolves.toEqual({ kind: "retired" }); + expect(delegate.destroyCalls).toBe(3); + }); + + test("rejects and retires a mutation whose container placement changes mid-RPC", async () => { + const sandbox = await createReadySandbox(); + delegate.onMkdir = () => { + delegate.placement = "placement-2"; + }; + + await expect( + (sandbox as unknown as { mkdir(path: string): Promise }).mkdir("/workspace"), + ).rejects.toThrow("replaced or stopped"); + expect(delegate.mkdirCalls).toBe(1); + expect(delegate.destroyCalls).toBe(1); + }); + + test("re-destroys a retired incarnation after a late readiness write", async () => { + const sandbox = createSandbox(); + await sandbox.activateRuntimeSubjectIncarnation(4, FULL_NETWORK_CONSTRAINTS_HASH); + await sandbox.configureNetworkConstraints(FULL_NETWORK_CONSTRAINTS); + const write = createBarrier(); + const started = createBarrier(); + delegate.writeFileBarrier = write.promise; + delegate.onWriteFileStart = started.release; + + const readiness = sandbox.markRuntimeSubjectIncarnationReady(4, FULL_NETWORK_CONSTRAINTS_HASH); + await started.promise; + await sandbox.destroyRuntimeSubjectIncarnation(4); + await sandbox.alarm(); + expect(delegate.destroyCalls).toBe(2); + + write.release(); + await expect(readiness).rejects.toThrow("retired"); + expect(delegate.destroyCalls).toBe(3); + }); + + test("fences terminal sessions returned by create and get", async () => { + const sandbox = await createReadySandbox(); + const handle = sandbox as unknown as { + createSession(options: { id: string }): Promise<{ + terminal(request: Request): Promise; + }>; + getSession(id: string): Promise<{ terminal(request: Request): Promise }>; + }; + const created = await handle.createSession({ id: "created-session" }); + const fetched = await handle.getSession("created-session"); + + const byPropertyKey = (left: PropertyKey, right: PropertyKey) => + String(left).localeCompare(String(right)); + expect(Reflect.ownKeys(created).toSorted(byPropertyKey)).toEqual( + delegate.lastSessionKeys.toSorted(byPropertyKey), + ); + + await expect(created.terminal(new Request("https://sandbox.test"))).resolves.toBeInstanceOf( + Response, + ); + await expect(fetched.terminal(new Request("https://sandbox.test"))).resolves.toBeInstanceOf( + Response, + ); + expect(delegate.terminalCalls).toBe(2); + }); + + test("re-destroys after a late terminal RPC resumes past the consumed alarm", async () => { + const sandbox = await createReadySandbox(); + const handle = sandbox as unknown as { + createSession(options: { id: string }): Promise<{ + terminal(request: Request): Promise; + }>; + }; + const session = await handle.createSession({ id: "terminal-session" }); + const terminal = createBarrier(); + const started = createBarrier(); + delegate.terminalBarrier = terminal.promise; + delegate.onTerminalStart = started.release; + + const response = session.terminal(new Request("https://sandbox.test")); + await started.promise; + await sandbox.destroyRuntimeSubjectIncarnation(4); + await sandbox.alarm(); + expect(delegate.destroyCalls).toBe(2); + + terminal.release(); + await expect(response).rejects.toThrow("retired during the operation"); + expect(delegate.destroyCalls).toBe(3); + }); + + test.each([ + ["successful cleanup", null], + ["failed cleanup", new Error("destroy failed")], + ] as const)("preserves a task failure after retirement with %s", async (_, destroyFailure) => { + const sandbox = await createReadySandbox(); + const handle = sandbox as unknown as { + createSession(options: { id: string }): Promise<{ + terminal(request: Request): Promise; + }>; + }; + const session = await handle.createSession({ id: "terminal-session" }); + const terminal = createBarrier(); + const started = createBarrier(); + const taskFailure = new Error("terminal failed"); + delegate.terminalBarrier = terminal.promise; + delegate.terminalError = taskFailure; + delegate.onTerminalStart = started.release; + + const response = session.terminal(new Request("https://sandbox.test")); + await started.promise; + await sandbox.destroyRuntimeSubjectIncarnation(4); + delegate.destroyError = destroyFailure; + terminal.release(); + + try { + await response; + throw new Error("The retired RPC unexpectedly succeeded."); + } catch (error) { + if (destroyFailure === null) { + expect(error).toBe(taskFailure); + } else { + if (!(error instanceof AggregateError)) { + throw error; + } + expect(error.errors).toEqual([taskFailure, destroyFailure]); + } + } + expect(delegate.destroyCalls).toBe(2); + }); + + test("rejects a late process start after the next incarnation retires its owner", async () => { + const sandbox = await createReadySandbox(); + const handle = sandbox as unknown as { + startProcess(command: string): Promise>; + }; + const process = createBarrier(); + const started = createBarrier(); + delegate.startProcessBarrier = process.promise; + delegate.onStartProcessStart = started.release; + + const lateStart = handle.startProcess("driver"); + await started.promise; + await sandbox.destroyRuntimeSubjectIncarnation(4); + await sandbox.alarm(); + expect(delegate.destroyCalls).toBe(2); + + process.release(); + await expect(lateStart).rejects.toThrow("retired during the operation"); + expect(delegate.destroyCalls).toBe(3); + }); + + test("fences every Process source and method after retirement", async () => { + const sandbox = await createReadySandbox(); + type GuardedProcess = ReturnType; + type GuardedSession = { + getProcess(id: string): Promise; + listProcesses(): Promise; + startProcess( + command: string, + options?: { onStart(process: GuardedProcess): void }, + ): Promise; + }; + let callbackProcess: GuardedProcess | null = null; + let sessionCallbackProcess: GuardedProcess | null = null; + const handle = sandbox as unknown as { + createSession(options: { id: string }): Promise; + getProcess(id: string): Promise; + listProcesses(): Promise; + startProcess( + command: string, + options?: { onStart(process: GuardedProcess): void }, + ): Promise; + }; + const started = await handle.startProcess("driver", { + onStart: (processHandle) => { + callbackProcess = processHandle; + }, + }); + expect(Object.keys(started).toSorted()).toEqual( + Object.keys(delegate.processHandle()).toSorted(), + ); + const fetched = await handle.getProcess("process-1"); + const [listed] = await handle.listProcesses(); + const session = await handle.createSession({ id: "process-session" }); + const sessionStarted = await session.startProcess("driver", { + onStart: (processHandle) => { + sessionCallbackProcess = processHandle; + }, + }); + const sessionFetched = await session.getProcess("process-1"); + const [sessionListed] = await session.listProcesses(); + if ( + callbackProcess === null || + sessionCallbackProcess === null || + listed === undefined || + sessionListed === undefined + ) { + throw new Error("Every Process source must return a handle."); + } + const processes = [ + started, + fetched, + listed, + callbackProcess, + sessionStarted, + sessionFetched, + sessionListed, + sessionCallbackProcess, + ]; + const methods = [ + ["getLogs", []], + ["getStatus", []], + ["kill", []], + ["waitForExit", []], + ["waitForLog", ["ready"]], + ["waitForPort", [8080]], + ] as const; + + await sandbox.destroyRuntimeSubjectIncarnation(4); + await sandbox.alarm(); + for (const process of processes) { + for (const [method, args] of methods) { + await expect(Reflect.apply(process[method], process, args)).rejects.toThrow("retired"); + } + } + expect(delegate.processActionCalls).toBe(0); + expect(delegate.destroyCalls).toBe(2 + processes.length * methods.length); + }); + + test("leaves DTOs and established streams untouched without reopening them", async () => { + const sandbox = await createReadySandbox(); + type Stream = ReturnType; + const handle = sandbox as unknown as { + createSession(options: { id: string }): Promise<{ execStream(): Promise }>; + exec(): Promise; + }; + const dto = await handle.exec(); + const session = await handle.createSession({ id: "stream-session" }); + const stream = await session.execStream(); + + expect(dto).toBe(delegate.plainDto); + expect(dto.nested).toBe(delegate.plainDto.nested); + expect(delegate.streamOpenCalls).toBe(1); + + await sandbox.destroyRuntimeSubjectIncarnation(4); + await sandbox.alarm(); + await expect(session.execStream()).rejects.toThrow("retired"); + expect(delegate.streamOpenCalls).toBe(1); + await expect(stream.next()).resolves.toEqual({ done: true, value: undefined }); + expect(delegate.streamReadCalls).toBe(1); + }); + + test("re-destroys after a late delegate alarm resumes", async () => { + const sandbox = await createReadySandbox(); + const alarm = createBarrier(); + const started = createBarrier(); + delegate.alarmBarrier = alarm.promise; + delegate.onAlarmStart = started.release; + + const lateAlarm = sandbox.alarm(); + await started.promise; + await sandbox.destroyRuntimeSubjectIncarnation(4); + expect(delegate.destroyCalls).toBe(1); + + alarm.release(); + await lateAlarm; + expect(delegate.destroyCalls).toBe(2); + }); + + test("aborts a wedged retired destroy while preserving its retry alarm", async () => { + const sandbox = await createReadySandbox(); + delegate.destroyBarrier = new Promise(() => {}); + const timeout = spyOn(globalThis, "setTimeout").mockImplementation((( + callback: TimerHandler, + ) => { + if (typeof callback === "function") { + queueMicrotask(callback); + } + return 1; + }) as typeof setTimeout); + + try { + await expect(sandbox.destroyRuntimeSubjectIncarnation(4)).rejects.toThrow( + "durable object aborted", + ); + } finally { + timeout.mockRestore(); + } + + expect(delegate.destroyCalls).toBe(1); + expect(alarmWrites).toBe(2); + expect(abortCalls).toBe(1); + expect(abortRetryAlarm).toBe(true); + }); +}); diff --git a/apps/api/tests/send-agent-session-events.test.ts b/apps/api/tests/send-agent-session-events.test.ts index 589cb054..8c3738ce 100644 --- a/apps/api/tests/send-agent-session-events.test.ts +++ b/apps/api/tests/send-agent-session-events.test.ts @@ -110,6 +110,10 @@ async function insertQueuedRunFixture( input.nowMs, ) .run(); + await database + .prepare("UPDATE session SET last_run_id = ?, status = ? WHERE id = ?") + .bind(input.runId, "RUNNING", PUBLIC_API_TEST_IDS.ownerSession) + .run(); } async function insertSessionFileRecord( @@ -531,6 +535,10 @@ describe("send agent session events", () => { nowMs, ) .run(); + await database + .prepare("UPDATE session SET last_run_id = ?, status = ? WHERE id = ?") + .bind(PUBLIC_API_TEST_IDS.run, "RUNNING", PUBLIC_API_TEST_IDS.ownerSession) + .run(); await database .prepare( ` diff --git a/apps/api/tests/session-event-stream-fold.test.ts b/apps/api/tests/session-event-stream-fold.test.ts index 2523d068..348d6319 100644 --- a/apps/api/tests/session-event-stream-fold.test.ts +++ b/apps/api/tests/session-event-stream-fold.test.ts @@ -2,100 +2,183 @@ import { describe, expect, test } from "bun:test"; import type { RuntimeEventId, SessionRunId } from "@mosoo/id"; -import { foldStreamedSessionEventRows } from "../src/modules/sessions/domain/session-event-stream-fold"; +import { + createMessageStreamLifecycle, + findLeftIncompleteSessionEventStreamKeys, + foldStreamedSessionEventRows, + reduceMessageStreamLifecycle, + resolveSealedMessageStream, +} from "../src/modules/sessions/domain/session-event-stream-fold"; import type { StreamFoldableSessionEventRow } from "../src/modules/sessions/domain/session-event-stream-fold"; const RUN_ID = "run-1" as SessionRunId; +interface TestSessionEventRow extends StreamFoldableSessionEventRow { + process_status: "available" | "error"; +} + function row(input: { content: string; eventType: string; id: string; + processStatus?: TestSessionEventRow["process_status"]; processType?: string; runId?: SessionRunId | null; seq: number; -}): StreamFoldableSessionEventRow { + streamId?: string | null; +}): TestSessionEventRow { return { content_text: input.content, ended_at: input.seq * 1000, event_type: input.eventType, id: input.id as RuntimeEventId, occurred_at: input.seq * 1000, + process_status: input.processStatus ?? "available", process_type: input.processType ?? "agent.message.delta", run_id: input.runId === undefined ? RUN_ID : input.runId, seq: input.seq, + stream_id: input.streamId === undefined ? "stream-1" : input.streamId, tokens: null, }; } describe("session event stream folding", () => { - test("folds a streamed assistant message into one row", () => { - const folded = foldStreamedSessionEventRows( - [ - row({ content: "Message updated.", eventType: "message.started", id: "m-start", seq: 1 }), - row({ content: "你", eventType: "message.delta", id: "m-1", seq: 2 }), - row({ content: "好", eventType: "message.delta", id: "m-2", seq: 3 }), - row({ content: ",世界", eventType: "message.delta", id: "m-3", seq: 4 }), - row({ content: "Message updated.", eventType: "message.completed", id: "m-end", seq: 5 }), - ], - { flushOpenStreams: true }, - ); - - expect(folded.openStreamRows).toEqual([]); - expect(folded.rows).toHaveLength(1); - expect(folded.rows[0]).toMatchObject({ - content_text: "你好,世界", - event_type: "message.completed", - id: "m-end", - occurred_at: 1000, - seq: 1, - }); + test.each([ + [[], false, false], + [["message.added"], true, false], + [["message.added", "message.completed"], true, true], + [["message.added", "message.completed", "message.delta"], true, false], + [["message.added", "message.completed", "message.started"], false, false], + [["message.added", "message.cancelled", "message.completed"], false, false], + [["message.added", "message.failed", "message.completed"], false, false], + [["message.started", "message.completed"], false, false], + ] as const)( + "reduces message lifecycle %j to authoritative=%s sealed=%s", + (eventTypes, authoritative, sealed) => { + let state = createMessageStreamLifecycle(); + for (const eventType of eventTypes) { + state = reduceMessageStreamLifecycle(state, eventType); + } + expect(state).toEqual({ authoritative, sealed }); + }, + ); + + test("rejects a streamed row without identity", () => { + expect(() => + foldStreamedSessionEventRows([ + row({ + content: "orphan", + eventType: "message.delta", + id: "orphan", + seq: 1, + streamId: null, + }), + ]), + ).toThrow("has no stream identity"); }); - test("appends repeated delta fragments without deduplicating prefixes", () => { - const folded = foldStreamedSessionEventRows( - [ - row({ content: "ha", eventType: "message.delta", id: "m-1", seq: 1 }), - row({ content: "ha", eventType: "message.delta", id: "m-2", seq: 2 }), - row({ content: "h", eventType: "message.delta", id: "m-3", seq: 3 }), - row({ content: "Message updated.", eventType: "message.completed", id: "m-end", seq: 4 }), - ], - { flushOpenStreams: true }, - ); - - expect(folded.rows.map((entry) => entry.content_text)).toEqual(["hahah"]); + test.each([ + ["message.completed", "available"], + ["message.cancelled", "available"], + ["message.failed", "error"], + ] as const)("folds a streamed assistant message closed by %s", (eventType, processStatus) => { + const folded = foldStreamedSessionEventRows([ + row({ content: "Partial ", eventType: "message.delta", id: "m-1", seq: 1 }), + row({ content: "answer", eventType: "message.delta", id: "m-2", seq: 2 }), + row({ + content: "Message updated.", + eventType, + id: "m-end", + processStatus, + seq: 3, + }), + ]); + + expect(folded).toEqual([ + expect.objectContaining({ + content_text: "Partial answer", + event_type: eventType, + id: "m-end", + process_status: processStatus, + seq: 1, + }), + ]); }); - test("prefers a closing snapshot that extends the streamed prefix", () => { - const folded = foldStreamedSessionEventRows( - [ - row({ content: "Final ans", eventType: "message.delta", id: "m-1", seq: 1 }), - row({ content: "Final answer.", eventType: "message.added", id: "m-added", seq: 2 }), - ], - { flushOpenStreams: true }, - ); - - expect(folded.rows).toHaveLength(1); - expect(folded.rows[0]).toMatchObject({ - content_text: "Final answer.", - id: "m-added", - }); + test("folds a cancelled thought stream", () => { + const folded = foldStreamedSessionEventRows([ + row({ + content: "Inspect", + eventType: "thought.delta", + id: "th-1", + processType: "agent.thinking.delta", + seq: 1, + }), + row({ + content: "Agent thinking updated.", + eventType: "thought.cancelled", + id: "th-end", + processType: "agent.thinking.delta", + seq: 2, + }), + ]); + + expect(folded).toEqual([ + expect.objectContaining({ content_text: "Inspect", event_type: "thought.cancelled" }), + ]); + }); + + test("appends deltas after an authoritative message snapshot", () => { + const folded = foldStreamedSessionEventRows([ + row({ content: "Final ", eventType: "message.added", id: "m-added", seq: 1 }), + row({ content: "answer.", eventType: "message.delta", id: "m-delta", seq: 2 }), + row({ content: "Message updated.", eventType: "message.completed", id: "m-end", seq: 3 }), + ]); + + expect(folded).toEqual([ + expect.objectContaining({ content_text: "Final answer.", id: "m-end" }), + ]); }); - test("keeps standalone message.added rows untouched", () => { - const first = row({ content: "First message.", eventType: "message.added", id: "m-1", seq: 1 }); + test("appends repeated delta fragments without prefix guessing", () => { + const folded = foldStreamedSessionEventRows([ + row({ content: "ha", eventType: "message.delta", id: "m-1", seq: 1 }), + row({ content: "ha", eventType: "message.delta", id: "m-2", seq: 2 }), + row({ content: "h", eventType: "message.delta", id: "m-3", seq: 3 }), + row({ content: "Message updated.", eventType: "message.completed", id: "m-end", seq: 4 }), + ]); + + expect(folded.map((entry) => entry.content_text)).toEqual(["hahah"]); + }); + + test("keeps standalone snapshots for distinct identities", () => { + const first = row({ + content: "First message.", + eventType: "message.added", + id: "m-1", + seq: 1, + streamId: "message-1", + }); const second = row({ content: "Second message.", eventType: "message.added", id: "m-2", seq: 2, + streamId: "message-2", }); - const folded = foldStreamedSessionEventRows([first, second], { flushOpenStreams: true }); - expect(folded.rows).toEqual([first, second]); + expect(foldStreamedSessionEventRows([first, second])).toEqual([first, second]); + }); + + test("treats an authoritative snapshot as a complete reverse-scan boundary", () => { + expect( + findLeftIncompleteSessionEventStreamKeys([ + row({ content: "Complete snapshot", eventType: "message.added", id: "m-1", seq: 1 }), + ]), + ).toEqual(new Set()); }); - test("keeps interleaved non-stream rows and folds around them", () => { + test("keeps timeline order around interleaved non-stream rows", () => { const toolRow = row({ content: "Read file", eventType: "tool.call.updated", @@ -103,57 +186,97 @@ describe("session event stream folding", () => { processType: "tool.use.started", seq: 3, }); - const folded = foldStreamedSessionEventRows( - [ - row({ content: "部分", eventType: "message.delta", id: "m-1", seq: 1 }), - row({ content: "回答", eventType: "message.delta", id: "m-2", seq: 2 }), - toolRow, - row({ content: "。", eventType: "message.delta", id: "m-3", seq: 4 }), - row({ content: "Message updated.", eventType: "message.completed", id: "m-end", seq: 5 }), - ], - { flushOpenStreams: true }, - ); - - expect(folded.rows).toHaveLength(2); - expect(folded.rows[0]).toEqual(toolRow); - expect(folded.rows[1]).toMatchObject({ content_text: "部分回答。", seq: 1 }); + const folded = foldStreamedSessionEventRows([ + row({ content: "部分", eventType: "message.delta", id: "m-1", seq: 1 }), + row({ content: "回答", eventType: "message.delta", id: "m-2", seq: 2 }), + toolRow, + row({ content: "。", eventType: "message.delta", id: "m-3", seq: 4 }), + row({ content: "Message updated.", eventType: "message.completed", id: "m-end", seq: 5 }), + ]); + + expect(folded).toHaveLength(2); + expect(folded[0]).toMatchObject({ content_text: "部分回答。", seq: 1 }); + expect(folded[1]).toEqual(toolRow); }); - test("folds message and thought streams independently", () => { - const folded = foldStreamedSessionEventRows( - [ - row({ - content: "思考", - eventType: "thought.delta", - id: "th-1", - processType: "agent.thinking.delta", - seq: 1, - }), - row({ content: "回答", eventType: "message.delta", id: "m-1", seq: 2 }), - row({ - content: "中", - eventType: "thought.delta", - id: "th-2", - processType: "agent.thinking.delta", - seq: 3, - }), - row({ - content: "Agent thinking updated.", - eventType: "thought.completed", - id: "th-end", - processType: "agent.thinking.delta", - seq: 4, - }), - row({ content: "完毕", eventType: "message.delta", id: "m-2", seq: 5 }), - row({ content: "Message updated.", eventType: "message.completed", id: "m-end", seq: 6 }), - ], - { flushOpenStreams: true }, - ); + test("separates message and thought streams with the same identity", () => { + const folded = foldStreamedSessionEventRows([ + row({ + content: "思考", + eventType: "thought.delta", + id: "th-1", + processType: "agent.thinking.delta", + seq: 1, + }), + row({ content: "回答", eventType: "message.delta", id: "m-1", seq: 2 }), + row({ + content: "Agent thinking updated.", + eventType: "thought.completed", + id: "th-end", + processType: "agent.thinking.delta", + seq: 3, + }), + row({ content: "Message updated.", eventType: "message.completed", id: "m-end", seq: 4 }), + ]); - expect(folded.rows.map((entry) => entry.content_text)).toEqual(["思考中", "回答完毕"]); + expect(folded.map((entry) => entry.content_text)).toEqual(["思考", "回答"]); }); - test("flushes interrupted streams when the run reaches a terminal event", () => { + test("separates user and assistant messages with the same stream identity", () => { + const folded = foldStreamedSessionEventRows([ + row({ + content: "User", + eventType: "message.delta", + id: "user-delta", + processType: "user.message", + seq: 1, + }), + row({ content: "Assistant", eventType: "message.delta", id: "agent-delta", seq: 2 }), + row({ + content: "Message updated.", + eventType: "message.completed", + id: "user-end", + processType: "user.message", + seq: 3, + }), + row({ + content: "Message updated.", + eventType: "message.completed", + id: "agent-end", + seq: 4, + }), + ]); + + expect(folded.map((entry) => [entry.process_type, entry.content_text])).toEqual([ + ["user.message", "User"], + ["agent.message.delta", "Assistant"], + ]); + }); + + test("separates the same stream identity across runs", () => { + const folded = foldStreamedSessionEventRows([ + row({ content: "run one", eventType: "message.delta", id: "r1-delta", seq: 1 }), + row({ + content: "run two", + eventType: "message.delta", + id: "r2-delta", + runId: "run-2" as SessionRunId, + seq: 2, + }), + row({ + content: "Message updated.", + eventType: "message.completed", + id: "r2-end", + runId: "run-2" as SessionRunId, + seq: 3, + }), + row({ content: "Message updated.", eventType: "message.completed", id: "r1-end", seq: 4 }), + ]); + + expect(folded.map((entry) => entry.content_text)).toEqual(["run one", "run two"]); + }); + + test("flushes interrupted streams when their run terminates", () => { const failedRow = row({ content: "Run failed.", eventType: "run.failed", @@ -161,215 +284,227 @@ describe("session event stream folding", () => { processType: "run.failed", seq: 3, }); - const folded = foldStreamedSessionEventRows( - [ - row({ content: "写到一", eventType: "message.delta", id: "m-1", seq: 1 }), - row({ content: "半", eventType: "message.delta", id: "m-2", seq: 2 }), - failedRow, - ], - { flushOpenStreams: false }, - ); - - expect(folded.openStreamRows).toEqual([]); - expect(folded.rows.map((entry) => entry.content_text)).toEqual(["写到一半", "Run failed."]); - }); + const folded = foldStreamedSessionEventRows([ + row({ content: "写到一", eventType: "message.delta", id: "m-1", seq: 1 }), + row({ content: "半", eventType: "message.delta", id: "m-2", seq: 2 }), + failedRow, + ]); - test("closes the previous stream when a new one starts without a completed row", () => { - const folded = foldStreamedSessionEventRows( - [ - row({ content: "第一条", eventType: "message.delta", id: "m-1", seq: 1 }), - row({ content: "Message updated.", eventType: "message.started", id: "m-start", seq: 2 }), - row({ content: "第二条", eventType: "message.delta", id: "m-2", seq: 3 }), - row({ content: "Message updated.", eventType: "message.completed", id: "m-end", seq: 4 }), - ], - { flushOpenStreams: true }, - ); - - expect(folded.rows.map((entry) => entry.content_text)).toEqual(["第一条", "第二条"]); + expect(folded.map((entry) => entry.content_text)).toEqual(["写到一半", "Run failed."]); }); - test("withholds open streams so callers can carry them across reads", () => { - const fragments = [ - row({ content: "流式", eventType: "message.delta", id: "m-1", seq: 1 }), - row({ content: "输出", eventType: "message.delta", id: "m-2", seq: 2 }), - ]; - const firstFold = foldStreamedSessionEventRows(fragments, { flushOpenStreams: false }); - - expect(firstFold.rows).toEqual([]); - expect(firstFold.openStreamRows).toEqual(fragments); - - const secondFold = foldStreamedSessionEventRows( - [ - ...firstFold.openStreamRows, - row({ content: "完成", eventType: "message.delta", id: "m-3", seq: 3 }), - row({ content: "Message updated.", eventType: "message.completed", id: "m-end", seq: 4 }), - ], - { flushOpenStreams: false }, - ); - - expect(secondFold.openStreamRows).toEqual([]); - expect(secondFold.rows).toHaveLength(1); - expect(secondFold.rows[0]).toMatchObject({ content_text: "流式输出完成", id: "m-end" }); - }); + test("reconciles lossless message rows persisted after a repaired run terminal", () => { + const folded = foldStreamedSessionEventRows([ + row({ content: "Message updated.", eventType: "message.started", id: "m-start", seq: 1 }), + row({ content: "Draft", eventType: "message.delta", id: "m-delta", seq: 2 }), + row({ content: "Run failed.", eventType: "run.failed", id: "run-failed", seq: 3 }), + row({ content: "Final ", eventType: "message.added", id: "m-added", seq: 4 }), + row({ content: "world", eventType: "message.delta", id: "m-late", seq: 5 }), + ]); - test("supersedes a closed stream with its trailing snapshot instead of duplicating it", () => { - // Streamed replies persist message.completed (at message_stop) before the - // aggregated assistant snapshot arrives, so the snapshot lands after its - // stream already closed and must replace the folded row, not repeat it. - const folded = foldStreamedSessionEventRows( - [ - row({ content: "Message updated.", eventType: "message.started", id: "m-start", seq: 1 }), - row({ content: "P", eventType: "message.delta", id: "m-1", seq: 2 }), - row({ - content: "ong. What would you like to work on?", - eventType: "message.delta", - id: "m-2", - seq: 3, - }), - row({ content: "Message updated.", eventType: "message.completed", id: "m-end", seq: 4 }), - row({ - content: "Pong. What would you like to work on?", - eventType: "message.added", - id: "m-added", - seq: 5, - }), - ], - { flushOpenStreams: true }, - ); - - expect(folded.rows).toHaveLength(1); - expect(folded.rows[0]).toMatchObject({ - content_text: "Pong. What would you like to work on?", - event_type: "message.added", - id: "m-added", - seq: 1, - }); + expect(folded.map((entry) => entry.content_text)).toEqual(["Final world", "Run failed."]); }); - test("collapses a fractured stream whose snapshot matches the concatenated fragments", () => { - // YEF-884: a dropped message_start fractures one reply into per-fragment - // messages, so the rows arrive as started/delta pairs per fragment plus a - // final full snapshot. The snapshot equals the fragment concatenation and - // must fold everything into a single timeline entry. - const folded = foldStreamedSessionEventRows( - [ - row({ content: "Message updated.", eventType: "message.started", id: "s-1", seq: 1 }), - row({ content: "P", eventType: "message.delta", id: "m-1", seq: 2 }), - row({ content: "Message updated.", eventType: "message.started", id: "s-2", seq: 3 }), - row({ - content: "ong. What would you like to work on?", - eventType: "message.delta", - id: "m-2", - seq: 4, - }), - row({ content: "Message updated.", eventType: "message.started", id: "s-3", seq: 5 }), - row({ - content: "Pong. What would you like to work on?", - eventType: "message.delta", - id: "m-3", - seq: 6, - }), - row({ - content: "Pong. What would you like to work on?", - eventType: "message.added", - id: "m-added", - seq: 7, - }), - row({ content: "Message updated.", eventType: "message.completed", id: "c-3", seq: 8 }), - row({ content: "Message updated.", eventType: "message.completed", id: "c-1", seq: 9 }), - row({ content: "Message updated.", eventType: "message.completed", id: "c-2", seq: 10 }), - row({ - content: "run-1", - eventType: "run.completed", - id: "r-done", - processType: "run.completed", - seq: 11, - }), - ], - { flushOpenStreams: true }, - ); + test("uses a trailing authoritative snapshot without replacing the terminal row", () => { + const folded = foldStreamedSessionEventRows([ + row({ content: "Message updated.", eventType: "message.started", id: "m-start", seq: 1 }), + row({ content: "corrupt preview", eventType: "message.delta", id: "m-delta", seq: 2 }), + row({ content: "Message updated.", eventType: "message.completed", id: "m-end", seq: 3 }), + row({ + content: "Authoritative answer.", + eventType: "message.added", + id: "m-added", + seq: 4, + }), + ]); - expect(folded.rows.map((entry) => entry.content_text)).toEqual([ - "Pong. What would you like to work on?", - "run-1", + expect(folded).toEqual([ + expect.objectContaining({ + content_text: "Authoritative answer.", + event_type: "message.completed", + id: "m-end", + seq: 1, + }), ]); - expect(folded.rows[0]).toMatchObject({ id: "m-added", seq: 1 }); }); - test("supersedes a truncated stream with the longer prefix-matching snapshot", () => { - const folded = foldStreamedSessionEventRows( - [ - row({ content: "Final ans", eventType: "message.delta", id: "m-1", seq: 1 }), - row({ content: "Message updated.", eventType: "message.completed", id: "m-end", seq: 2 }), - row({ content: "Final answer.", eventType: "message.added", id: "m-added", seq: 3 }), - ], - { flushOpenStreams: true }, - ); - - expect(folded.rows).toHaveLength(1); - expect(folded.rows[0]).toMatchObject({ - content_text: "Final answer.", - id: "m-added", - seq: 1, + test("folds interleaved message streams by identity", () => { + const folded = foldStreamedSessionEventRows([ + row({ content: "A1", eventType: "message.delta", id: "a-1", seq: 1, streamId: "a" }), + row({ content: "B1", eventType: "message.delta", id: "b-1", seq: 2, streamId: "b" }), + row({ content: "A2", eventType: "message.delta", id: "a-2", seq: 3, streamId: "a" }), + row({ + content: "Message updated.", + eventType: "message.completed", + id: "b-end", + seq: 4, + streamId: "b", + }), + row({ + content: "Message updated.", + eventType: "message.completed", + id: "a-end", + seq: 5, + streamId: "a", + }), + ]); + + expect( + Object.fromEntries(folded.map((entry) => [entry.stream_id, entry.content_text])), + ).toEqual({ + a: "A1A2", + b: "B1", }); }); - test("keeps a snapshot that does not extend the closed stream as its own message", () => { - const folded = foldStreamedSessionEventRows( - [ - row({ content: "第一条进度", eventType: "message.delta", id: "m-1", seq: 1 }), - row({ content: "Message updated.", eventType: "message.completed", id: "m-end", seq: 2 }), - row({ content: "另一条最终回复", eventType: "message.added", id: "m-added", seq: 3 }), - ], - { flushOpenStreams: true }, - ); - - expect(folded.rows.map((entry) => entry.content_text)).toEqual([ - "第一条进度", - "另一条最终回复", + test("keeps a snapshot for a different identity as its own message", () => { + const folded = foldStreamedSessionEventRows([ + row({ + content: "第一条进度", + eventType: "message.delta", + id: "m-1", + seq: 1, + streamId: "message-1", + }), + row({ + content: "Message updated.", + eventType: "message.completed", + id: "m-end", + seq: 2, + streamId: "message-1", + }), + row({ + content: "另一条最终回复", + eventType: "message.added", + id: "m-added", + seq: 3, + streamId: "message-2", + }), ]); + + expect(folded.map((entry) => entry.content_text)).toEqual(["第一条进度", "另一条最终回复"]); }); - test("consumes fragments per snapshot across a multi-message turn", () => { - const folded = foldStreamedSessionEventRows( - [ - row({ content: "进度说明", eventType: "message.delta", id: "m-1", seq: 1 }), - row({ content: "Message updated.", eventType: "message.completed", id: "c-1", seq: 2 }), - row({ content: "进度说明", eventType: "message.added", id: "a-1", seq: 3 }), - row({ content: "最终回复", eventType: "message.delta", id: "m-2", seq: 4 }), - row({ content: "Message updated.", eventType: "message.completed", id: "c-2", seq: 5 }), - row({ content: "最终回复", eventType: "message.added", id: "a-2", seq: 6 }), - ], - { flushOpenStreams: true }, - ); - - expect(folded.rows.map((entry) => entry.content_text)).toEqual(["进度说明", "最终回复"]); - expect(folded.rows.map((entry) => entry.id)).toEqual(["a-1", "a-2"]); + test("associates trailing snapshots with their stream in a multi-message turn", () => { + const folded = foldStreamedSessionEventRows([ + row({ content: "进度", eventType: "message.delta", id: "m-1", seq: 1, streamId: "one" }), + row({ + content: "Message updated.", + eventType: "message.completed", + id: "c-1", + seq: 2, + streamId: "one", + }), + row({ content: "进度说明", eventType: "message.added", id: "a-1", seq: 3, streamId: "one" }), + row({ content: "最终", eventType: "message.delta", id: "m-2", seq: 4, streamId: "two" }), + row({ + content: "Message updated.", + eventType: "message.completed", + id: "c-2", + seq: 5, + streamId: "two", + }), + row({ content: "最终回复", eventType: "message.added", id: "a-2", seq: 6, streamId: "two" }), + ]); + + expect(folded.map((entry) => entry.content_text)).toEqual(["进度说明", "最终回复"]); + expect(folded.map((entry) => entry.id)).toEqual(["c-1", "c-2"]); }); - test("drops streams that never carried text", () => { - const folded = foldStreamedSessionEventRows( - [ - row({ content: "Message updated.", eventType: "message.started", id: "m-start", seq: 1 }), - row({ content: "Message updated.", eventType: "message.completed", id: "m-end", seq: 2 }), - ], - { flushOpenStreams: true }, - ); + test("keeps a terminal stream row with empty content", () => { + const folded = foldStreamedSessionEventRows([ + row({ content: "Message updated.", eventType: "message.started", id: "m-start", seq: 1 }), + row({ content: "Message updated.", eventType: "message.completed", id: "m-end", seq: 2 }), + ]); - expect(folded.rows).toEqual([]); + expect(folded).toEqual([ + expect.objectContaining({ content_text: "", event_type: "message.completed", id: "m-end" }), + ]); }); test("re-folding folded rows is a no-op", () => { - const folded = foldStreamedSessionEventRows( - [ - row({ content: "你", eventType: "message.delta", id: "m-1", seq: 1 }), - row({ content: "好", eventType: "message.delta", id: "m-2", seq: 2 }), - row({ content: "Message updated.", eventType: "message.completed", id: "m-end", seq: 3 }), - ], - { flushOpenStreams: true }, - ); - const refolded = foldStreamedSessionEventRows(folded.rows, { flushOpenStreams: true }); - - expect(refolded.rows).toEqual(folded.rows); + const folded = foldStreamedSessionEventRows([ + row({ content: "你", eventType: "message.delta", id: "m-1", seq: 1 }), + row({ content: "好", eventType: "message.delta", id: "m-2", seq: 2 }), + row({ content: "Message updated.", eventType: "message.completed", id: "m-end", seq: 3 }), + ]); + + expect(foldStreamedSessionEventRows(folded)).toEqual(folded); + }); + + test("resolves only the latest sealed authoritative message snapshot", () => { + const firstCompletion = [ + row({ content: "Draft", eventType: "message.added", id: "m-draft", seq: 1 }), + row({ content: "Message updated.", eventType: "message.completed", id: "m-end-1", seq: 2 }), + ]; + const replacement = [ + ...firstCompletion, + row({ content: "Final ", eventType: "message.added", id: "m-added", seq: 3 }), + row({ content: "answer", eventType: "message.delta", id: "m-final", seq: 4 }), + ]; + + expect(resolveSealedMessageStream(firstCompletion)).toEqual({ text: "Draft" }); + expect(resolveSealedMessageStream(replacement)).toBeNull(); + expect( + resolveSealedMessageStream([ + ...replacement, + row({ + content: "Message updated.", + eventType: "message.completed", + id: "m-end-2", + seq: 5, + }), + ]), + ).toEqual({ text: "Final answer" }); + }); + + test("does not treat failed, cancelled, or restarted messages as sealed final output", () => { + for (const eventType of ["message.failed", "message.cancelled"] as const) { + expect( + resolveSealedMessageStream([ + row({ content: "partial", eventType: "message.delta", id: "m-delta", seq: 1 }), + row({ content: "Message updated.", eventType, id: "m-terminal", seq: 2 }), + ]), + ).toBeNull(); + } + + expect( + resolveSealedMessageStream([ + row({ content: "old", eventType: "message.added", id: "m-delta", seq: 1 }), + row({ + content: "Message updated.", + eventType: "message.completed", + id: "m-terminal", + seq: 2, + }), + row({ content: "Message updated.", eventType: "message.started", id: "m-restart", seq: 3 }), + ]), + ).toBeNull(); + + expect( + resolveSealedMessageStream([ + row({ content: "Message updated.", eventType: "message.started", id: "m-start", seq: 1 }), + row({ content: "best effort", eventType: "message.delta", id: "m-delta", seq: 2 }), + row({ + content: "Message updated.", + eventType: "message.completed", + id: "m-terminal", + seq: 3, + }), + ]), + ).toBeNull(); + + expect( + resolveSealedMessageStream([ + row({ content: "old", eventType: "message.added", id: "m-added", seq: 1 }), + row({ content: "Message updated.", eventType: "message.failed", id: "m-failed", seq: 2 }), + row({ + content: "Message updated.", + eventType: "message.completed", + id: "m-terminal", + seq: 3, + }), + ]), + ).toBeNull(); }); }); diff --git a/apps/api/tests/session-lifecycle-mutation.test.ts b/apps/api/tests/session-lifecycle-mutation.test.ts index 5381f65e..0725bdd9 100644 --- a/apps/api/tests/session-lifecycle-mutation.test.ts +++ b/apps/api/tests/session-lifecycle-mutation.test.ts @@ -3,6 +3,8 @@ import { describe, expect, test } from "bun:test"; import type { RuntimeOperationId } from "@mosoo/id"; import type { AuthenticatedViewer } from "../src/modules/auth/application/viewer-auth.service"; +import type { SandboxHandle } from "../src/modules/runtime/infrastructure/sandbox-handles"; +import { setSessionRunStatus } from "../src/modules/runtime/infrastructure/session-runs/session-run-store.repository"; import { deleteSessionCascade, repairStaleSessionDeleteCleanups, @@ -10,16 +12,20 @@ import { import { archiveAgentSession, deleteAgentSession, + repairStaleSessionArchiveCleanups, unarchiveAgentSession, } from "../src/modules/sessions/application/session-lifecycle-mutation.service"; import type { ApiBindings } from "../src/platform/cloudflare/worker-types"; +import { applyDrizzleMigration } from "./helpers/drizzle-migrations"; import { PUBLIC_API_TEST_IDS, createPublicHttpContractDatabase, createPublicHttpTestBindings, + insertActiveSandboxSessionFixture, insertOwnerSession, insertNonOwnerSession, } from "./helpers/public-api-http-test-fixture"; +import { SqliteD1Database } from "./helpers/sqlite-d1"; const OWNER_VIEWER: AuthenticatedViewer = { email: "owner@example.com", @@ -64,11 +70,17 @@ function withDriverConnection(bindings: ApiBindings, paths: string[]): ApiBindin }; } -function createSessionLifecycleBinding(paths: string[], options: { destroyError?: Error } = {}) { +function createSessionLifecycleBinding( + paths: string[], + options: { closeError?: Error; destroyError?: Error } = {}, +) { return { get: () => ({ closeViewers: async (_sessionId: string, reason: string) => { paths.push(`close:${reason}`); + if (options.closeError !== undefined) { + throw options.closeError; + } }, destroy: async (_sessionId: string, reason: string) => { paths.push(`destroy:${reason}`); @@ -87,108 +99,65 @@ function createSessionLifecycleBinding(paths: string[], options: { destroyError? function withSessionLifecycleBinding( bindings: ApiBindings, paths: string[] = [], - options: { destroyError?: Error } = {}, + options: { closeError?: Error; destroyError?: Error } = {}, ): ApiBindings { return { ...bindings, + runtimeSubjectHandleFactory: () => createCleanupSandboxHandle(), Session: createSessionLifecycleBinding(paths, options) as ApiBindings["Session"], }; } -async function ensureRuntimeLifecycleTables(database: D1Database): Promise { - await database - .prepare( - ` - CREATE TABLE IF NOT EXISTS sandbox ( - id text PRIMARY KEY NOT NULL, - inactive_deadline_at integer, - kind text NOT NULL, - status text DEFAULT 'active' NOT NULL, - updated_at integer DEFAULT 1 NOT NULL - ) - `, - ) - .run(); - await database - .prepare( - ` - CREATE TABLE IF NOT EXISTS sandbox_session ( - cloudflare_session_id text NOT NULL, - created_at integer NOT NULL, - cwd text NOT NULL, - origin_json text NOT NULL, - sandbox_id text NOT NULL, - session_id text PRIMARY KEY NOT NULL, - status text NOT NULL, - updated_at integer NOT NULL - ) - `, - ) - .run(); - await database - .prepare( - ` - CREATE TABLE IF NOT EXISTS sandbox_backup ( - id text PRIMARY KEY NOT NULL, - dir text NOT NULL, - sandbox_id text NOT NULL, - status text NOT NULL, - created_at integer NOT NULL, - updated_at integer NOT NULL - ) - `, - ) - .run(); +function createCleanupSandboxHandle(): SandboxHandle { + const unavailable = async (): Promise => { + throw new Error("Unexpected sandbox test method call."); + }; + + return { + configureNetworkConstraints: unavailable, + createBackup: unavailable, + createSession: unavailable, + deleteSession: async (sessionId) => ({ + sessionId, + success: true, + timestamp: new Date(0).toISOString(), + }), + destroy: unavailable, + exec: unavailable, + getSession: unavailable, + mkdir: unavailable, + mountBucket: unavailable, + readFile: unavailable, + restoreBackup: unavailable, + setKeepAlive: unavailable, + startProcess: unavailable, + terminal: unavailable, + unmountBucket: unavailable, + watch: unavailable, + writeFile: unavailable, + wsConnect: unavailable, + }; } async function insertSandboxSession( - database: D1Database, + database: SqliteD1Database, sessionId: string = PUBLIC_API_TEST_IDS.nonOwnerSession, ): Promise { - await ensureRuntimeLifecycleTables(database); - await database - .prepare( - ` - INSERT INTO sandbox ( - id, - kind, - subject_kind, - subject_id, - status, - created_at, - updated_at - ) - VALUES (?, ?, ?, ?, ?, ?, ?) - `, - ) - .bind(PUBLIC_API_TEST_IDS.sandbox, "pet", "agent", PUBLIC_API_TEST_IDS.agent, "active", 1, 1) - .run(); + const ownerAccountId = + sessionId === PUBLIC_API_TEST_IDS.ownerSession + ? PUBLIC_API_TEST_IDS.ownerAccount + : PUBLIC_API_TEST_IDS.nonOwnerAccount; + await insertActiveSandboxSessionFixture(database, { + cwd: "/workspace/session-cwd", + ownerAccountId, + sandboxId: PUBLIC_API_TEST_IDS.sandbox, + sandboxSessionId: "01J0000000000000000000000S", + sessionId, + timestampMs: 1, + }); await database - .prepare( - ` - INSERT INTO sandbox_session ( - cloudflare_session_id, - created_at, - cwd, - origin_json, - sandbox_id, - session_id, - status, - updated_at - ) - VALUES (?, ?, ?, ?, ?, ?, ?, ?) - `, - ) - .bind( - "01J0000000000000000000000S", - 1, - "session-cwd", - "{}", - PUBLIC_API_TEST_IDS.sandbox, - sessionId, - "closed", - 1, - ) + .prepare("UPDATE sandbox_session SET status = 'closed' WHERE session_id = ?") + .bind(sessionId) .run(); } @@ -260,6 +229,7 @@ async function insertDriverInstance( INSERT INTO driver_instance ( id, sandbox_id, + sandbox_incarnation, sandbox_session_id, runtime, protocol, @@ -272,12 +242,13 @@ async function insertDriverInstance( created_at, updated_at ) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) `, ) .bind( input.driverId, PUBLIC_API_TEST_IDS.sandbox, + 1, input.sandboxSessionId, "cloudflare-container", "driver-ws", @@ -357,15 +328,286 @@ async function insertSessionFileForCleanup(database: D1Database): Promise } describe("session lifecycle mutations", () => { + test("cleanup migration preserves data without enqueueing external cleanup", async () => { + const database = new SqliteD1Database(); + database.execute(` + CREATE TABLE session ( + id text PRIMARY KEY, + archived_at integer, + status text NOT NULL, + status_operation_id text, + status_seq integer NOT NULL, + updated_at integer NOT NULL + ); + CREATE TABLE child ( + session_id text REFERENCES session(id) ON DELETE CASCADE + ); + INSERT INTO session VALUES ('session-1', NULL, 'IDLE', NULL, 0, 1); + INSERT INTO session VALUES ('legacy-delete', 2, 'TERMINATED', 'delete-op', 4, 2); + INSERT INTO session VALUES ('legacy-archive', 3, 'IDLE', NULL, 7, 3); + INSERT INTO child VALUES ('session-1'); + `); + applyDrizzleMigration(database, "0015_session-cleanup-operation"); + + expect(await database.prepare("SELECT COUNT(*) AS count FROM child").first()).toEqual({ + count: 1, + }); + expect( + await database + .prepare( + "SELECT cleanup_operation_kind, status, status_operation_id, status_seq FROM session WHERE id = 'legacy-delete'", + ) + .first(), + ).toEqual({ + cleanup_operation_kind: null, + status: "TERMINATED", + status_operation_id: "delete-op", + status_seq: 4, + }); + expect( + await database + .prepare( + "SELECT cleanup_operation_kind, status, status_operation_id, status_seq FROM session WHERE id = 'legacy-archive'", + ) + .first(), + ).toEqual({ + cleanup_operation_kind: null, + status: "IDLE", + status_operation_id: null, + status_seq: 7, + }); + await expect( + database + .prepare("UPDATE session SET cleanup_operation_kind = 'archive' WHERE id = 'session-1'") + .run(), + ).rejects.toThrow("session_cleanup_operation_kind_check"); + await expect( + database + .prepare( + `UPDATE session + SET runtime_provisioning_operation_id = '01J0000000000000000000000R', + runtime_provisioning_sandbox_id = '01J0000000000000000000000S' + WHERE id = 'session-1'`, + ) + .run(), + ).rejects.toThrow("session_runtime_provisioning_lease_check"); + await expect( + database + .prepare( + `UPDATE session + SET runtime_provisioning_operation_id = '01J0000000000000000000000R', + runtime_provisioning_sandbox_id = '01J0000000000000000000000S', + runtime_provisioning_heartbeat_at = 'not-a-timestamp' + WHERE id = 'session-1'`, + ) + .run(), + ).rejects.toThrow("session_runtime_provisioning_lease_check"); + await database + .prepare( + `UPDATE session + SET archived_at = 1, + cleanup_operation_kind = 'archive', + status = 'RESCHEDULING', + status_operation_id = 'operation-1' + WHERE id = 'session-1'`, + ) + .run(); + await database + .prepare( + `UPDATE session + SET cleanup_operation_kind = 'archive', + status = 'IDLE', + status_operation_id = NULL + WHERE id = 'session-1'`, + ) + .run(); + }); + + test("maintenance adopts cleanup claims written by an old worker after migration", async () => { + const archiveDatabase = await createPublicHttpContractDatabase(); + await insertOwnerSession(archiveDatabase); + await archiveDatabase + .prepare( + `UPDATE session + SET archived_at = 1, + cleanup_operation_kind = NULL, + status = 'RESCHEDULING', + status_operation_id = ?, + updated_at = 1 + WHERE id = ?`, + ) + .bind(PUBLIC_API_TEST_IDS.operation, PUBLIC_API_TEST_IDS.ownerSession) + .run(); + + expect( + await repairStaleSessionArchiveCleanups( + withSessionLifecycleBinding(createPublicHttpTestBindings(archiveDatabase) as ApiBindings), + { limit: 10, staleUpdatedAtLte: 1 }, + ), + ).toBe(1); + expect( + await archiveDatabase + .prepare( + "SELECT cleanup_operation_kind, status, status_operation_id FROM session WHERE id = ?", + ) + .bind(PUBLIC_API_TEST_IDS.ownerSession) + .first(), + ).toEqual({ + cleanup_operation_kind: "archive", + status: "IDLE", + status_operation_id: null, + }); + + const deleteDatabase = await createPublicHttpContractDatabase(); + await insertNonOwnerSession(deleteDatabase); + await deleteDatabase + .prepare( + `UPDATE session + SET archived_at = 1, + cleanup_operation_kind = NULL, + status = 'TERMINATED', + status_operation_id = ?, + updated_at = 1 + WHERE id = ?`, + ) + .bind(PUBLIC_API_TEST_IDS.operation, PUBLIC_API_TEST_IDS.nonOwnerSession) + .run(); + + expect( + await repairStaleSessionDeleteCleanups( + withSessionLifecycleBinding(createPublicHttpTestBindings(deleteDatabase) as ApiBindings), + { limit: 10, staleUpdatedAtLte: 1 }, + ), + ).toBe(1); + expect( + await deleteDatabase + .prepare("SELECT id FROM session WHERE id = ?") + .bind(PUBLIC_API_TEST_IDS.nonOwnerSession) + .first(), + ).toBeNull(); + }); + + test("archive and delete cannot pass an active runtime provisioning fence", async () => { + for (const action of ["archive", "delete"] as const) { + const database = await createPublicHttpContractDatabase(); + await insertOwnerSession(database); + await database + .prepare( + `UPDATE session + SET runtime_provisioning_heartbeat_at = 1, + runtime_provisioning_operation_id = ?, + runtime_provisioning_sandbox_id = ? + WHERE id = ?`, + ) + .bind( + PUBLIC_API_TEST_IDS.operation, + PUBLIC_API_TEST_IDS.sandbox, + PUBLIC_API_TEST_IDS.ownerSession, + ) + .run(); + const bindings = withSessionLifecycleBinding( + createPublicHttpTestBindings(database) as ApiBindings, + ); + + const mutation = + action === "archive" + ? archiveAgentSession({ + bindings, + appId: PUBLIC_API_TEST_IDS.app, + sessionId: PUBLIC_API_TEST_IDS.ownerSession, + viewer: OWNER_VIEWER, + }) + : deleteSessionCascade(bindings, PUBLIC_API_TEST_IDS.ownerSession); + await expect(mutation).rejects.toThrow(); + expect( + await database + .prepare( + "SELECT archived_at, cleanup_operation_kind, runtime_provisioning_operation_id FROM session WHERE id = ?", + ) + .bind(PUBLIC_API_TEST_IDS.ownerSession) + .first(), + ).toEqual({ + archived_at: null, + cleanup_operation_kind: null, + runtime_provisioning_operation_id: PUBLIC_API_TEST_IDS.operation, + }); + } + }); + + test("failed archive repairs move behind older pending work", async () => { + const database = await createPublicHttpContractDatabase(); + await insertNonOwnerSession(database); + await insertOwnerSession(database); + await database + .prepare( + `UPDATE session + SET archived_at = 1, + cleanup_operation_kind = 'archive', + status = 'RESCHEDULING', + status_operation_id = ?, + updated_at = 1 + WHERE id = ?`, + ) + .bind(PUBLIC_API_TEST_IDS.operation, PUBLIC_API_TEST_IDS.nonOwnerSession) + .run(); + await database + .prepare( + `UPDATE session + SET archived_at = 2, + cleanup_operation_kind = 'archive', + status = 'RESCHEDULING', + status_operation_id = ?, + updated_at = 2 + WHERE id = ?`, + ) + .bind(PUBLIC_API_TEST_IDS.operation, PUBLIC_API_TEST_IDS.ownerSession) + .run(); + const bindings = withSessionLifecycleBinding( + createPublicHttpTestBindings(database) as ApiBindings, + [], + { closeError: new Error("viewer cleanup interrupted") }, + ); + + expect( + await repairStaleSessionArchiveCleanups(bindings, { + limit: 1, + staleUpdatedAtLte: 2, + }), + ).toBe(1); + const firstAttempt = await database + .prepare("SELECT updated_at FROM session WHERE id = ?") + .bind(PUBLIC_API_TEST_IDS.nonOwnerSession) + .first<{ updated_at: number }>(); + expect(firstAttempt?.updated_at).toBeGreaterThan(2); + expect( + await database + .prepare("SELECT updated_at FROM session WHERE id = ?") + .bind(PUBLIC_API_TEST_IDS.ownerSession) + .first(), + ).toEqual({ updated_at: 2 }); + + expect( + await repairStaleSessionArchiveCleanups(bindings, { + limit: 1, + staleUpdatedAtLte: 2, + }), + ).toBe(1); + const secondAttempt = await database + .prepare("SELECT updated_at FROM session WHERE id = ?") + .bind(PUBLIC_API_TEST_IDS.ownerSession) + .first<{ updated_at: number }>(); + expect(secondAttempt?.updated_at).toBeGreaterThan(2); + }); + test("delete cascade removes live and terminal driver instances associated with the session", async () => { const database = await createPublicHttpContractDatabase(); await insertNonOwnerSession(database); await insertSandboxSession(database); - await insertSessionRun(database, { runId: PUBLIC_API_TEST_IDS.run }); await insertSessionRun(database, { runId: PUBLIC_API_TEST_IDS.runAlt, status: "completed", }); + await insertSessionRun(database, { runId: PUBLIC_API_TEST_IDS.run }); await insertDriverInstance(database, { driverId: PUBLIC_API_TEST_IDS.driverNonOwner, sandboxSessionId: PUBLIC_API_TEST_IDS.nonOwnerSession, @@ -466,7 +708,7 @@ describe("session lifecycle mutations", () => { expect(anchoredSession?.archived_at).toBeNumber(); expect(anchoredSession).toMatchObject({ - status: "TERMINATED", + status: "IDLE", status_operation_id: operationId, }); @@ -493,7 +735,7 @@ describe("session lifecycle mutations", () => { const bucket = new RetriableSessionFileBucket(); const bindings = withSessionLifecycleBinding( createPublicHttpTestBindings(database, { - fileBucket: bucket as unknown as R2Bucket, + fileBucket: bucket as R2Bucket, }) as ApiBindings, ); @@ -513,7 +755,7 @@ describe("session lifecycle mutations", () => { .first<{ status: string }>(); expect(bucket.deletedKeys).toEqual([`session-files/${SESSION_FILE_ID}/notes.txt`]); - expect(anchoredSession).toEqual({ status: "TERMINATED" }); + expect(anchoredSession).toEqual({ status: "IDLE" }); expect(retainedFile).toEqual({ status: "deleting" }); bucket.failDelete = false; @@ -542,7 +784,6 @@ describe("session lifecycle mutations", () => { test("archive cancels active runs and exposes an idle archived session", async () => { const database = await createPublicHttpContractDatabase(); await insertOwnerSession(database); - await ensureRuntimeLifecycleTables(database); await insertSandboxSession(database, PUBLIC_API_TEST_IDS.ownerSession); await insertSessionRun(database, { createdByAccountId: PUBLIC_API_TEST_IDS.ownerAccount, @@ -586,7 +827,120 @@ describe("session lifecycle mutations", () => { expect(outcomes.every((outcome) => outcome.status !== "failed")).toBe(true); }); - test("unarchive normalizes stale rescheduling state before exposing the session", async () => { + test("maintenance resumes an admitted archive after external cleanup fails", async () => { + const database = await createPublicHttpContractDatabase(); + await insertOwnerSession(database); + const baseBindings = createPublicHttpTestBindings(database) as ApiBindings; + const failingBindings = withSessionLifecycleBinding(baseBindings, [], { + closeError: new Error("viewer cleanup interrupted"), + }); + + await expect( + archiveAgentSession({ + bindings: failingBindings, + appId: PUBLIC_API_TEST_IDS.app, + sessionId: PUBLIC_API_TEST_IDS.ownerSession, + viewer: OWNER_VIEWER, + }), + ).rejects.toThrow("viewer cleanup interrupted"); + + const admitted = await database + .prepare( + "SELECT archived_at, cleanup_operation_kind, status, status_operation_id FROM session WHERE id = ?", + ) + .bind(PUBLIC_API_TEST_IDS.ownerSession) + .first<{ + archived_at: number | null; + cleanup_operation_kind: string | null; + status: string; + status_operation_id: string | null; + }>(); + expect(admitted?.archived_at).toBeNumber(); + expect(admitted).toMatchObject({ + cleanup_operation_kind: "archive", + status: "RESCHEDULING", + }); + expect(admitted?.status_operation_id).toBeString(); + + const repaired = await repairStaleSessionArchiveCleanups( + withSessionLifecycleBinding(baseBindings), + { limit: 10, staleUpdatedAtLte: Date.now() }, + ); + const archived = await database + .prepare( + "SELECT archived_at, cleanup_operation_kind, status, status_operation_id FROM session WHERE id = ?", + ) + .bind(PUBLIC_API_TEST_IDS.ownerSession) + .first<{ + archived_at: number | null; + cleanup_operation_kind: string | null; + status: string; + status_operation_id: string | null; + }>(); + + expect(repaired).toBe(1); + expect(archived?.archived_at).toBeNumber(); + expect(archived).toMatchObject({ + cleanup_operation_kind: "archive", + status: "IDLE", + status_operation_id: null, + }); + }); + + for (const cleanupOperationKind of ["archive", "delete"] as const) { + test(`${cleanupOperationKind} ownership rejects duplicate and advancing active Run projections`, async () => { + for (const initialRunStatus of ["booting", "queued"] as const) { + const database = await createPublicHttpContractDatabase(); + await insertOwnerSession(database); + await insertSessionRun(database, { + createdByAccountId: PUBLIC_API_TEST_IDS.ownerAccount, + runId: PUBLIC_API_TEST_IDS.run, + sessionId: PUBLIC_API_TEST_IDS.ownerSession, + status: initialRunStatus, + }); + const racingDatabase = claimCleanupAfterLifecycleRead(database, cleanupOperationKind); + + const outcome = await setSessionRunStatus(racingDatabase, { + runId: PUBLIC_API_TEST_IDS.run, + status: "booting", + }); + + expect(outcome.kind).toBe(initialRunStatus === "booting" ? "duplicate" : "stale"); + expect(await readCleanupOwnedRunProjection(database)).toEqual({ + archived_at: 2, + cleanup_operation_kind: cleanupOperationKind, + run_status: initialRunStatus, + run_status_seq: 0, + session_status: "RESCHEDULING", + session_status_operation_id: PUBLIC_API_TEST_IDS.operation, + session_status_seq: 1, + }); + } + }); + + test(`${cleanupOperationKind} ownership rejects a duplicate active projection repair`, async () => { + const database = await createPublicHttpContractDatabase(); + await insertOwnerSession(database); + await insertSessionRun(database, { + createdByAccountId: PUBLIC_API_TEST_IDS.ownerAccount, + runId: PUBLIC_API_TEST_IDS.run, + sessionId: PUBLIC_API_TEST_IDS.ownerSession, + status: "booting", + }); + await claimCleanup(database, cleanupOperationKind); + const before = await readCleanupOwnedRunProjection(database); + + const outcome = await setSessionRunStatus(database, { + runId: PUBLIC_API_TEST_IDS.run, + status: "booting", + }); + + expect(outcome.kind).toBe("repair_needed"); + expect(await readCleanupOwnedRunProjection(database)).toEqual(before); + }); + } + + test("unarchive refuses to clear an in-progress cleanup fence", async () => { const database = await createPublicHttpContractDatabase(); await insertOwnerSession(database); await insertSessionRun(database, { @@ -599,25 +953,35 @@ describe("session lifecycle mutations", () => { ` UPDATE session SET archived_at = ?, + cleanup_operation_kind = ?, status = ?, status_operation_id = ? WHERE id = ? `, ) - .bind(1, "RESCHEDULING", PUBLIC_API_TEST_IDS.operation, PUBLIC_API_TEST_IDS.ownerSession) + .bind( + 1, + "delete", + "RESCHEDULING", + PUBLIC_API_TEST_IDS.operation, + PUBLIC_API_TEST_IDS.ownerSession, + ) .run(); - await unarchiveAgentSession({ - database, - appId: PUBLIC_API_TEST_IDS.app, - sessionId: PUBLIC_API_TEST_IDS.ownerSession, - viewer: OWNER_VIEWER, - }); + await expect( + unarchiveAgentSession({ + database, + appId: PUBLIC_API_TEST_IDS.app, + sessionId: PUBLIC_API_TEST_IDS.ownerSession, + viewer: OWNER_VIEWER, + }), + ).rejects.toThrow("cleanup is still in progress"); const row = await database .prepare( ` SELECT session.archived_at, + session.cleanup_operation_kind, session.status AS session_status, session.status_operation_id, session_run.status AS run_status @@ -629,17 +993,44 @@ describe("session lifecycle mutations", () => { .bind(PUBLIC_API_TEST_IDS.ownerSession) .first<{ archived_at: number | null; + cleanup_operation_kind: string | null; run_status: string; session_status: string; status_operation_id: string | null; }>(); expect(row).toEqual({ - archived_at: null, - run_status: "cancelled", - session_status: "IDLE", - status_operation_id: null, + archived_at: 1, + cleanup_operation_kind: "delete", + run_status: "running", + session_status: "RESCHEDULING", + status_operation_id: PUBLIC_API_TEST_IDS.operation, + }); + }); + + test("unarchive exposes a stable archived Session with no cleanup owner", async () => { + const database = await createPublicHttpContractDatabase(); + await insertOwnerSession(database); + await database + .prepare( + "UPDATE session SET archived_at = 1, cleanup_operation_kind = 'archive' WHERE id = ?", + ) + .bind(PUBLIC_API_TEST_IDS.ownerSession) + .run(); + + await unarchiveAgentSession({ + database, + appId: PUBLIC_API_TEST_IDS.app, + sessionId: PUBLIC_API_TEST_IDS.ownerSession, + viewer: OWNER_VIEWER, }); + + expect( + await database + .prepare("SELECT archived_at FROM session WHERE id = ?") + .bind(PUBLIC_API_TEST_IDS.ownerSession) + .first(), + ).toEqual({ archived_at: null }); }); test("lifecycle mutations reject attributed participants who are not session creators", async () => { @@ -690,3 +1081,95 @@ describe("session lifecycle mutations", () => { ).rejects.toThrow(); }); }); + +async function readCleanupOwnedRunProjection(database: D1Database) { + return database + .prepare( + `SELECT session.archived_at, + session.cleanup_operation_kind, + session.status AS session_status, + session.status_operation_id AS session_status_operation_id, + session.status_seq AS session_status_seq, + session_run.status AS run_status, + session_run.status_seq AS run_status_seq + FROM session + JOIN session_run ON session_run.id = session.last_run_id + WHERE session.id = ?`, + ) + .bind(PUBLIC_API_TEST_IDS.ownerSession) + .first(); +} + +function claimCleanupAfterLifecycleRead( + database: D1Database, + cleanupOperationKind: "archive" | "delete", +): D1Database { + let claimed = false; + let interceptedLifecycleRead = false; + + async function claim(): Promise { + if (claimed) { + return; + } + claimed = true; + await claimCleanup(database, cleanupOperationKind); + } + + function injectAfterRead(statement: D1PreparedStatement): D1PreparedStatement { + return new Proxy(statement, { + get(target, property, receiver) { + if (property === "bind") { + return (...values: unknown[]) => injectAfterRead(target.bind(...values)); + } + if (property === "all" || property === "first" || property === "raw") { + return async (...args: unknown[]) => { + const method = Reflect.get(target, property, receiver) as ( + ...values: unknown[] + ) => unknown; + const result = await method.apply(target, args); + await claim(); + return result; + }; + } + const value = Reflect.get(target, property, receiver); + return typeof value === "function" ? value.bind(target) : value; + }, + }); + } + + return new Proxy(database, { + get(target, property, receiver) { + if (property === "prepare") { + return (query: string) => { + const statement = target.prepare(query); + if (interceptedLifecycleRead) { + return statement; + } + interceptedLifecycleRead = true; + return injectAfterRead(statement); + }; + } + const value = Reflect.get(target, property, receiver); + return typeof value === "function" ? value.bind(target) : value; + }, + }); +} + +async function claimCleanup( + database: D1Database, + cleanupOperationKind: "archive" | "delete", +): Promise { + await database + .prepare( + `UPDATE session + SET archived_at = 2, + cleanup_operation_kind = ?, + status = 'RESCHEDULING', + status_operation_id = ?, + status_seq = status_seq + 1, + updated_at = 2 + WHERE id = ?`, + ) + .bind(cleanupOperationKind, PUBLIC_API_TEST_IDS.operation, PUBLIC_API_TEST_IDS.ownerSession) + .run(); +} diff --git a/apps/api/tests/session-message-store.test.ts b/apps/api/tests/session-message-store.test.ts index 92a875be..ce3a0fad 100644 --- a/apps/api/tests/session-message-store.test.ts +++ b/apps/api/tests/session-message-store.test.ts @@ -21,11 +21,23 @@ function createSessionMessageStoreDatabase(): SqliteD1Database { created_by_account_id text NOT NULL, id text PRIMARY KEY NOT NULL, plan_json text, + projection_format text DEFAULT 'materialized' NOT NULL, role text NOT NULL, segments_json text, seq integer NOT NULL, session_id text NOT NULL, - session_run_id text + session_run_id text, + CHECK (projection_format IN ('materialized', 'event_stream_v3')), + CHECK ( + projection_format <> 'event_stream_v3' + OR ( + role = 'assistant' + AND session_run_id IS NOT NULL + AND content_text = '' + AND plan_json IS NULL + AND segments_json IS NULL + ) + ) ); CREATE UNIQUE INDEX session_message_session_seq_idx diff --git a/apps/api/tests/session-model-call-identity.test.ts b/apps/api/tests/session-model-call-identity.test.ts index 27cabc0c..9ed5f1d9 100644 --- a/apps/api/tests/session-model-call-identity.test.ts +++ b/apps/api/tests/session-model-call-identity.test.ts @@ -37,6 +37,7 @@ const DRIVER_INSTANCE_ID = parsePlatformId( "01J00000000000000000000017", "driver instance ID", ); +const USAGE_CREATED_AT_MS = 1_500; interface IdentityProjection { metadata_json: string | null; @@ -86,6 +87,7 @@ function createSessionModelCallDatabase(): SqliteD1Database { CREATE TABLE session_run ( agent_id text NOT NULL, completed_at integer, + created_at integer NOT NULL, created_by_account_id text NOT NULL, deployment_version_id text, id text PRIMARY KEY NOT NULL, @@ -94,6 +96,7 @@ function createSessionModelCallDatabase(): SqliteD1Database { runtime_id text, session_id text NOT NULL, started_at integer, + status text NOT NULL, trigger text NOT NULL ); @@ -114,6 +117,7 @@ function createSessionModelCallDatabase(): SqliteD1Database { native_call_id text, output_tokens integer, provider text NOT NULL, + source_event_seq integer DEFAULT 0 NOT NULL, session_id text NOT NULL, session_run_id text NOT NULL, started_at integer, @@ -148,10 +152,18 @@ function createSessionModelCallDatabase(): SqliteD1Database { session_run_id text, source text NOT NULL, source_event_id text NOT NULL, + source_event_seq integer DEFAULT 0 NOT NULL, total_cost_usd_micros integer NOT NULL, usage_contract text NOT NULL, UNIQUE (source, source_event_id) ); + + CREATE TABLE usage_event_rollup_receipt ( + rolled_up_at integer NOT NULL, + source text NOT NULL, + source_event_id text NOT NULL, + PRIMARY KEY (source, source_event_id) + ); `); return database; @@ -257,6 +269,7 @@ async function seedRunIdentity( INSERT INTO session_run ( agent_id, completed_at, + created_at, created_by_account_id, deployment_version_id, id, @@ -265,9 +278,10 @@ async function seedRunIdentity( runtime_id, session_id, started_at, + status, trigger ) - VALUES (?, 1800, ?, ?, ?, 'gpt-5.4', 'openai', 'openai-runtime', ?, 1200, 'user_prompt') + VALUES (?, 1800, 1000, ?, ?, ?, 'gpt-5.4', 'openai', 'openai-runtime', ?, 1200, 'completed', 'user_prompt') `, ) .bind(AGENT_ID, createdByAccountId, deploymentVersionId, SESSION_RUN_ID, SESSION_ID) @@ -293,10 +307,11 @@ describe("session model call identity", () => { } satisfies SessionUsageSummary; await upsertSessionModelCallUsage(database, { + createdAtMs: USAGE_CREATED_AT_MS, driverInstanceId: DRIVER_INSTANCE_ID, sessionId: SESSION_ID, sessionRunId: SESSION_RUN_ID, - status: "completed", + sourceEventSeq: 5, traceId: "trace-1", usage, }); @@ -377,10 +392,11 @@ describe("session model call identity", () => { await expect( upsertSessionModelCallUsage(database, { + createdAtMs: USAGE_CREATED_AT_MS, driverInstanceId: DRIVER_INSTANCE_ID, sessionId: SESSION_ID, sessionRunId: SESSION_RUN_ID, - status: "completed", + sourceEventSeq: 5, traceId: "trace-wrong-app", usage, }), @@ -398,10 +414,11 @@ describe("session model call identity", () => { usageContract: "openai_total_with_cached_breakdown", } satisfies SessionUsageSummary; const input = { + createdAtMs: USAGE_CREATED_AT_MS, driverInstanceId: DRIVER_INSTANCE_ID, sessionId: SESSION_ID, sessionRunId: SESSION_RUN_ID, - status: "completed" as const, + sourceEventSeq: 5, traceId: "trace-atomic-ledger", usage, }; @@ -435,4 +452,247 @@ describe("session model call identity", () => { .first<{ count: number }>(), ).toEqual({ count: 1 }); }); + + test.each([ + ["running", null, "started"], + ["waiting_input", null, "started"], + ["completed", 1_800, "completed"], + ["failed", 1_800, "failed"], + ["cancelled", 1_800, "failed"], + ["expired", 1_800, "failed"], + ] as const)( + "derives model-call status from durable Run status %s", + async (runStatus, completedAt, expectedStatus) => { + const database = createSessionModelCallDatabase(); + await seedRunIdentity(database); + await database + .prepare("UPDATE session_run SET completed_at = ?, status = ? WHERE id = ?") + .bind(completedAt, runStatus, SESSION_RUN_ID) + .run(); + + await upsertSessionModelCallUsage(database, { + createdAtMs: USAGE_CREATED_AT_MS, + driverInstanceId: DRIVER_INSTANCE_ID, + sessionId: SESSION_ID, + sessionRunId: SESSION_RUN_ID, + sourceEventSeq: 5, + traceId: "trace-status", + usage: { + callId: "status-call", + inputTokens: 10, + source: "prompt_response", + usageContract: "openai_total_with_cached_breakdown", + }, + }); + + expect( + await database + .prepare("SELECT completed_at, status FROM session_model_call") + .first<{ completed_at: number | null; status: string }>(), + ).toEqual({ completed_at: completedAt, status: expectedStatus }); + }, + ); + + test("converges model-call and usage rows by durable event seq", async () => { + const database = createSessionModelCallDatabase(); + await seedRunIdentity(database); + const input = { + createdAtMs: USAGE_CREATED_AT_MS, + driverInstanceId: DRIVER_INSTANCE_ID, + sessionId: SESSION_ID, + sessionRunId: SESSION_RUN_ID, + sourceEventSeq: 5, + traceId: "trace-sequenced", + usage: { + callId: "sequenced-call", + inputTokens: 10, + outputTokens: 5, + source: "prompt_response" as const, + usageContract: "openai_total_with_cached_breakdown" as const, + }, + }; + + await upsertSessionModelCallUsage(database, input); + await upsertSessionModelCallUsage(database, { + ...input, + sourceEventSeq: 4, + usage: { ...input.usage, inputTokens: 1 }, + }); + await upsertSessionModelCallUsage(database, input); + await expect( + upsertSessionModelCallUsage(database, { + ...input, + usage: { ...input.usage, inputTokens: 20 }, + }), + ).rejects.toThrow("replayed with conflicting content"); + await upsertSessionModelCallUsage(database, { + ...input, + sourceEventSeq: 6, + usage: { ...input.usage, inputTokens: 30 }, + }); + + expect( + await database + .prepare( + "SELECT input_tokens, source_event_seq, status FROM session_model_call WHERE call_key = ?", + ) + .bind("model_call:sequenced-call") + .first<{ input_tokens: number; source_event_seq: number; status: string }>(), + ).toEqual({ input_tokens: 30, source_event_seq: 6, status: "completed" }); + expect( + await database + .prepare("SELECT input_tokens, source_event_seq FROM usage_event WHERE source_event_id = ?") + .bind(`${DRIVER_INSTANCE_ID}:sequenced-call`) + .first<{ input_tokens: number; source_event_seq: number }>(), + ).toEqual({ input_tokens: 30, source_event_seq: 6 }); + }); + + test("merges higher partial usage identically into the model call and ledger", async () => { + const database = createSessionModelCallDatabase(); + await seedRunIdentity(database); + const input = { + createdAtMs: USAGE_CREATED_AT_MS, + driverInstanceId: DRIVER_INSTANCE_ID, + sessionId: SESSION_ID, + sessionRunId: SESSION_RUN_ID, + sourceEventSeq: 5, + traceId: "trace-partial", + usage: { + callId: "partial-call", + inputTokens: 10, + outputTokens: 5, + source: "prompt_response" as const, + usageContract: "openai_total_with_cached_breakdown" as const, + }, + }; + + const partialInput = { + ...input, + createdAtMs: USAGE_CREATED_AT_MS + 100, + sourceEventSeq: 6, + usage: { + callId: "partial-call", + outputTokens: 7, + source: "prompt_response", + usageContract: "openai_total_with_cached_breakdown", + }, + }; + + await upsertSessionModelCallUsage(database, input); + await upsertSessionModelCallUsage(database, partialInput); + await upsertSessionModelCallUsage(database, partialInput); + + expect( + await database + .prepare( + `SELECT created_at, input_tokens, output_tokens, source_event_seq + FROM session_model_call WHERE call_key = ?`, + ) + .bind("model_call:partial-call") + .first(), + ).toEqual({ + created_at: USAGE_CREATED_AT_MS, + input_tokens: 10, + output_tokens: 7, + source_event_seq: 6, + }); + expect( + await database + .prepare( + `SELECT created_at, input_tokens, output_tokens, source_event_seq + FROM usage_event WHERE source_event_id = ?`, + ) + .bind(`${DRIVER_INSTANCE_ID}:partial-call`) + .first(), + ).toEqual({ + created_at: USAGE_CREATED_AT_MS, + input_tokens: 10, + output_tokens: 7, + source_event_seq: 6, + }); + + await database + .prepare("UPDATE session_run SET model = 'gpt-5.5' WHERE id = ?") + .bind(SESSION_RUN_ID) + .run(); + await expect( + upsertSessionModelCallUsage(database, { + ...partialInput, + sourceEventSeq: 7, + usage: { ...partialInput.usage, outputTokens: 9 }, + }), + ).rejects.toThrow(); + + expect( + await database + .prepare( + `SELECT model, output_tokens, source_event_seq + FROM session_model_call WHERE call_key = ?`, + ) + .bind("model_call:partial-call") + .first(), + ).toEqual({ model: "gpt-5.4", output_tokens: 7, source_event_seq: 6 }); + expect( + await database + .prepare( + `SELECT model, output_tokens, source_event_seq + FROM usage_event WHERE source_event_id = ?`, + ) + .bind(`${DRIVER_INSTANCE_ID}:partial-call`) + .first(), + ).toEqual({ model: "gpt-5.4", output_tokens: 7, source_event_seq: 6 }); + }); + + test("never replaces raw usage after its durable call was rolled up", async () => { + const database = createSessionModelCallDatabase(); + await seedRunIdentity(database); + const input = { + createdAtMs: USAGE_CREATED_AT_MS, + driverInstanceId: DRIVER_INSTANCE_ID, + sessionId: SESSION_ID, + sessionRunId: SESSION_RUN_ID, + sourceEventSeq: 5, + traceId: "trace-rolled", + usage: { + callId: "rolled-call", + inputTokens: 10, + outputTokens: 5, + source: "prompt_response" as const, + usageContract: "openai_total_with_cached_breakdown" as const, + }, + }; + + await upsertSessionModelCallUsage(database, input); + await database + .prepare( + "INSERT INTO usage_event_rollup_receipt (source, source_event_id, rolled_up_at) VALUES (?, ?, ?)", + ) + .bind("runtime_driver", `${DRIVER_INSTANCE_ID}:rolled-call`, 2_000) + .run(); + await database + .prepare("DELETE FROM usage_event WHERE source_event_id = ?") + .bind(`${DRIVER_INSTANCE_ID}:rolled-call`) + .run(); + + await upsertSessionModelCallUsage(database, input); + await expect( + upsertSessionModelCallUsage(database, { + ...input, + sourceEventSeq: 6, + usage: { ...input.usage, inputTokens: 20 }, + }), + ).rejects.toThrow("already rolled up"); + + expect( + await database + .prepare("SELECT source_event_seq FROM session_model_call WHERE call_key = ?") + .bind("model_call:rolled-call") + .first<{ source_event_seq: number }>(), + ).toEqual({ source_event_seq: 5 }); + expect( + await database + .prepare("SELECT COUNT(*) AS count FROM usage_event") + .first<{ count: number }>(), + ).toEqual({ count: 0 }); + }); }); diff --git a/apps/api/tests/session-process-events.test.ts b/apps/api/tests/session-process-events.test.ts index 6d4796cb..b93e89e0 100644 --- a/apps/api/tests/session-process-events.test.ts +++ b/apps/api/tests/session-process-events.test.ts @@ -96,6 +96,7 @@ function createProcessEventQueryDatabase(): SqliteD1Database { run_id text, seq integer NOT NULL, session_id text NOT NULL, + stream_id text, tokens integer, visibility text NOT NULL ); @@ -200,12 +201,18 @@ async function insertSessionProcessEvent( runId?: string | null; seq: number; sessionId?: string; + streamId?: string | null; tokens?: number | null; visibility?: SessionRuntimeEventVisibility; }, ): Promise { const occurredAt = input.occurredAt ?? input.seq * 1000; const processType = input.processType ?? "run.started"; + const eventType = input.eventType ?? processType; + const streamId = + input.streamId === undefined && /^(?:message|thought)\./u.test(eventType) + ? "stream-1" + : (input.streamId ?? null); await database .prepare( @@ -221,22 +228,24 @@ async function insertSessionProcessEvent( run_id, seq, session_id, + stream_id, tokens, visibility - ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) `, ) .bind( input.id, input.content ?? `run-${input.seq}`, input.endedAt ?? occurredAt, - input.eventType ?? processType, + eventType, occurredAt, input.processStatus ?? "available", processType, input.runId ?? null, input.seq, input.sessionId ?? SESSION_ID, + streamId, input.tokens ?? null, input.visibility ?? "all_consumers", ) @@ -498,6 +507,464 @@ describe("session process event projection", () => { }); }); + test("pages raw stream rows until the latest logical events are complete", async () => { + const database = createProcessEventQueryDatabase(); + const fragmentCount = 1_005; + await insertSessionProcessEvent(database, { + content: "run-1", + eventType: "run.started", + id: "event-run-started", + runId: "run-1", + seq: 1, + }); + + for (let index = 0; index < fragmentCount; index += 1) { + await insertSessionProcessEvent(database, { + content: "x", + eventType: "message.delta", + id: `event-message-delta-${index}`, + processType: "agent.message.delta", + runId: "run-1", + seq: index + 2, + }); + } + + await insertSessionProcessEvent(database, { + content: "Message updated.", + eventType: "message.completed", + id: "event-message-completed", + processType: "agent.message.delta", + runId: "run-1", + seq: fragmentCount + 2, + }); + await insertSessionProcessEvent(database, { + content: "run-1", + eventType: "run.completed", + id: "event-run-completed", + processType: "run.completed", + runId: "run-1", + seq: fragmentCount + 3, + }); + + const events = await getThreadSessionProcessEvents( + database, + VIEWER, + { appId: APP_ID, sessionId: SESSION_ID }, + { limit: 2 }, + ); + + expect(events.map((event) => event.type)).toEqual([ + "session.status", + "agent.message.delta", + "run.completed", + ]); + expect(events[1]).toMatchObject({ + content: "x".repeat(fragmentCount), + id: "event-message-completed", + }); + }); + + test("does not cut a retained stream at a page boundary", async () => { + const database = createProcessEventQueryDatabase(); + const fragmentCount = 550; + await insertSessionProcessEvent(database, { + content: "run-1", + eventType: "run.started", + id: "event-run-started", + runId: "run-1", + seq: 1, + }); + await insertSessionProcessEvent(database, { + content: "Agent thinking updated.", + eventType: "thought.started", + id: "event-thought-a-started", + processType: "agent.thinking.delta", + runId: "run-1", + seq: 2, + streamId: "thought-a", + }); + await insertSessionProcessEvent(database, { + content: "Agent thinking updated.", + eventType: "thought.started", + id: "event-thought-b-started", + processType: "agent.thinking.delta", + runId: "run-1", + seq: 3, + streamId: "thought-b", + }); + + for (let index = 0; index < fragmentCount; index += 1) { + await insertSessionProcessEvent(database, { + content: "a", + eventType: "thought.delta", + id: `event-thought-a-delta-${index}`, + processType: "agent.thinking.delta", + runId: "run-1", + seq: index * 2 + 4, + streamId: "thought-a", + }); + await insertSessionProcessEvent(database, { + content: "b", + eventType: "thought.delta", + id: `event-thought-b-delta-${index}`, + processType: "agent.thinking.delta", + runId: "run-1", + seq: index * 2 + 5, + streamId: "thought-b", + }); + } + + const firstTerminalSeq = fragmentCount * 2 + 4; + await insertSessionProcessEvent(database, { + content: "Agent thinking updated.", + eventType: "thought.completed", + id: "event-thought-a-completed", + processType: "agent.thinking.delta", + runId: "run-1", + seq: firstTerminalSeq, + streamId: "thought-a", + }); + await insertSessionProcessEvent(database, { + content: "Agent thinking updated.", + eventType: "thought.completed", + id: "event-thought-b-completed", + processType: "agent.thinking.delta", + runId: "run-1", + seq: firstTerminalSeq + 1, + streamId: "thought-b", + }); + await insertSessionProcessEvent(database, { + content: "run-1", + eventType: "run.completed", + id: "event-run-completed", + processType: "run.completed", + runId: "run-1", + seq: firstTerminalSeq + 2, + }); + + const events = await getThreadSessionProcessEvents( + database, + VIEWER, + { appId: APP_ID, sessionId: SESSION_ID }, + { limit: 2 }, + ); + + expect(events.map((event) => event.type)).toEqual([ + "session.status", + "agent.thinking.delta", + "run.completed", + ]); + expect(events[1]).toMatchObject({ + content: "b".repeat(fragmentCount), + id: "event-thought-b-completed", + }); + }); + + test("does not expose an incomplete stream when the raw scan ceiling is reached", async () => { + const database = createProcessEventQueryDatabase(); + const fragmentCount = 20_001; + await insertSessionProcessEvent(database, { + content: "Agent thinking updated.", + eventType: "thought.started", + id: "event-thought-started", + processType: "agent.thinking.delta", + runId: "run-1", + seq: 1, + streamId: "ceiling-thought", + }); + + for (let index = 0; index < fragmentCount; index += 1) { + await insertSessionProcessEvent(database, { + content: "x", + eventType: "thought.delta", + id: `event-thought-delta-${index}`, + processType: "agent.thinking.delta", + runId: "run-1", + seq: index + 2, + streamId: "ceiling-thought", + }); + } + + await insertSessionProcessEvent(database, { + content: "Agent thinking updated.", + eventType: "thought.completed", + id: "event-thought-completed", + processType: "agent.thinking.delta", + runId: "run-1", + seq: fragmentCount + 2, + streamId: "ceiling-thought", + }); + await insertSessionProcessEvent(database, { + content: "Complete final answer", + eventType: "message.added", + id: "event-final-message", + processType: "agent.message.delta", + runId: "run-1", + seq: fragmentCount + 3, + streamId: "complete-final-message", + }); + await insertSessionProcessEvent(database, { + content: "run-1", + eventType: "run.completed", + id: "event-run-completed", + processType: "run.completed", + runId: "run-1", + seq: fragmentCount + 4, + }); + + const events = await getThreadSessionProcessEvents( + database, + VIEWER, + { appId: APP_ID, sessionId: SESSION_ID }, + { limit: 2 }, + ); + + expect(events.map((event) => event.type)).toEqual([ + "session.status", + "agent.message.delta", + "run.completed", + ]); + expect(events[0]?.content).toContain("Earlier runtime events are hidden"); + expect(events[1]?.content).toBe("Complete final answer"); + }, 15_000); + + test("recognizes the database start at the exact raw scan ceiling", async () => { + const database = createProcessEventQueryDatabase(); + const fillerCount = 19_997; + + for (let seq = 1; seq <= fillerCount; seq += 1) { + await insertSessionProcessEvent(database, { + content: `filler-${seq}`, + id: `exact-ceiling-filler-${seq}`, + processType: "usage.updated", + seq, + }); + } + + await insertSessionProcessEvent(database, { + content: "retained", + eventType: "message.delta", + id: "exact-ceiling-message-delta", + processType: "agent.message.delta", + runId: null, + seq: fillerCount + 1, + streamId: "exact-ceiling-message", + }); + await insertSessionProcessEvent(database, { + content: "Message updated.", + eventType: "message.completed", + id: "exact-ceiling-message-completed", + processType: "agent.message.delta", + runId: null, + seq: fillerCount + 2, + streamId: "exact-ceiling-message", + }); + await insertSessionProcessEvent(database, { + content: "run-1", + eventType: "run.completed", + id: "exact-ceiling-run-completed", + processType: "run.completed", + runId: "run-1", + seq: fillerCount + 3, + }); + + const events = await getThreadSessionProcessEvents( + database, + VIEWER, + { appId: APP_ID, sessionId: SESSION_ID }, + { limit: 2 }, + ); + + expect(events.map((event) => event.type)).toEqual([ + "session.status", + "agent.message.delta", + "run.completed", + ]); + expect(events[1]?.content).toBe("retained"); + }, 15_000); + + test("orders paged events by durable sequence rather than driver time", async () => { + const database = createProcessEventQueryDatabase(); + const fragmentCount = 1_002; + await insertSessionProcessEvent(database, { + content: "run-1", + eventType: "run.started", + id: "out-of-order-run-started", + occurredAt: 1_000, + processType: "run.started", + runId: "run-1", + seq: 1, + }); + await insertSessionProcessEvent(database, { + content: "Message updated.", + eventType: "message.started", + id: "out-of-order-message-started", + occurredAt: 100_000, + processType: "agent.message.delta", + runId: "run-1", + seq: 2, + streamId: "out-of-order-message", + }); + + for (let index = 0; index < fragmentCount; index += 1) { + await insertSessionProcessEvent(database, { + content: "x", + eventType: "message.delta", + id: `out-of-order-message-delta-${index}`, + occurredAt: 101_000 + index, + processType: "agent.message.delta", + runId: "run-1", + seq: index + 3, + streamId: "out-of-order-message", + }); + } + + await insertSessionProcessEvent(database, { + content: "Read file", + eventType: "tool.call.updated", + id: "out-of-order-tool", + occurredAt: 2_000, + processType: "tool.use.started", + runId: "run-1", + seq: fragmentCount + 3, + }); + await insertSessionProcessEvent(database, { + content: "run-1", + eventType: "run.completed", + id: "out-of-order-run-completed", + occurredAt: 3_000, + processType: "run.completed", + runId: "run-1", + seq: fragmentCount + 4, + }); + + const events = await getThreadSessionProcessEvents( + database, + VIEWER, + { appId: APP_ID, sessionId: SESSION_ID }, + { limit: 2 }, + ); + + expect(events.map((event) => event.type)).toEqual([ + "session.status", + "tool.use.started", + "run.completed", + ]); + expect(events[1]?.id).toBe("out-of-order-tool"); + }); + + test.each([ + ["message.cancelled", "available"], + ["message.failed", "error"], + ] as const)("folds persisted streams closed by %s", async (eventType, processStatus) => { + const database = createProcessEventQueryDatabase(); + await insertSessionProcessEvent(database, { + content: "Partial answer", + eventType: "message.delta", + id: "event-message-delta", + processType: "agent.message.delta", + runId: "run-1", + seq: 1, + }); + await insertSessionProcessEvent(database, { + content: "Message updated.", + eventType, + id: "event-message-end", + processStatus, + processType: "agent.message.delta", + runId: "run-1", + seq: 2, + }); + + const events = await getThreadSessionProcessEvents( + database, + VIEWER, + { appId: APP_ID, sessionId: SESSION_ID }, + { limit: 100 }, + ); + + expect(events).toEqual([ + expect.objectContaining({ + content: "Partial answer", + id: "event-message-end", + status: processStatus, + type: "agent.message.delta", + }), + ]); + }); + + test("keeps a terminal message without deltas", async () => { + const database = createProcessEventQueryDatabase(); + await insertSessionProcessEvent(database, { + content: "Message updated.", + eventType: "message.completed", + id: "event-message-completed", + processType: "agent.message.delta", + runId: "run-1", + seq: 1, + }); + + const events = await getThreadSessionProcessEvents( + database, + VIEWER, + { appId: APP_ID, sessionId: SESSION_ID }, + { limit: 100 }, + ); + + expect(events).toEqual([ + expect.objectContaining({ + content: "", + id: "event-message-completed", + type: "agent.message.delta", + }), + ]); + }); + + test("folds interleaved messages by stream identity", async () => { + const database = createProcessEventQueryDatabase(); + + for (const event of [ + { content: "A1", id: "a-1", seq: 1, streamId: "message-a" }, + { content: "B1", id: "b-1", seq: 2, streamId: "message-b" }, + { content: "A2", id: "a-2", seq: 3, streamId: "message-a" }, + ]) { + await insertSessionProcessEvent(database, { + ...event, + eventType: "message.delta", + processType: "agent.message.delta", + runId: "run-1", + }); + } + + await insertSessionProcessEvent(database, { + content: "Message updated.", + eventType: "message.completed", + id: "b-end", + processType: "agent.message.delta", + runId: "run-1", + seq: 4, + streamId: "message-b", + }); + await insertSessionProcessEvent(database, { + content: "Message updated.", + eventType: "message.completed", + id: "a-end", + processType: "agent.message.delta", + runId: "run-1", + seq: 5, + streamId: "message-a", + }); + + const events = await getThreadSessionProcessEvents( + database, + VIEWER, + { appId: APP_ID, sessionId: SESSION_ID }, + { limit: 100 }, + ); + + expect(events.map((event) => event.content)).toEqual(["A1A2", "B1"]); + }); + test("shows an in-flight streamed message as a single folded process event", async () => { const database = createProcessEventQueryDatabase(); await insertSessionProcessEvent(database, { diff --git a/apps/api/tests/session-resource-files.test.ts b/apps/api/tests/session-resource-files.test.ts index 20315008..d334943c 100644 --- a/apps/api/tests/session-resource-files.test.ts +++ b/apps/api/tests/session-resource-files.test.ts @@ -12,13 +12,6 @@ import { listSessionResources, } from "../src/modules/sessions/application/session-resource.service"; import type { ApiBindings } from "../src/platform/cloudflare/worker-types"; -import { - PUBLIC_API_TEST_IDS, - PublicApiMemoryFileBucket, - createPublicHttpContractDatabase, - createPublicHttpTestBindings, - insertOwnerSession, -} from "./helpers/public-api-http-test-fixture"; import { SqliteD1Database } from "./helpers/sqlite-d1"; const OWNER_ID = parsePlatformId("01J00000000000000000000001", "owner ID"); @@ -99,6 +92,7 @@ function createSessionResourceDatabase(input: { includeFile?: boolean } = {}): S parent_path text NOT NULL, path text NOT NULL, purpose text NOT NULL, + runtime_event_seq integer, size integer NOT NULL, updated_at integer NOT NULL, version integer NOT NULL @@ -573,55 +567,6 @@ describe("session resource files", () => { expect(artifacts.files.map((file) => file.id)).toEqual([ARTIFACT_FILE_ID]); }); - test("records runtime outputs as session-scoped artifacts", async () => { - const database = await createPublicHttpContractDatabase(); - await insertOwnerSession(database); - const bucket = new PublicApiMemoryFileBucket(); - const ownerViewer: AuthenticatedViewer = { - email: "owner@example.com", - emailVerified: true, - id: PUBLIC_API_TEST_IDS.ownerAccount, - imageUrl: null, - name: "Owner", - }; - const file = await fileStore.recordRuntimeOutput({ - bindings: createPublicHttpTestBindings(database, { - fileBucket: bucket as unknown as R2Bucket, - }) as ApiBindings, - body: new TextEncoder().encode("runtime summary"), - contentType: "text/markdown", - createdBy: PUBLIC_API_TEST_IDS.ownerAccount, - path: "outputs/reports/summary.md", - sessionId: PUBLIC_API_TEST_IDS.ownerSession, - }); - - expect(file.owner).toEqual({ - id: PUBLIC_API_TEST_IDS.ownerSession, - kind: "session", - }); - expect(file.purpose).toBe("session_artifact"); - expect(file.scope).toEqual({ - id: PUBLIC_API_TEST_IDS.ownerSession, - kind: "session", - }); - expect(file.sessionKind).toBe("artifact"); - expect(file.sourcePath).toBe("outputs/reports/summary.md"); - - const resources = await listSessionResources(database, ownerViewer, { - appId: PUBLIC_API_TEST_IDS.app, - sessionId: PUBLIC_API_TEST_IDS.ownerSession, - }); - - expect(resources).toEqual([ - expect.objectContaining({ - id: file.id, - kind: "artifact", - name: "summary.md", - path: `session-artifacts/${file.id}/summary.md`, - }), - ]); - }); - test("lists session artifacts but does not treat them as removable attachments", async () => { const database = createSessionResourceDatabase(); insertSessionArtifact(database); diff --git a/apps/api/tests/session-resource-mount.test.ts b/apps/api/tests/session-resource-mount.test.ts index ac4a7df2..83d5a621 100644 --- a/apps/api/tests/session-resource-mount.test.ts +++ b/apps/api/tests/session-resource-mount.test.ts @@ -75,7 +75,7 @@ function createSandbox(input: { async destroy() {}, async exec(command) { calls.exec.push(command); - return commandResult(input.pathExists); + return commandResult(command.includes("readlink") || input.pathExists); }, async getSession() { throw new Error("getSession is not used in session resource mount tests."); @@ -123,7 +123,8 @@ describe("ensureSessionResourcesMounted", () => { sessionId: "session-local", }); - expect(sandbox.calls.exec).toHaveLength(1); + expect(sandbox.calls.exec).toHaveLength(2); + expect(sandbox.calls.exec[1]).toContain("../../.mosoo/session-files/session-local"); expect(sandbox.calls.mkdir).toEqual([]); expect(sandbox.calls.mountBucket).toEqual([]); }); @@ -137,7 +138,8 @@ describe("ensureSessionResourcesMounted", () => { sessionId: "session-remote", }); - expect(sandbox.calls.exec).toHaveLength(1); + expect(sandbox.calls.exec).toHaveLength(2); + expect(sandbox.calls.exec[1]).toContain("../../.mosoo/session-files/session-remote"); expect(sandbox.calls.mkdir).toHaveLength(1); expect(sandbox.calls.mountBucket).toHaveLength(1); expect(sandbox.calls.mountBucket[0]).toEqual({ @@ -170,7 +172,8 @@ describe("ensureSessionResourcesMounted", () => { sessionId: "session-remote", }); - expect(sandbox.calls.exec).toHaveLength(2); + expect(sandbox.calls.exec).toHaveLength(3); + expect(sandbox.calls.exec[2]).toContain("../../.mosoo/session-files/session-remote"); expect(sandbox.calls.mkdir).toHaveLength(1); expect(sandbox.calls.mountBucket.map((call) => call.mountPath)).toEqual(sandbox.calls.mkdir); }); diff --git a/apps/api/tests/session-run-admission-atomicity.test.ts b/apps/api/tests/session-run-admission-atomicity.test.ts index 9be8b6dd..5c484648 100644 --- a/apps/api/tests/session-run-admission-atomicity.test.ts +++ b/apps/api/tests/session-run-admission-atomicity.test.ts @@ -1,12 +1,21 @@ import { describe, expect, test } from "bun:test"; -import { parsePlatformId } from "@mosoo/id"; -import type { AgentDeploymentVersionId, SessionId, SessionRunId } from "@mosoo/id"; +import { createPlatformId, parsePlatformId } from "@mosoo/id"; +import type { + AgentDeploymentVersionId, + SessionId, + SessionMessageId, + SessionRunId, +} from "@mosoo/id"; +import { createRuntimeEventSemanticHash } from "@mosoo/runtime-events"; import { API_COMMAND_QUEUE_SEND_FAILED_CODE } from "../src/modules/api-command/application/api-command-ledger"; import { getAccountViewer } from "../src/modules/auth/application/public-api-caller.service"; import type { AuthenticatedViewer } from "../src/modules/auth/application/viewer-auth.service"; import { queueSessionRun } from "../src/modules/runtime/application/session-run.service"; +import { recordCanonicalSessionRunTerminal } from "../src/modules/runtime/application/session-runs/session-run-terminal-failure.service"; +import { createQueuedSessionRunRuntimeEvents } from "../src/modules/runtime/application/session-runs/session-run-view-events.service"; +import { prepareAssistantMessageProjection } from "../src/modules/runtime/infrastructure/driver-instance/assistant-message-projection"; import { setSessionRunStatus } from "../src/modules/runtime/infrastructure/session-runs/session-run-store.repository"; import type { ApiBindings } from "../src/platform/cloudflare/worker-types"; import { API_ERROR_CODE } from "../src/platform/errors"; @@ -18,6 +27,7 @@ import { insertOwnerSession, } from "./helpers/public-api-http-test-fixture"; import type { ApiCommandQueueStub, SqliteD1Database } from "./helpers/public-api-http-test-fixture"; +import { insertRuntimeEvent } from "./public-thread-api-fixtures"; interface AdmissionCounts { apiCommand: number; @@ -186,8 +196,12 @@ async function createFixture() { return { database, viewer }; } -async function completeRun(database: D1Database, runId: SessionRunId): Promise { - for (const status of ["booting", "running", "completed"] as const) { +async function completeRun( + bindings: ApiBindings, + database: SqliteD1Database, + runId: SessionRunId, +): Promise { + for (const status of ["booting", "running"] as const) { const outcome = await setSessionRunStatus(database, { runId, source: "driver", @@ -195,74 +209,200 @@ async function completeRun(database: D1Database, runId: SessionRunId): Promise(); + await insertRuntimeEvent(database, { + kind: "message.added", + occurredAt: 3, + payload: { content: "done", messageId: finalMessageId, role: "agent" }, + runId, + seq: 3, + sessionId: PUBLIC_API_TEST_IDS.ownerSession, + }); + await insertRuntimeEvent(database, { + kind: "message.completed", + occurredAt: 4, + payload: { messageId: finalMessageId, role: "agent" }, + runId, + seq: 4, + sessionId: PUBLIC_API_TEST_IDS.ownerSession, + }); + await database + .prepare("UPDATE session SET runtime_event_seq_cursor = 4 WHERE id = ?") + .bind(PUBLIC_API_TEST_IDS.ownerSession) + .run(); + await recordCanonicalSessionRunTerminal(bindings, { + assistantMessage: prepareAssistantMessageProjection({ + createdByAccountId: PUBLIC_API_TEST_IDS.ownerAccount, + messageId: finalMessageId, + sessionId: PUBLIC_API_TEST_IDS.ownerSession, + sessionRunId: runId, + }), + error: null, + runId, + sessionId: PUBLIC_API_TEST_IDS.ownerSession, + source: "driver", + status: "completed", + timestampMs: 5, + }); } describe("Session Run atomic admission", () => { - test("blocks a cattle follow-up until the previous completed Run has a ready checkpoint", async () => { + test("hashes the exact sourced admission events and keeps replay cursors stable", async () => { const { database, viewer } = await createFixture(); - const apiCommandQueue = createApiCommandQueueStub(); - const bindings = createPublicHttpTestBindings(database, { apiCommandQueue }) as ApiBindings; - await database - .prepare("UPDATE session SET kind = 'cattle' WHERE id = ?") - .bind(PUBLIC_API_TEST_IDS.ownerSession) - .run(); - const first = await queueOwnerRun({ - bindings, - clientRequestId: "checkpoint-run-a", - viewer, + const bindings = createPublicHttpTestBindings(database, { + apiCommandQueue: createApiCommandQueueStub(), + }) as ApiBindings; + const clientRequestId = "semantic-admission-request"; + const prompt = "Admit this request atomically."; + const admitted = await queueOwnerRun({ bindings, clientRequestId, viewer }); + const messageId = await database + .prepare("SELECT id FROM session_message WHERE session_run_id = ?") + .bind(admitted.run.id) + .first("id"); + const storedEvents = await database + .prepare( + `SELECT id, occurred_at, semantic_hash, source_event_id + FROM session_event + WHERE run_id = ? + ORDER BY seq`, + ) + .bind(admitted.run.id) + .all<{ + id: string; + occurred_at: number; + semantic_hash: string; + source_event_id: string; + }>(); + + if (messageId === null || storedEvents.results.length !== 2) { + throw new Error("Admission did not persist its canonical message and events."); + } + + const expectedEvents = createQueuedSessionRunRuntimeEvents({ + prompt, + run: admitted.run, + sessionId: parsePlatformId(PUBLIC_API_TEST_IDS.ownerSession, "fixture session"), + sessionMessageId: parsePlatformId(messageId, "fixture message"), }); - await completeRun(database, first.run.id); + const expectedHashes = await Promise.all( + expectedEvents.map((event, index) => { + const stored = storedEvents.results[index]; + if (stored === undefined) { + throw new Error("Admission event identity is missing."); + } + return createRuntimeEventSemanticHash({ + ...event, + id: parsePlatformId(stored.id, "fixture runtime event"), + occurredAt: new Date(stored.occurred_at).toISOString(), + sourceEventId: stored.source_event_id, + }); + }), + ); - await expect( - database - .prepare("SELECT workspace_checkpoint_required FROM session WHERE id = ?") - .bind(PUBLIC_API_TEST_IDS.ownerSession) - .first("workspace_checkpoint_required"), - ).resolves.toBe(1); + expect(storedEvents.results.map((event) => event.semantic_hash)).toEqual(expectedHashes); + expect(storedEvents.results.map((event) => event.source_event_id)).toEqual([ + clientRequestId, + storedEvents.results[1]?.id, + ]); + const beforeReplay = await readSessionState(database); - await expect( - queueOwnerRun({ bindings, clientRequestId: "checkpoint-run-b", viewer }), - ).rejects.toMatchObject({ - code: API_ERROR_CODE.sessionRunCheckpointPending, - message: expect.stringContaining("still committing its previous workspace checkpoint"), + await expect(queueOwnerRun({ bindings, clientRequestId, viewer })).rejects.toMatchObject({ + code: API_ERROR_CODE.sessionRunClientRequestDuplicate, status: 409, }); + await expect(readSessionState(database)).resolves.toEqual(beforeReplay); + await expect(readAdmissionCounts(database)).resolves.toEqual({ + apiCommand: 1, + event: 2, + message: 1, + run: 1, + }); + }); - database.execute(` + test.each([ + ["rejects a checkpoint owned by another workspace", PUBLIC_API_TEST_IDS.nonOwnerSession, false], + ["accepts its ready checkpoint", PUBLIC_API_TEST_IDS.ownerSession, true], + ] as const)( + "%s before admitting a cattle follow-up", + async (_name, workspaceSessionId, ready) => { + const { database, viewer } = await createFixture(); + const apiCommandQueue = createApiCommandQueueStub(); + const bindings = createPublicHttpTestBindings(database, { apiCommandQueue }) as ApiBindings; + await database + .prepare("UPDATE session SET kind = 'cattle' WHERE id = ?") + .bind(PUBLIC_API_TEST_IDS.ownerSession) + .run(); + const first = await queueOwnerRun({ + bindings, + clientRequestId: "checkpoint-run-a", + viewer, + }); + await completeRun(bindings, database, first.run.id); + + await expect( + database + .prepare("SELECT workspace_checkpoint_required FROM session WHERE id = ?") + .bind(PUBLIC_API_TEST_IDS.ownerSession) + .first("workspace_checkpoint_required"), + ).resolves.toBe(1); + + await expect( + queueOwnerRun({ bindings, clientRequestId: "checkpoint-run-b", viewer }), + ).rejects.toMatchObject({ + code: API_ERROR_CODE.sessionRunCheckpointPending, + message: expect.stringContaining("still committing its previous workspace checkpoint"), + status: 409, + }); + + database.execute(` INSERT INTO sandbox ( - id, kind, subject_kind, subject_id, status, bind_mount_ready, + agent_id, app_id, id, incarnation, kind, network_constraints_hash, + owner_account_id, subject_kind, subject_id, status, bind_mount_ready, global_mounts_json, created_at, updated_at ) VALUES ( - '${PUBLIC_API_TEST_IDS.sandbox}', 'cattle', 'session', '${PUBLIC_API_TEST_IDS.ownerSession}', + '${PUBLIC_API_TEST_IDS.agent}', '${PUBLIC_API_TEST_IDS.app}', + '${PUBLIC_API_TEST_IDS.sandbox}', 1, 'cattle', '${"0".repeat(64)}', + '${PUBLIC_API_TEST_IDS.ownerAccount}', 'session', '${PUBLIC_API_TEST_IDS.ownerSession}', 'active', 1, '[]', 1, 1 ); INSERT INTO sandbox_session ( cloudflare_session_id, created_at, cwd, origin_json, sandbox_id, - session_id, status, updated_at + sandbox_incarnation, session_id, status, updated_at ) VALUES ( '01J0000000000000000000000Z', 1, '/workspace/se/${PUBLIC_API_TEST_IDS.ownerSession}', '{}', - '${PUBLIC_API_TEST_IDS.sandbox}', '${PUBLIC_API_TEST_IDS.ownerSession}', 'active', 1 + '${PUBLIC_API_TEST_IDS.sandbox}', 1, '${PUBLIC_API_TEST_IDS.ownerSession}', 'active', 1 ); INSERT INTO sandbox_backup ( - created_at, dir, id, keep, sandbox_id, session_run_id, status, ttl_seconds, updated_at + created_at, dir, id, keep, sandbox_id, sandbox_incarnation, session_run_id, + staging_id, status, ttl_seconds, updated_at, workspace_session_id ) VALUES ( 1, '/workspace/se/${PUBLIC_API_TEST_IDS.ownerSession}', '${PUBLIC_API_TEST_IDS.operation}', - 0, '${PUBLIC_API_TEST_IDS.sandbox}', '${first.run.id}', 'ready', 315360000, 1 + 0, '${PUBLIC_API_TEST_IDS.sandbox}', 1, '${first.run.id}', + '${PUBLIC_API_TEST_IDS.operation}', 'ready', 315360000, 1, + '${workspaceSessionId}' ); `); - const second = await queueOwnerRun({ - bindings, - clientRequestId: "checkpoint-run-b", - viewer, - }); - expect(second.run.status).toBe("queued"); - }); + const followUp = queueOwnerRun({ + bindings, + clientRequestId: "checkpoint-run-b", + viewer, + }); + if (!ready) { + await expect(followUp).rejects.toMatchObject({ + code: API_ERROR_CODE.sessionRunCheckpointPending, + }); + return; + } + + await expect(followUp).resolves.toMatchObject({ run: { status: "queued" } }); + }, + ); test("grandfathers a completed cattle Run from before the checkpoint rollout", async () => { const { database, viewer } = await createFixture(); @@ -379,6 +519,44 @@ describe("Session Run atomic admission", () => { expect(apiCommandQueue.sent).toHaveLength(1); }); + test("does not admit a Run while provisioning owns the Session", async () => { + const { database, viewer } = await createFixture(); + const apiCommandQueue = createApiCommandQueueStub(); + const bindings = createPublicHttpTestBindings(database, { apiCommandQueue }) as ApiBindings; + + await database + .prepare( + `UPDATE session + SET runtime_provisioning_heartbeat_at = ?, + runtime_provisioning_operation_id = ?, + runtime_provisioning_sandbox_id = ? + WHERE id = ?`, + ) + .bind( + 1, + PUBLIC_API_TEST_IDS.operation, + PUBLIC_API_TEST_IDS.sandbox, + PUBLIC_API_TEST_IDS.ownerSession, + ) + .run(); + + await expect(queueOwnerRun({ bindings, viewer })).rejects.toThrow(); + await expect(readAdmissionCounts(database)).resolves.toEqual({ + apiCommand: 0, + event: 0, + message: 0, + run: 0, + }); + await expect(readSessionState(database)).resolves.toEqual({ + lastMessageAt: null, + lastRunId: null, + messageSeqCursor: 0, + runtimeEventSeqCursor: 0, + status: "IDLE", + }); + expect(apiCommandQueue.sent).toHaveLength(0); + }); + test("classifies a completed client request replay without creating a second Run", async () => { const { database, viewer } = await createFixture(); const apiCommandQueue = createApiCommandQueueStub(); diff --git a/apps/api/tests/session-run-cancel.test.ts b/apps/api/tests/session-run-cancel.test.ts index e6fbd213..b690a99f 100644 --- a/apps/api/tests/session-run-cancel.test.ts +++ b/apps/api/tests/session-run-cancel.test.ts @@ -6,14 +6,20 @@ import type { AccountId, DriverInstanceId, SandboxId, SessionId, SessionRunId } import type { AuthenticatedViewer } from "../src/modules/auth/application/viewer-auth.service"; import { cancelRun } from "../src/modules/runtime/application/session-runs/cancel-run.service"; import { resolvePermissionRequest } from "../src/modules/runtime/application/session-runs/resolve-permission-request.service"; +import { createSessionRunUpdatedEvent } from "../src/modules/runtime/application/session-runs/session-run-view-events.service"; +import { createSessionRunTerminalSourceId } from "../src/modules/runtime/domain/session-run-terminal-event-id"; +import { commitTerminalRunProjection } from "../src/modules/runtime/infrastructure/driver-instance/completed-run-commit.repository"; import { recordRuntimeRunLeaseAcquired } from "../src/modules/runtime/infrastructure/runtime-subject-lifecycle/runtime-run-lease-store"; +import { getSessionRunSummary } from "../src/modules/runtime/infrastructure/session-runs/session-run-store.repository"; import type { ApiBindings } from "../src/platform/cloudflare/worker-types"; import { PUBLIC_API_TEST_IDS, createPublicHttpContractDatabase, createPublicHttpTestBindings, + insertActiveSandboxSessionFixture, insertOwnerSession, } from "./helpers/public-api-http-test-fixture"; +import type { SqliteD1Database } from "./helpers/sqlite-d1"; const OWNER_ACCOUNT_ID = parsePlatformId( "01J00000000000000000000001", @@ -38,7 +44,7 @@ const ownerViewer: AuthenticatedViewer = { name: "Owner", }; -function createDriverConnectionBinding(requests: unknown[]) { +function createDriverConnectionBinding(requests: unknown[], onSend?: () => Promise) { return { get: () => ({ fetch: async (request: Request) => { @@ -47,6 +53,7 @@ function createDriverConnectionBinding(requests: unknown[]) { body, path: new URL(request.url).pathname, }); + await onSend?.(); return Response.json({ ok: true }); }, }), @@ -54,58 +61,49 @@ function createDriverConnectionBinding(requests: unknown[]) { }; } -function withDriverConnection(bindings: ApiBindings, requests: unknown[]): ApiBindings { +function withDriverConnection( + bindings: ApiBindings, + requests: unknown[], + onSend?: () => Promise, +): ApiBindings { return { ...bindings, - DriverConnection: createDriverConnectionBinding(requests) as ApiBindings["DriverConnection"], + DriverConnection: createDriverConnectionBinding( + requests, + onSend, + ) as ApiBindings["DriverConnection"], }; } -async function ensureRuntimeLeaseTables(database: D1Database): Promise { - await database - .prepare( - ` - CREATE TABLE IF NOT EXISTS driver_command ( - acked_at integer, - completed_at integer, - delivery_connection_id text, - driver_instance_id text NOT NULL, - error_json text, - expires_at integer, - id text PRIMARY KEY NOT NULL, - issued_at integer NOT NULL, - kind text NOT NULL, - payload_json text NOT NULL, - result_json text, - seq integer NOT NULL, - status text NOT NULL - ) - `, - ) - .run(); - await database - .prepare( - ` - CREATE TABLE IF NOT EXISTS sandbox ( - id text PRIMARY KEY NOT NULL, - inactive_deadline_at integer, - kind text NOT NULL, - updated_at integer NOT NULL - ) - `, - ) - .run(); - await database - .prepare( - ` - CREATE TABLE IF NOT EXISTS sandbox_session ( - sandbox_id text NOT NULL, - session_id text PRIMARY KEY NOT NULL, - status text NOT NULL - ) - `, - ) - .run(); +async function commitDriverTerminalRun( + database: D1Database, + status: "cancelled" | "completed", + timestampMs = Date.now(), +): Promise { + const current = await getSessionRunSummary(database, RUN_ID); + if (current === null) throw new Error("Missing test Session Run."); + const timestamp = new Date(timestampMs).toISOString(); + const run = { + ...current, + completedAt: timestamp, + startedAt: current.startedAt ?? timestamp, + status, + updatedAt: timestamp, + }; + const kind = status === "completed" ? "run.completed" : "run.cancelled"; + const sourceEventId = createSessionRunTerminalSourceId(RUN_ID, kind); + const event = createSessionRunUpdatedEvent(run, OWNER_SESSION_ID, "IDLE", sourceEventId); + + await commitTerminalRunProjection(database, { + assistantMessage: null, + error: null, + runId: RUN_ID, + sessionId: OWNER_SESSION_ID, + source: "driver", + targetStatus: status, + terminalEvent: { event, occurredAt: timestampMs, sourceEventId }, + timestampMs, + }); } async function insertRunningSessionRun( @@ -157,15 +155,28 @@ async function insertRunningSessionRun( } async function insertRunDriverInstance( - database: D1Database, - input: { bindRun?: boolean; sessionId: SessionId; status?: string }, + database: SqliteD1Database, + input: { + bindRun?: boolean; + kind?: "cattle" | "pet"; + sessionId: SessionId; + status?: string; + }, ): Promise { + await insertActiveSandboxSessionFixture(database, { + kind: input.kind, + ownerAccountId: OWNER_ACCOUNT_ID, + sandboxId: SANDBOX_ID, + sessionId: input.sessionId, + }); await database .prepare( ` INSERT INTO driver_instance ( id, + connection_id, sandbox_id, + sandbox_incarnation, sandbox_session_id, runtime, protocol, @@ -179,12 +190,14 @@ async function insertRunDriverInstance( created_at, updated_at ) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) `, ) .bind( DRIVER_INSTANCE_ID, + "driver-connection-1", SANDBOX_ID, + 1, input.sessionId, "cloudflare-container", "driver-ws", @@ -244,62 +257,19 @@ describe("session run cancel", () => { createdByAccountId: OWNER_ACCOUNT_ID, sessionId: OWNER_SESSION_ID, }); - await ensureRuntimeLeaseTables(database); - await database - .prepare( - ` - INSERT INTO sandbox ( - id, - inactive_deadline_at, - kind, - subject_kind, - subject_id, - status, - created_at, - updated_at - ) - VALUES (?, ?, ?, ?, ?, ?, ?, ?) - `, - ) - .bind(SANDBOX_ID, 1, "cattle", "session", OWNER_SESSION_ID, "active", 1, 1) - .run(); - await database - .prepare( - ` - INSERT INTO sandbox_session ( - cloudflare_session_id, - created_at, - cwd, - origin_json, - sandbox_id, - session_id, - status, - updated_at - ) - VALUES (?, ?, ?, ?, ?, ?, ?, ?) - `, - ) - .bind( - "cloudflare-session-1", - 1, - "/workspace", - "{}", - SANDBOX_ID, - OWNER_SESSION_ID, - "active", - 1, - ) - .run(); await insertRunDriverInstance(database, { bindRun: false, + kind: "cattle", sessionId: OWNER_SESSION_ID, status: "provisioning", }); await expect( recordRuntimeRunLeaseAcquired(database, { + driverGeneration: 0, driverInstanceId: DRIVER_INSTANCE_ID, runtimeSubjectId: SANDBOX_ID, + runtimeSubjectIncarnation: 1, sessionId: OWNER_SESSION_ID, sessionRunId: RUN_ID, }), @@ -315,6 +285,7 @@ describe("session run cancel", () => { const bindings = withDriverConnection( createPublicHttpTestBindings(database) as ApiBindings, driverRequests, + () => commitDriverTerminalRun(database, "cancelled"), ); const result = await cancelRun(bindings, ownerViewer, { @@ -327,6 +298,194 @@ describe("session run cancel", () => { expect(driverRequests).toHaveLength(1); }); + test("adopts a Driver completion that wins the cancellation race", async () => { + const database = await createPublicHttpContractDatabase(); + await insertOwnerSession(database); + await insertRunningSessionRun(database, { + createdByAccountId: OWNER_ACCOUNT_ID, + sessionId: OWNER_SESSION_ID, + }); + await insertRunDriverInstance(database, { + bindRun: true, + sessionId: OWNER_SESSION_ID, + }); + const bindings = withDriverConnection( + createPublicHttpTestBindings(database) as ApiBindings, + [], + () => commitDriverTerminalRun(database, "completed"), + ); + + const result = await cancelRun(bindings, ownerViewer, { + appId: PUBLIC_API_TEST_IDS.app, + runId: RUN_ID, + sessionId: OWNER_SESSION_ID, + }); + + expect(result.run.status).toBe("completed"); + const terminals = await database + .prepare( + "SELECT event_type FROM session_event WHERE run_id = ? AND event_type IN ('run.cancelled', 'run.completed', 'run.failed')", + ) + .bind(RUN_ID) + .all<{ event_type: string }>(); + expect(terminals.results).toEqual([{ event_type: "run.completed" }]); + }); + + test("adopts one canonical legacy terminal projection without advancing cursors", async () => { + const database = await createPublicHttpContractDatabase(); + await insertOwnerSession(database); + await insertRunningSessionRun(database, { + createdByAccountId: OWNER_ACCOUNT_ID, + sessionId: OWNER_SESSION_ID, + }); + await commitDriverTerminalRun(database, "cancelled"); + await database + .prepare( + "UPDATE session_event SET semantic_hash = NULL, terminal_event_json = NULL WHERE run_id = ?", + ) + .bind(RUN_ID) + .run(); + const before = await database + .prepare( + "SELECT message_seq_cursor, runtime_event_seq_cursor, status, status_operation_id, status_seq FROM session WHERE id = ?", + ) + .bind(OWNER_SESSION_ID) + .first(); + + await expect(commitDriverTerminalRun(database, "cancelled")).resolves.toBeUndefined(); + + const after = await database + .prepare( + "SELECT message_seq_cursor, runtime_event_seq_cursor, status, status_operation_id, status_seq FROM session WHERE id = ?", + ) + .bind(OWNER_SESSION_ID) + .first(); + const terminals = await database + .prepare("SELECT semantic_hash, source_event_id FROM session_event WHERE run_id = ?") + .bind(RUN_ID) + .all(); + expect(after).toEqual(before); + expect(terminals.results).toEqual([ + { + semantic_hash: null, + source_event_id: createSessionRunTerminalSourceId(RUN_ID, "run.cancelled"), + }, + ]); + }); + + test("does not move the Session lease timestamp backwards when a terminal event is older", async () => { + const database = await createPublicHttpContractDatabase(); + await insertOwnerSession(database); + await insertRunningSessionRun(database, { + createdByAccountId: OWNER_ACCOUNT_ID, + sessionId: OWNER_SESSION_ID, + }); + await database + .prepare("UPDATE session SET updated_at = ? WHERE id = ?") + .bind(2_000, OWNER_SESSION_ID) + .run(); + + await commitDriverTerminalRun(database, "cancelled", 1_000); + + await expect( + database + .prepare("SELECT updated_at FROM session WHERE id = ?") + .bind(OWNER_SESSION_ID) + .first("updated_at"), + ).resolves.toBe(2_000); + }); + + test("rejects a noncanonical legacy terminal projection", async () => { + const database = await createPublicHttpContractDatabase(); + await insertOwnerSession(database); + await insertRunningSessionRun(database, { + createdByAccountId: OWNER_ACCOUNT_ID, + sessionId: OWNER_SESSION_ID, + }); + await commitDriverTerminalRun(database, "cancelled"); + await database + .prepare( + "UPDATE session_event SET semantic_hash = NULL, terminal_event_json = NULL, source_event_id = 'provider-terminal' WHERE run_id = ?", + ) + .bind(RUN_ID) + .run(); + + await expect(commitDriverTerminalRun(database, "cancelled")).rejects.toThrow( + "Legacy terminal projection conflicts", + ); + }); + + test("synthesizes cancellation only when the Driver control socket is confirmed missing", async () => { + const database = await createPublicHttpContractDatabase(); + await insertOwnerSession(database); + await insertRunningSessionRun(database, { + createdByAccountId: OWNER_ACCOUNT_ID, + sessionId: OWNER_SESSION_ID, + }); + await insertRunDriverInstance(database, { + bindRun: true, + sessionId: OWNER_SESSION_ID, + }); + await database + .prepare("UPDATE driver_instance SET connection_id = NULL WHERE id = ?") + .bind(DRIVER_INSTANCE_ID) + .run(); + const driverRequests: string[] = []; + const bindings = { + ...(createPublicHttpTestBindings(database) as ApiBindings), + DriverConnection: { + get: () => ({ + fetch: async (request: Request) => { + const path = new URL(request.url).pathname; + driverRequests.push(path); + if (path === "/control/send") { + throw new Error("Runtime driver control socket is not connected."); + } + if (path === "/control/fail") { + await database + .prepare("UPDATE driver_instance SET status = 'failed' WHERE id = ?") + .bind(DRIVER_INSTANCE_ID) + .run(); + } + return Response.json({ ok: true }); + }, + }), + idFromName: (name: string) => name, + } as ApiBindings["DriverConnection"], + }; + + const result = await cancelRun(bindings, ownerViewer, { + appId: PUBLIC_API_TEST_IDS.app, + runId: RUN_ID, + sessionId: OWNER_SESSION_ID, + }); + const run = await database + .prepare("SELECT status, status_source FROM session_run WHERE id = ?") + .bind(RUN_ID) + .first<{ status: string; status_source: string }>(); + + expect(result.run.status).toBe("cancelled"); + expect(run).toEqual({ status: "cancelled", status_source: "viewer" }); + expect(driverRequests).toEqual([ + "/control/send", + "/control/send", + "/control/fail", + "/wait/close", + ]); + await expect( + database + .prepare("SELECT status, status_operation_id FROM driver_instance WHERE id = ?") + .bind(DRIVER_INSTANCE_ID) + .first(), + ).resolves.toEqual({ status: "failed", status_operation_id: null }); + await expect( + database + .prepare("SELECT driver_instance_id FROM session_run WHERE id = ?") + .bind(RUN_ID) + .first(), + ).resolves.toEqual({ driver_instance_id: DRIVER_INSTANCE_ID }); + }); + test("resolves permission requests for the App owner through App ownership", async () => { const database = await createPublicHttpContractDatabase(); await insertOwnerSession(database); @@ -350,6 +509,7 @@ describe("session run cancel", () => { driverInstanceId: DRIVER_INSTANCE_ID, appId: PUBLIC_API_TEST_IDS.app, requestId: "permission-1", + runId: RUN_ID, sessionId: OWNER_SESSION_ID, }), ).resolves.toBeUndefined(); diff --git a/apps/api/tests/session-run-lifecycle.test.ts b/apps/api/tests/session-run-lifecycle.test.ts index a64ffb5e..a69030d5 100644 --- a/apps/api/tests/session-run-lifecycle.test.ts +++ b/apps/api/tests/session-run-lifecycle.test.ts @@ -1,6 +1,5 @@ import { describe, expect, test } from "bun:test"; -import { updateSessionLastRun } from "../src/modules/runtime/infrastructure/session-runs/session-run-session.repository"; import { createSessionRunRecordIfSessionIdle, setSessionRunStatus, @@ -8,16 +7,13 @@ import { import { createPublicHttpContractDatabase, insertNonOwnerSession, + PUBLIC_API_TEST_IDS, } from "./helpers/public-api-http-test-fixture"; -async function insertSessionRun( - database: D1Database, - input: { - runId: string; - sessionId?: string; - status: string; - }, -): Promise { +const SESSION_ID = PUBLIC_API_TEST_IDS.nonOwnerSession; +const RUN_ID = "01J0000000000000000000000T"; + +async function insertRunningSessionRun(database: D1Database): Promise { await database .prepare( ` @@ -39,453 +35,141 @@ async function insertSessionRun( `, ) .bind( - input.runId, - input.sessionId ?? "01J0000000000000000000000B", - "01J00000000000000000000009", - "01J00000000000000000000002", + RUN_ID, + SESSION_ID, + PUBLIC_API_TEST_IDS.agent, + PUBLIC_API_TEST_IDS.nonOwnerAccount, "user_prompt", - input.status, + "running", "openai", "gpt-5.4", "openai-runtime", - `trace-${input.runId}`, + `trace-${RUN_ID}`, 1, 1, ) .run(); await database - .prepare("UPDATE session SET last_run_id = ?, status = ?, updated_at = ? WHERE id = ?") - .bind( - input.runId, - input.status === "completed" ? "IDLE" : "RUNNING", - 1, - "01J0000000000000000000000B", - ) + .prepare("UPDATE session SET last_run_id = ?, status = 'RUNNING', updated_at = 1 WHERE id = ?") + .bind(RUN_ID, SESSION_ID) .run(); } -function failSessionProjectionStatementInBatch(database: D1Database): D1Database { - return new Proxy(database, { - get(target, property, receiver) { - if (property === "batch") { - return async (statements: D1PreparedStatement[]) => { - const firstStatement = statements[0]; - - if (firstStatement === undefined) { - throw new Error("Expected a Run status statement in the D1 batch."); - } - - const failingStatement = new Proxy(target.prepare("SELECT 1"), { - get(statement, statementProperty, statementReceiver) { - if (statementProperty === "run") { - return async () => { - throw new Error("injected Session lifecycle projection failure"); - }; - } - - const value = Reflect.get(statement, statementProperty, statementReceiver); - return typeof value === "function" ? value.bind(statement) : value; - }, - }); - - return target.batch([firstStatement, failingStatement]); - }; - } - - const value = Reflect.get(target, property, receiver); - return typeof value === "function" ? value.bind(target) : value; - }, - }); -} - -function advanceRunBeforeBatch( - database: D1Database, - input: { - readonly runId: string; - }, -): D1Database { - let advanced = false; - - return new Proxy(database, { - get(target, property, receiver) { - if (property === "batch") { - return async (statements: D1PreparedStatement[]) => { - if (!advanced) { - advanced = true; - await setSessionRunStatus(target, { - runId: input.runId, - source: "driver", - status: "running", - }); - } - - return target.batch(statements); - }; - } - - const value = Reflect.get(target, property, receiver); - return typeof value === "function" ? value.bind(target) : value; - }, - }); +function createRunInput() { + return { + agentId: PUBLIC_API_TEST_IDS.agent, + createdBy: PUBLIC_API_TEST_IDS.nonOwnerAccount, + model: "gpt-5.4", + provider: "openai", + runtimeId: "openai-runtime", + sessionId: SESSION_ID, + status: "queued" as const, + trigger: "user_prompt" as const, + }; } -describe("session run lifecycle", () => { - test("emits one structured business log for an applied terminal transition", async () => { +describe("Session Run non-terminal lifecycle writer", () => { + test("fails closed when an untyped caller attempts a terminal transition", async () => { const database = await createPublicHttpContractDatabase(); await insertNonOwnerSession(database); - await insertSessionRun(database, { - runId: "run-terminal-log", - status: "running", - }); - - const originalConsoleInfo = console.info; - const output: string[] = []; - console.info = (...values: unknown[]) => output.push(values.map(String).join(" ")); - - try { - await setSessionRunStatus(database, { - runId: "run-terminal-log", - source: "driver", - status: "completed", - }); - await setSessionRunStatus(database, { - runId: "run-terminal-log", - source: "driver", - status: "completed", - }); - } finally { - console.info = originalConsoleInfo; - } + await insertRunningSessionRun(database); - const terminalEntries = output - .map((entry) => JSON.parse(entry)) - .filter((entry) => entry.message === "session.run.terminal"); - - expect(terminalEntries).toHaveLength(1); - expect(terminalEntries[0]).toMatchObject({ - level: "info", - metadata: { - errorCode: null, - runId: "run-terminal-log", - runtimeId: "openai-runtime", - sessionType: "ui", + await expect( + setSessionRunStatus(database, { + runId: RUN_ID, source: "driver", status: "completed", - traceId: "trace-run-terminal-log", - trigger: "user_prompt", - }, - namespace: "api", - }); - expect(terminalEntries[0]?.metadata.durationMs).toBeGreaterThanOrEqual(0); + } as never), + ).rejects.toThrow("atomic terminal projection"); + + expect( + await database + .prepare("SELECT status, status_seq FROM session_run WHERE id = ?") + .bind(RUN_ID) + .first(), + ).toEqual({ status: "running", status_seq: 0 }); }); - test("does not let a stale terminal event revive or overwrite a completed run", async () => { + test("keeps duplicate active transitions idempotent", async () => { const database = await createPublicHttpContractDatabase(); await insertNonOwnerSession(database); - await insertSessionRun(database, { - runId: "run-terminal", - status: "running", - }); + await insertRunningSessionRun(database); - await setSessionRunStatus(database, { - runId: "run-terminal", - source: "driver", - status: "completed", - }); - await setSessionRunStatus(database, { - error: { - code: "runtime.late_failure", - details: {}, - message: "Late failure.", - retryable: false, - }, - runId: "run-terminal", - source: "driver", - status: "failed", - }); - - const row = await database - .prepare( - ` - SELECT error_code, status - FROM session_run - WHERE id = ? - `, - ) - .bind("run-terminal") - .first<{ - error_code: string | null; - status: string; - }>(); - expect(row).toEqual({ - error_code: null, - status: "completed", - }); - }); - - test("leaves duplicate transitions idempotent", async () => { - const database = await createPublicHttpContractDatabase(); - await insertNonOwnerSession(database); - await insertSessionRun(database, { - runId: "run-duplicate", - status: "running", - }); - - await setSessionRunStatus(database, { - runId: "run-duplicate", + const outcome = await setSessionRunStatus(database, { + runId: RUN_ID, source: "driver", status: "running", }); - const row = await database - .prepare("SELECT status FROM session_run WHERE id = ?") - .bind("run-duplicate") - .first<{ status: string }>(); - expect(row).toEqual({ status: "running" }); + expect(outcome.kind).toBe("duplicate"); + expect( + await database + .prepare("SELECT status, status_seq FROM session_run WHERE id = ?") + .bind(RUN_ID) + .first(), + ).toEqual({ status: "running", status_seq: 0 }); }); - test("rejects new runs after the owning session is terminated", async () => { + test("does not advance a current Run after its Session is terminated", async () => { const database = await createPublicHttpContractDatabase(); await insertNonOwnerSession(database); + await insertRunningSessionRun(database); await database - .prepare("UPDATE session SET status = ? WHERE id = ?") - .bind("TERMINATED", "01J0000000000000000000000B") + .prepare("UPDATE session_run SET status = 'booting' WHERE id = ?") + .bind(RUN_ID) .run(); - - await expect( - createSessionRunRecordIfSessionIdle(database, { - agentId: "01J00000000000000000000009", - createdBy: "01J00000000000000000000002", - model: "gpt-5.4", - provider: "openai", - runtimeId: "openai-runtime", - sessionId: "01J0000000000000000000000B", - status: "queued", - trigger: "user_prompt", - }), - ).rejects.toThrow(); - }); - - test("rejects new runs while a runtime operation owns the session", async () => { - const database = await createPublicHttpContractDatabase(); - await insertNonOwnerSession(database); await database - .prepare( - ` - UPDATE session - SET status = ?, status_operation_id = ? - WHERE id = ? - `, - ) - .bind("RESCHEDULING", "01J0000000000000000000000R", "01J0000000000000000000000B") + .prepare("UPDATE session SET status = 'TERMINATED' WHERE id = ?") + .bind(SESSION_ID) .run(); - await expect( - createSessionRunRecordIfSessionIdle(database, { - agentId: "01J00000000000000000000009", - createdBy: "01J00000000000000000000002", - model: "gpt-5.4", - provider: "openai", - runtimeId: "openai-runtime", - sessionId: "01J0000000000000000000000B", - status: "queued", - trigger: "user_prompt", - }), - ).rejects.toThrow(); - }); - - test("session run projections expose the session as idle after completion", async () => { - const database = await createPublicHttpContractDatabase(); - await insertNonOwnerSession(database); - const run = await createSessionRunRecordIfSessionIdle(database, { - agentId: "01J00000000000000000000009", - createdBy: "01J00000000000000000000002", - model: "gpt-5.4", - provider: "openai", - runtimeId: "openai-runtime", - sessionId: "01J0000000000000000000000B", - status: "running", - trigger: "user_prompt", - }); - if (run.createdRun === null) { - throw new Error("Expected session run creation."); - } - - await setSessionRunStatus(database, { - runId: run.createdRun.id, + const outcome = await setSessionRunStatus(database, { + runId: RUN_ID, source: "driver", - status: "completed", - }); - - const row = await database - .prepare( - ` - SELECT status, status_operation_id - FROM session - WHERE id = ? - `, - ) - .bind("01J0000000000000000000000B") - .first<{ - status: string; - status_operation_id: string | null; - }>(); - - expect(row).toEqual({ - status: "IDLE", - status_operation_id: null, - }); - }); - - test("rolls back a terminal Run transition when its Session projection fails", async () => { - const database = await createPublicHttpContractDatabase(); - await insertNonOwnerSession(database); - await insertSessionRun(database, { - runId: "run-atomic-terminal-projection", status: "running", }); - await expect( - setSessionRunStatus(failSessionProjectionStatementInBatch(database), { - runId: "run-atomic-terminal-projection", - source: "driver", - status: "completed", - }), - ).rejects.toThrow("injected Session lifecycle projection failure"); - - const interrupted = await database - .prepare( - ` - SELECT session.status AS session_status, session_run.status AS run_status - FROM session - INNER JOIN session_run ON session_run.id = session.last_run_id - WHERE session.id = ? - `, - ) - .bind("01J0000000000000000000000B") - .first<{ run_status: string; session_status: string }>(); - - expect(interrupted).toEqual({ - run_status: "running", - session_status: "RUNNING", - }); - - await setSessionRunStatus(database, { - runId: "run-atomic-terminal-projection", - source: "driver", - status: "completed", - }); - - const admitted = await createSessionRunRecordIfSessionIdle(database, { - agentId: "01J00000000000000000000009", - createdBy: "01J00000000000000000000002", - model: "gpt-5.4", - provider: "openai", - runtimeId: "openai-runtime", - sessionId: "01J0000000000000000000000B", - status: "queued", - trigger: "user_prompt", + expect(outcome.kind).toBe("stale"); + expect( + await database + .prepare( + `SELECT session.status AS session_status, + session.status_seq AS session_status_seq, + session_run.status AS run_status, + session_run.status_seq AS run_status_seq + FROM session + JOIN session_run ON session_run.id = session.last_run_id + WHERE session.id = ?`, + ) + .bind(SESSION_ID) + .first(), + ).toEqual({ + run_status: "booting", + run_status_seq: 0, + session_status: "TERMINATED", + session_status_seq: 0, }); - - expect(admitted.createdRun).not.toBeNull(); }); - test("does not project a stale terminal transition onto a newer Run state", async () => { + test("rejects admission after the Session is terminated", async () => { const database = await createPublicHttpContractDatabase(); await insertNonOwnerSession(database); - await insertSessionRun(database, { - runId: "run-stale-terminal-projection", - status: "booting", - }); - - const outcome = await setSessionRunStatus( - advanceRunBeforeBatch(database, { - runId: "run-stale-terminal-projection", - }), - { - error: { - code: "runtime.stale_terminal", - details: {}, - message: "The stale terminal transition must not update the Session.", - retryable: false, - }, - runId: "run-stale-terminal-projection", - source: "driver", - status: "failed", - }, - ); - - expect(outcome).toMatchObject({ - kind: "stale", - reason: "concurrent_transition", - }); - const current = await database - .prepare( - ` - SELECT session.status AS session_status, session_run.status AS run_status - FROM session - INNER JOIN session_run ON session_run.id = session.last_run_id - WHERE session.id = ? - `, - ) - .bind("01J0000000000000000000000B") - .first<{ run_status: string; session_status: string }>(); + await database + .prepare("UPDATE session SET status = 'TERMINATED' WHERE id = ?") + .bind(SESSION_ID) + .run(); - expect(current).toEqual({ - run_status: "running", - session_status: "RUNNING", - }); + await expect(createSessionRunRecordIfSessionIdle(database, createRunInput())).rejects.toThrow(); }); - test("does not revive terminated sessions from stale run projections", async () => { + test("rejects admission while another operation owns the Session", async () => { const database = await createPublicHttpContractDatabase(); await insertNonOwnerSession(database); - await insertSessionRun(database, { - runId: "run-stale-session", - status: "running", - }); await database - .prepare("UPDATE session SET status = ? WHERE id = ?") - .bind("TERMINATED", "01J0000000000000000000000B") + .prepare("UPDATE session SET status = 'RESCHEDULING', status_operation_id = ? WHERE id = ?") + .bind(PUBLIC_API_TEST_IDS.operation, SESSION_ID) .run(); - await expect( - updateSessionLastRun(database, { - model: "gpt-5.4", - provider: "openai", - runId: "run-stale-session", - sessionId: "01J0000000000000000000000B", - timestampMs: 2, - }), - ).resolves.toBe(false); - await setSessionRunStatus(database, { - error: { - code: "runtime.stale_session", - details: {}, - message: "Stale session.", - retryable: false, - }, - preserveSessionLifecycle: true, - runId: "run-stale-session", - source: "maintenance", - status: "failed", - }); - - const row = await database - .prepare( - ` - SELECT session.status AS session_status, session_run.status AS run_status - FROM session - INNER JOIN session_run ON session_run.id = session.last_run_id - WHERE session.id = ? - `, - ) - .bind("01J0000000000000000000000B") - .first<{ run_status: string; session_status: string }>(); - - expect(row).toEqual({ - run_status: "failed", - session_status: "TERMINATED", - }); + await expect(createSessionRunRecordIfSessionIdle(database, createRunInput())).rejects.toThrow(); }); }); diff --git a/apps/api/tests/session-run-read.test.ts b/apps/api/tests/session-run-read.test.ts index 39b4958f..19fb8a24 100644 --- a/apps/api/tests/session-run-read.test.ts +++ b/apps/api/tests/session-run-read.test.ts @@ -7,6 +7,7 @@ import { import { createPublicHttpContractDatabase, insertNonOwnerSession, + PUBLIC_API_TEST_IDS, } from "./helpers/public-api-http-test-fixture"; async function insertQueuedSessionRun( @@ -16,7 +17,7 @@ async function insertQueuedSessionRun( id?: string; } = {}, ): Promise { - const id = input.id ?? "run-active-probe"; + const id = input.id ?? PUBLIC_API_TEST_IDS.run; const createdAt = input.createdAt ?? 1; await database @@ -68,11 +69,11 @@ describe("session run reads", () => { test("loads the latest active run id", async () => { const database = await createPublicHttpContractDatabase(); await insertNonOwnerSession(database); - await insertQueuedSessionRun(database, { createdAt: 1, id: "run-active-probe-old" }); - await insertQueuedSessionRun(database, { createdAt: 2, id: "run-active-probe-latest" }); + await insertQueuedSessionRun(database, { createdAt: 1, id: PUBLIC_API_TEST_IDS.run }); + await insertQueuedSessionRun(database, { createdAt: 2, id: PUBLIC_API_TEST_IDS.runAlt }); await expect(getActiveSessionRunId(database, "01J0000000000000000000000B")).resolves.toBe( - "run-active-probe-latest", + PUBLIC_API_TEST_IDS.runAlt, ); }); }); diff --git a/apps/api/tests/session-run-reconciliation.test.ts b/apps/api/tests/session-run-reconciliation.test.ts index 90aea293..19126974 100644 --- a/apps/api/tests/session-run-reconciliation.test.ts +++ b/apps/api/tests/session-run-reconciliation.test.ts @@ -9,7 +9,9 @@ import { RUNTIME_SOCKET_TIMEOUT_MS, } from "../src/modules/runtime/domain/runtime-config"; import { + PUBLIC_API_TEST_IDS, createPublicHttpContractDatabase, + insertActiveSandboxSessionFixture, insertNonOwnerSession, } from "./helpers/public-api-http-test-fixture"; @@ -17,6 +19,11 @@ describe("session run reconciliation", () => { test("keeps connecting runs alive for the cold ready budget", async () => { const database = await createPublicHttpContractDatabase(); await insertNonOwnerSession(database); + await insertActiveSandboxSessionFixture(database, { + ownerAccountId: PUBLIC_API_TEST_IDS.nonOwnerAccount, + sandboxId: PUBLIC_API_TEST_IDS.sandbox, + sessionId: PUBLIC_API_TEST_IDS.nonOwnerSession, + }); const driverId = "01J0000000000000000000000E"; const runId = "01J0000000000000000000000N"; @@ -26,6 +33,7 @@ describe("session run reconciliation", () => { INSERT INTO driver_instance ( id, sandbox_id, + sandbox_incarnation, sandbox_session_id, runtime, protocol, @@ -39,12 +47,13 @@ describe("session run reconciliation", () => { created_at, updated_at ) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) `, ) .bind( driverId, "01J0000000000000000000000D", + 1, "01J0000000000000000000000B", "cloudflare-container", "driver-ws", @@ -140,7 +149,7 @@ describe("session run reconciliation", () => { `, ) .bind( - "run-stale", + PUBLIC_API_TEST_IDS.run, "01J0000000000000000000000B", "01J00000000000000000000009", "01J00000000000000000000002", @@ -156,7 +165,7 @@ describe("session run reconciliation", () => { .run(); await database .prepare("UPDATE session SET last_run_id = ?, status = ? WHERE id = ?") - .bind("run-stale", "RUNNING", "01J0000000000000000000000B") + .bind(PUBLIC_API_TEST_IDS.run, "RUNNING", "01J0000000000000000000000B") .run(); await expect( @@ -165,7 +174,7 @@ describe("session run reconciliation", () => { const run = await database .prepare("SELECT error_code, status FROM session_run WHERE id = ?") - .bind("run-stale") + .bind(PUBLIC_API_TEST_IDS.run) .first<{ error_code: string | null; status: string }>(); expect(run).toMatchObject({ status: "failed", @@ -173,6 +182,108 @@ describe("session run reconciliation", () => { expect(run?.error_code).toBeString(); }); + test("does not fail a stale candidate after its Driver heartbeat recovers", async () => { + const database = await createPublicHttpContractDatabase(); + await insertNonOwnerSession(database); + await insertActiveSandboxSessionFixture(database, { + ownerAccountId: PUBLIC_API_TEST_IDS.nonOwnerAccount, + sandboxId: PUBLIC_API_TEST_IDS.sandbox, + sessionId: PUBLIC_API_TEST_IDS.nonOwnerSession, + }); + const driverId = "01J0000000000000000000000E"; + const runId = PUBLIC_API_TEST_IDS.run; + const staleAt = Date.now() - RUNTIME_SOCKET_TIMEOUT_MS - 1_000; + + await database + .prepare( + `INSERT INTO driver_instance ( + id, sandbox_id, sandbox_incarnation, sandbox_session_id, runtime, protocol, protocol_version, status, + boot_token_hash, boot_token_expires_at, generation, heartbeat_count, + last_heartbeat_at, expires_at, created_at, updated_at + ) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, + ) + .bind( + driverId, + "01J0000000000000000000000D", + 1, + "01J0000000000000000000000B", + "cloudflare-container", + "driver-ws", + 1, + "ready", + new Uint8Array([1]), + Date.now() + 10_000, + 0, + 1, + staleAt, + Date.now() + 20_000, + 1, + staleAt, + ) + .run(); + await database + .prepare( + `INSERT INTO session_run ( + id, session_id, agent_id, created_by_account_id, trigger, status, provider, model, + runtime_id, trace_id, driver_instance_id, created_at, updated_at + ) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, + ) + .bind( + runId, + "01J0000000000000000000000B", + "01J00000000000000000000009", + "01J00000000000000000000002", + "user_prompt", + "running", + "openai", + "gpt-5.4", + "openai-runtime", + "trace-heartbeat-race", + driverId, + 1, + 1, + ) + .run(); + await database + .prepare("UPDATE session SET last_run_id = ?, status = ? WHERE id = ?") + .bind(runId, "RUNNING", "01J0000000000000000000000B") + .run(); + + let raced = false; + const racingDatabase = new Proxy(database, { + get(target, property) { + if (property === "batch") { + return async (statements: D1PreparedStatement[]) => { + if (!raced) { + raced = true; + const recoveredAt = Date.now(); + await target + .prepare( + "UPDATE driver_instance SET heartbeat_count = heartbeat_count + 1, last_heartbeat_at = ?, updated_at = ? WHERE id = ?", + ) + .bind(recoveredAt, recoveredAt, driverId) + .run(); + } + return target.batch(statements); + }; + } + + const value = Reflect.get(target, property); + return typeof value === "function" ? value.bind(target) : value; + }, + }) as D1Database; + + await expect( + reconcileStaleActiveSessionRun(racingDatabase, "01J0000000000000000000000B"), + ).resolves.toBe(false); + expect(raced).toBe(true); + await expect( + database.prepare("SELECT status FROM session_run WHERE id = ?").bind(runId).first(), + ).resolves.toEqual({ status: "running" }); + }); + test("reconciles stale active runs in batches", async () => { const database = await createPublicHttpContractDatabase(); await insertNonOwnerSession(database); @@ -198,7 +309,7 @@ describe("session run reconciliation", () => { `, ) .bind( - "run-stale", + PUBLIC_API_TEST_IDS.run, "01J0000000000000000000000B", "01J00000000000000000000009", "01J00000000000000000000002", @@ -214,7 +325,7 @@ describe("session run reconciliation", () => { .run(); await database .prepare("UPDATE session SET last_run_id = ?, status = ? WHERE id = ?") - .bind("run-stale", "RUNNING", "01J0000000000000000000000B") + .bind(PUBLIC_API_TEST_IDS.run, "RUNNING", "01J0000000000000000000000B") .run(); await expect( @@ -222,13 +333,13 @@ describe("session run reconciliation", () => { limit: 10, }), ).resolves.toEqual({ - reconciledRunIds: ["run-stale"], + reconciledRunIds: [PUBLIC_API_TEST_IDS.run], reconciledSessionIds: ["01J0000000000000000000000B"], }); const run = await database .prepare("SELECT error_code, status FROM session_run WHERE id = ?") - .bind("run-stale") + .bind(PUBLIC_API_TEST_IDS.run) .first<{ error_code: string | null; status: string }>(); expect(run).toMatchObject({ status: "failed", diff --git a/apps/api/tests/session-run-skill-persistence.test.ts b/apps/api/tests/session-run-skill-persistence.test.ts index f84425bb..4ff0317a 100644 --- a/apps/api/tests/session-run-skill-persistence.test.ts +++ b/apps/api/tests/session-run-skill-persistence.test.ts @@ -4,9 +4,10 @@ import { persistSessionRunSkills } from "../src/modules/runtime/application/sess import { createPublicHttpContractDatabase, insertNonOwnerSession, + PUBLIC_API_TEST_IDS, } from "./helpers/public-api-http-test-fixture"; -const RUN_ID = "run-skill-persistence"; +const RUN_ID = PUBLIC_API_TEST_IDS.run; async function insertQueuedSessionRun(database: D1Database): Promise { await database diff --git a/apps/api/tests/session-run-state.test.ts b/apps/api/tests/session-run-state.test.ts index 3cdb5263..18660f29 100644 --- a/apps/api/tests/session-run-state.test.ts +++ b/apps/api/tests/session-run-state.test.ts @@ -1,14 +1,14 @@ import { describe, expect, test } from "bun:test"; -import { - acquireSessionRunDispatch, - updateSessionRunStatusIfActive, -} from "../src/modules/runtime/application/session-runs/session-run-state.repository"; +import { acquireSessionRunDispatch } from "../src/modules/runtime/application/session-runs/session-run-state.repository"; import { createPublicHttpContractDatabase, insertNonOwnerSession, + PUBLIC_API_TEST_IDS, } from "./helpers/public-api-http-test-fixture"; +const RUN_ID = PUBLIC_API_TEST_IDS.run; + async function insertQueuedSessionRun(database: D1Database): Promise { await database .prepare( @@ -31,7 +31,7 @@ async function insertQueuedSessionRun(database: D1Database): Promise { `, ) .bind( - "run-active-transition", + RUN_ID, "01J0000000000000000000000B", "01J00000000000000000000009", "01J00000000000000000000002", @@ -47,46 +47,26 @@ async function insertQueuedSessionRun(database: D1Database): Promise { .run(); await database .prepare("UPDATE session SET last_run_id = ?, status = ?, updated_at = ? WHERE id = ?") - .bind("run-active-transition", "RUNNING", 1, "01J0000000000000000000000B") + .bind(RUN_ID, "RUNNING", 1, "01J0000000000000000000000B") .run(); } describe("session run state", () => { - test("returns active status transitions", async () => { - const database = await createPublicHttpContractDatabase(); - await insertNonOwnerSession(database); - await insertQueuedSessionRun(database); - - const run = await updateSessionRunStatusIfActive(database, { - runId: "run-active-transition", - status: "booting", - }); - - expect(run?.id).toBe("run-active-transition"); - expect(run?.status).toBe("booting"); - - const stored = await database - .prepare("SELECT status FROM session_run WHERE id = ?") - .bind("run-active-transition") - .first<{ status: string }>(); - expect(stored).toEqual({ status: "booting" }); - }); - test("only the first dispatch acquire can continue a run", async () => { const database = await createPublicHttpContractDatabase(); await insertNonOwnerSession(database); await insertQueuedSessionRun(database); - const first = await acquireSessionRunDispatch(database, "run-active-transition"); - const second = await acquireSessionRunDispatch(database, "run-active-transition"); + const first = await acquireSessionRunDispatch(database, RUN_ID); + const second = await acquireSessionRunDispatch(database, RUN_ID); - expect(first?.id).toBe("run-active-transition"); + expect(first?.id).toBe(RUN_ID); expect(first?.status).toBe("booting"); expect(second).toBeNull(); const stored = await database .prepare("SELECT status FROM session_run WHERE id = ?") - .bind("run-active-transition") + .bind(RUN_ID) .first<{ status: string }>(); expect(stored).toEqual({ status: "booting" }); }); diff --git a/apps/api/tests/session-run-terminal-failure.test.ts b/apps/api/tests/session-run-terminal-failure.test.ts index 54b023a0..e251b419 100644 --- a/apps/api/tests/session-run-terminal-failure.test.ts +++ b/apps/api/tests/session-run-terminal-failure.test.ts @@ -1,23 +1,35 @@ import { describe, expect, test } from "bun:test"; -import type { DriverInstanceId, SessionRunId } from "@mosoo/id"; +import type { DriverInstanceId, SessionMessageId, SessionRunId } from "@mosoo/id"; -import { recordCanonicalSessionRunFailure } from "../src/modules/runtime/application/session-runs/session-run-terminal-failure.service"; -import { reconcileTerminalSessionRuns } from "../src/modules/runtime/application/session-runs/terminal-run-reconciliation.service"; +import { + recordCanonicalSessionRunFailure, + recordCanonicalSessionRunTerminal, +} from "../src/modules/runtime/application/session-runs/session-run-terminal-failure.service"; +import { createSessionRunUpdatedEvent } from "../src/modules/runtime/application/session-runs/session-run-view-events.service"; +import { + assertCanonicalTerminalSessionRunProjection, + reconcileTerminalSessionRuns, +} from "../src/modules/runtime/application/session-runs/terminal-run-reconciliation.service"; +import { createSessionRunTerminalSourceId } from "../src/modules/runtime/domain/session-run-terminal-event-id"; +import { commitTerminalRunProjection } from "../src/modules/runtime/infrastructure/driver-instance/completed-run-commit.repository"; import { getRuntimeSessionLink } from "../src/modules/runtime/infrastructure/driver-instance/session-link.repository"; import { recordDriverInstanceFailure } from "../src/modules/runtime/infrastructure/driver-instance/terminal-driver-events"; -import { setSessionRunStatus } from "../src/modules/runtime/infrastructure/session-runs/session-run-store.repository"; +import { getSessionRunSummary } from "../src/modules/runtime/infrastructure/session-runs/session-run-store.repository"; import type { ApiBindings } from "../src/platform/cloudflare/worker-types"; import { createPublicHttpContractDatabase, createPublicHttpTestBindings, + insertActiveSandboxSessionFixture, insertOwnerSession, PUBLIC_API_TEST_IDS, } from "./helpers/public-api-http-test-fixture"; import type { SqliteD1Database } from "./helpers/public-api-http-test-fixture"; +import { insertRuntimeEvent } from "./public-thread-api-fixtures"; const RUN_ID = PUBLIC_API_TEST_IDS.run as SessionRunId; const DRIVER_ID = PUBLIC_API_TEST_IDS.driverOwner as DriverInstanceId; +const FINAL_MESSAGE_ID = "01J0000000000000000000000M" as SessionMessageId; const CANONICAL_FAILURE_SOURCE_ID = `session-run-terminal:${RUN_ID}:run.failed`; const DRIVER_ERROR = { code: "driver.command_failed", @@ -38,11 +50,62 @@ interface FailureEventRow { source_event_id: string; } +function advanceRunAfterFirstSummaryRead( + database: SqliteD1Database, + advance: () => Promise, +): { database: D1Database; advanced: () => boolean } { + let advanced = false; + + function wrap(statement: D1PreparedStatement): D1PreparedStatement { + return new Proxy(statement, { + get(target, property, receiver) { + if (property === "bind") { + return (...values: unknown[]) => wrap(target.bind(...values)); + } + if (property === "raw") { + return async () => { + const rows = await target.raw(); + if (!advanced) { + advanced = true; + await advance(); + } + return rows; + }; + } + return Reflect.get(target, property, receiver); + }, + }); + } + + return { + advanced: () => advanced, + database: new Proxy(database, { + get(target, property) { + if (property === "prepare") { + return (query: string) => + query.includes('from "session_run"') + ? wrap(target.prepare(query)) + : target.prepare(query); + } + + const value = Reflect.get(target, property); + return typeof value === "function" ? value.bind(target) : value; + }, + }) as D1Database, + }; +} + async function insertLinkedRunFixture( database: SqliteD1Database, status: "booting" | "completed" | "cancelled" | "failed" = "booting", ): Promise { await insertOwnerSession(database); + await insertActiveSandboxSessionFixture(database, { + ownerAccountId: PUBLIC_API_TEST_IDS.ownerAccount, + sandboxId: PUBLIC_API_TEST_IDS.sandbox, + sessionId: PUBLIC_API_TEST_IDS.ownerSession, + timestampMs: 1, + }); await database .prepare( ` @@ -57,11 +120,12 @@ async function insertLinkedRunFixture( protocol_version, runtime, sandbox_id, + sandbox_incarnation, sandbox_session_id, status, updated_at ) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) `, ) .bind( @@ -75,6 +139,7 @@ async function insertLinkedRunFixture( 1, "openai-runtime", PUBLIC_API_TEST_IDS.sandbox, + 1, PUBLIC_API_TEST_IDS.ownerSession, "ready", 1, @@ -102,10 +167,11 @@ async function insertLinkedRunFixture( error_code, error_message, error_details_json, + error_retryable, created_at, updated_at ) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) `, ) .bind( @@ -127,16 +193,48 @@ async function insertLinkedRunFixture( status === "failed" ? DRIVER_ERROR.code : null, status === "failed" ? DRIVER_ERROR.message : null, status === "failed" ? "{}" : null, + status === "failed" ? 0 : null, 1, 1, ) .run(); await database - .prepare("UPDATE session SET last_run_id = ?, status = ? WHERE id = ?") + .prepare("UPDATE session SET last_run_id = ?, status = ?, updated_at = 1 WHERE id = ?") .bind(RUN_ID, status === "booting" ? "RUNNING" : "IDLE", PUBLIC_API_TEST_IDS.ownerSession) .run(); } +async function insertHealthyFailedRunFixture(database: SqliteD1Database): Promise { + const runId = PUBLIC_API_TEST_IDS.runAlt as SessionRunId; + await database + .prepare( + `INSERT INTO session_run ( + id, session_id, agent_id, created_by_account_id, deployment_version_id, + deployment_version_number, driver_instance_id, trigger, status, provider, + model, runtime_id, trace_id, started_at, completed_at, error_code, + error_message, error_details_json, error_retryable, created_at, updated_at + ) VALUES (?, ?, ?, ?, ?, 1, ?, 'user_prompt', 'failed', 'openai', + 'gpt-5.4', 'openai-runtime', 'trace-healthy-terminal-repair', 2, 2, + ?, ?, '{}', 0, 2, 2)`, + ) + .bind( + runId, + PUBLIC_API_TEST_IDS.ownerSession, + PUBLIC_API_TEST_IDS.agent, + PUBLIC_API_TEST_IDS.ownerAccount, + PUBLIC_API_TEST_IDS.deployment, + DRIVER_ID, + PROVISION_ERROR.code, + PROVISION_ERROR.message, + ) + .run(); + await database + .prepare("UPDATE session SET last_run_id = ?, status = 'RUNNING', updated_at = 2 WHERE id = ?") + .bind(runId, PUBLIC_API_TEST_IDS.ownerSession) + .run(); + return runId; +} + async function readFailureEvents(database: SqliteD1Database): Promise { return database .prepare( @@ -152,7 +250,255 @@ async function readFailureEvents(database: SqliteD1Database): Promise result.results ?? []); } +async function insertSealedAssistantAuthority(database: SqliteD1Database): Promise { + await insertRuntimeEvent(database, { + kind: "message.added", + occurredAt: 1, + payload: { content: "done", messageId: FINAL_MESSAGE_ID, role: "agent" }, + runId: RUN_ID, + seq: 1, + sessionId: PUBLIC_API_TEST_IDS.ownerSession, + }); + await insertRuntimeEvent(database, { + kind: "message.completed", + occurredAt: 2, + payload: { messageId: FINAL_MESSAGE_ID, role: "agent" }, + runId: RUN_ID, + seq: 2, + sessionId: PUBLIC_API_TEST_IDS.ownerSession, + }); + await database + .prepare( + ` + INSERT INTO session_message ( + content_text, + created_at, + created_by_account_id, + id, + plan_json, + projection_format, + role, + segments_json, + seq, + session_id, + session_run_id + ) + VALUES ('', 1, ?, ?, NULL, 'event_stream_v3', 'assistant', NULL, 1, ?, ?) + `, + ) + .bind( + PUBLIC_API_TEST_IDS.ownerAccount, + FINAL_MESSAGE_ID, + PUBLIC_API_TEST_IDS.ownerSession, + RUN_ID, + ) + .run(); + await database + .prepare("UPDATE session SET message_seq_cursor = 1, runtime_event_seq_cursor = 2 WHERE id = ?") + .bind(PUBLIC_API_TEST_IDS.ownerSession) + .run(); +} + +async function insertParentlessToolOutput( + database: SqliteD1Database, + eventSeq: number, +): Promise { + await insertRuntimeEvent(database, { + kind: "tool.call.updated", + occurredAt: eventSeq, + payload: { + rawOutput: "parentless output", + status: "completed", + title: "Shell", + toolCallId: "parentless-tool", + }, + runId: RUN_ID, + seq: eventSeq, + sessionId: PUBLIC_API_TEST_IDS.ownerSession, + }); + await database + .prepare( + `UPDATE session + SET runtime_event_seq_cursor = MAX(runtime_event_seq_cursor, ?) + WHERE id = ?`, + ) + .bind(eventSeq, PUBLIC_API_TEST_IDS.ownerSession) + .run(); +} + +async function insertParentlessToolCarrier( + database: SqliteD1Database, + input: { readonly eventSeq: number; readonly messageSeq: number }, +): Promise { + await insertParentlessToolOutput(database, input.eventSeq); + await database + .prepare( + `INSERT INTO session_message ( + content_text, created_at, created_by_account_id, id, plan_json, + projection_format, role, segments_json, seq, session_id, session_run_id + ) VALUES ('', ?, ?, ?, NULL, 'event_stream_v3', 'assistant', NULL, ?, ?, ?)`, + ) + .bind( + input.eventSeq, + PUBLIC_API_TEST_IDS.ownerAccount, + RUN_ID, + input.messageSeq, + PUBLIC_API_TEST_IDS.ownerSession, + RUN_ID, + ) + .run(); + await database + .prepare( + `UPDATE session + SET message_seq_cursor = MAX(message_seq_cursor, ?) + WHERE id = ?`, + ) + .bind(input.messageSeq, PUBLIC_API_TEST_IDS.ownerSession) + .run(); +} + +async function commitCompletedWithoutFinalStream(database: SqliteD1Database): Promise { + const current = await getSessionRunSummary(database, RUN_ID); + if (current === null) { + throw new Error("Missing Session Run fixture."); + } + const completedAt = new Date(2).toISOString(); + const run = { + ...current, + completedAt, + startedAt: current.startedAt ?? completedAt, + status: "completed" as const, + updatedAt: completedAt, + }; + const sourceEventId = createSessionRunTerminalSourceId(RUN_ID, "run.completed"); + const event = createSessionRunUpdatedEvent( + run, + PUBLIC_API_TEST_IDS.ownerSession, + "IDLE", + sourceEventId, + ); + await commitTerminalRunProjection(database, { + assistantMessage: null, + error: null, + runId: RUN_ID, + sessionId: PUBLIC_API_TEST_IDS.ownerSession, + source: "driver", + targetStatus: "completed", + terminalEvent: { event, occurredAt: 2, sourceEventId }, + timestampMs: 2, + }); +} + +async function insertLegacyCompletedAuthorityWithCarrier( + database: SqliteD1Database, +): Promise { + await insertParentlessToolCarrier(database, { eventSeq: 1, messageSeq: 2 }); + await database + .prepare( + `INSERT INTO session_message ( + content_text, created_at, created_by_account_id, id, plan_json, + projection_format, role, segments_json, seq, session_id, session_run_id + ) VALUES ('legacy final', 1, ?, ?, NULL, 'materialized', 'assistant', NULL, 1, ?, ?)`, + ) + .bind( + PUBLIC_API_TEST_IDS.ownerAccount, + FINAL_MESSAGE_ID, + PUBLIC_API_TEST_IDS.ownerSession, + RUN_ID, + ) + .run(); + await insertRuntimeEvent(database, { + kind: "run.completed", + occurredAt: 2, + payload: { finalMessageId: FINAL_MESSAGE_ID, stopReason: "end_turn" }, + runId: RUN_ID, + seq: 2, + sessionId: PUBLIC_API_TEST_IDS.ownerSession, + }); + await database + .prepare( + `UPDATE session_event SET semantic_hash = NULL, terminal_event_json = NULL + WHERE run_id = ? AND event_type = 'run.completed'`, + ) + .bind(RUN_ID) + .run(); + await database + .prepare( + `UPDATE session + SET message_seq_cursor = 2, runtime_event_seq_cursor = 2 + WHERE id = ?`, + ) + .bind(PUBLIC_API_TEST_IDS.ownerSession) + .run(); +} + describe("canonical session run terminal failure", () => { + test("returns stale when the Run advances after its authoritative read", async () => { + const database = await createPublicHttpContractDatabase(); + await insertLinkedRunFixture(database); + await database + .prepare( + `UPDATE session_run + SET started_at = NULL, + status = 'queued', + status_seq = status_seq + 1, + updated_at = 1 + WHERE id = ?`, + ) + .bind(RUN_ID) + .run(); + const race = advanceRunAfterFirstSummaryRead(database, async () => { + await database + .prepare( + `UPDATE session_run + SET started_at = 2, + status = 'booting', + status_seq = status_seq + 1, + updated_at = 2 + WHERE id = ?`, + ) + .bind(RUN_ID) + .run(); + }); + const bindings = { + ...(createPublicHttpTestBindings(database) as ApiBindings), + DB: race.database, + }; + + await expect( + recordCanonicalSessionRunFailure(bindings, { + error: PROVISION_ERROR, + runId: RUN_ID, + sessionId: PUBLIC_API_TEST_IDS.ownerSession, + source: "api", + }), + ).resolves.toEqual({ kind: "not_failed", status: "booting" }); + + expect(race.advanced()).toBe(true); + await expect( + database + .prepare( + `SELECT completed_at, error_code, started_at, status + FROM session_run + WHERE id = ?`, + ) + .bind(RUN_ID) + .first(), + ).resolves.toEqual({ + completed_at: null, + error_code: null, + started_at: 2, + status: "booting", + }); + await expect( + database + .prepare("SELECT status FROM session WHERE id = ?") + .bind(PUBLIC_API_TEST_IDS.ownerSession) + .first(), + ).resolves.toEqual({ status: "RUNNING" }); + expect(await readFailureEvents(database)).toEqual([]); + }); + test("treats a concurrent failed transition as canonical success", async () => { const database = await createPublicHttpContractDatabase(); await insertLinkedRunFixture(database); @@ -173,7 +519,7 @@ describe("canonical session run terminal failure", () => { }), ]); - expect(outcomes.map((outcome) => outcome.kind)).toEqual(["failed", "failed"]); + expect(outcomes.map((outcome) => outcome.kind).toSorted()).toEqual(["failed", "not_failed"]); expect(await readFailureEvents(database)).toHaveLength(1); }); @@ -185,6 +531,7 @@ describe("canonical session run terminal failure", () => { await recordDriverInstanceFailure(bindings, { driverInstanceId: DRIVER_ID, error: DRIVER_ERROR, + sessionRunId: RUN_ID, }); await recordCanonicalSessionRunFailure(bindings, { error: PROVISION_ERROR, @@ -226,6 +573,7 @@ describe("canonical session run terminal failure", () => { await recordDriverInstanceFailure(bindings, { driverInstanceId: DRIVER_ID, error: DRIVER_ERROR, + sessionRunId: RUN_ID, }); const run = await database @@ -253,16 +601,16 @@ describe("canonical session run terminal failure", () => { const bindings = createPublicHttpTestBindings(database) as ApiBindings; await recordCanonicalSessionRunFailure(bindings, { - error: PROVISION_ERROR, + error: DRIVER_ERROR, runId: RUN_ID, sessionId: PUBLIC_API_TEST_IDS.ownerSession, - source: "api", + source: "system", }); await recordCanonicalSessionRunFailure(bindings, { - error: PROVISION_ERROR, + error: DRIVER_ERROR, runId: RUN_ID, sessionId: PUBLIC_API_TEST_IDS.ownerSession, - source: "api", + source: "system", }); expect(await readFailureEvents(database)).toEqual([ @@ -274,24 +622,24 @@ describe("canonical session run terminal failure", () => { ]); }); - test("repairs an API failure event from the Driver cached run link", async () => { + test("keeps the API terminal winner when the Driver uses a cached run link", async () => { const database = await createPublicHttpContractDatabase(); await insertLinkedRunFixture(database); const bindings = createPublicHttpTestBindings(database) as ApiBindings; const link = await getRuntimeSessionLink(database, DRIVER_ID); - await setSessionRunStatus(database, { + await recordCanonicalSessionRunFailure(bindings, { error: PROVISION_ERROR, runId: RUN_ID, + sessionId: PUBLIC_API_TEST_IDS.ownerSession, source: "api", - status: "failed", }); - expect(await readFailureEvents(database)).toEqual([]); await recordDriverInstanceFailure(bindings, { driverInstanceId: DRIVER_ID, error: DRIVER_ERROR, link, + sessionRunId: RUN_ID, }); expect(await readFailureEvents(database)).toEqual([ @@ -308,12 +656,29 @@ describe("canonical session run terminal failure", () => { await insertLinkedRunFixture(database); const bindings = createPublicHttpTestBindings(database) as ApiBindings; - await setSessionRunStatus(database, { - error: PROVISION_ERROR, - runId: RUN_ID, - source: "api", - status: "failed", - }); + await database + .prepare( + ` + UPDATE session_run + SET completed_at = 2, + error_code = ?, + error_details_json = '{}', + error_message = ?, + error_retryable = 0, + status = 'failed', + status_operation_id = NULL, + status_seq = status_seq + 1, + status_source = 'api', + updated_at = 2 + WHERE id = ? + `, + ) + .bind(PROVISION_ERROR.code, PROVISION_ERROR.message, RUN_ID) + .run(); + await database + .prepare("UPDATE session SET status = 'IDLE', updated_at = 2 WHERE id = ?") + .bind(PUBLIC_API_TEST_IDS.ownerSession) + .run(); await database .prepare("UPDATE driver_instance SET status = ? WHERE id = ?") @@ -322,8 +687,12 @@ describe("canonical session run terminal failure", () => { expect(await readFailureEvents(database)).toEqual([]); - const firstRepair = await reconcileTerminalSessionRuns(bindings, { limit: 10 }); - const secondRepair = await reconcileTerminalSessionRuns(bindings, { limit: 10 }); + const firstRepair = await reconcileTerminalSessionRuns(bindings, { + limit: 10, + }); + const secondRepair = await reconcileTerminalSessionRuns(bindings, { + limit: 10, + }); expect(firstRepair.reconciledRunIds).toEqual([RUN_ID]); expect(secondRepair.reconciledRunIds).toEqual([]); @@ -336,9 +705,43 @@ describe("canonical session run terminal failure", () => { ]); }); - test("repairs an inherited terminal Run whose Session and completion event are both stale", async () => { + test("repairs a completed Run that has no final assistant", async () => { + const database = await createPublicHttpContractDatabase(); + await insertLinkedRunFixture(database, "completed"); + await insertParentlessToolCarrier(database, { eventSeq: 1, messageSeq: 1 }); + const bindings = createPublicHttpTestBindings(database) as ApiBindings; + + await database + .prepare("UPDATE driver_instance SET status = ? WHERE id = ?") + .bind("stopped", DRIVER_ID) + .run(); + + const repaired = await reconcileTerminalSessionRuns(bindings, { + limit: 10, + }); + + expect(repaired.reconciledRunIds).toEqual([RUN_ID]); + expect( + await database + .prepare( + "SELECT stream_id FROM session_event WHERE run_id = ? AND event_type = 'run.completed'", + ) + .bind(RUN_ID) + .first(), + ).toEqual({ stream_id: null }); + expect( + await database + .prepare("SELECT id FROM session_message WHERE session_run_id = ? AND role = 'assistant'") + .bind(RUN_ID) + .all(), + ).toMatchObject({ results: [{ id: RUN_ID }] }); + }); + + test("repairs a completed Run from its unique sealed assistant authority", async () => { const database = await createPublicHttpContractDatabase(); await insertLinkedRunFixture(database, "completed"); + await insertSealedAssistantAuthority(database); + await insertParentlessToolCarrier(database, { eventSeq: 3, messageSeq: 2 }); const bindings = createPublicHttpTestBindings(database) as ApiBindings; await database @@ -350,7 +753,16 @@ describe("canonical session run terminal failure", () => { .bind("stopped", DRIVER_ID) .run(); - const repaired = await reconcileTerminalSessionRuns(bindings, { limit: 10 }); + const repaired = await reconcileTerminalSessionRuns(bindings, { + limit: 10, + }); + await expect( + assertCanonicalTerminalSessionRunProjection(bindings, { + runId: RUN_ID, + sessionId: PUBLIC_API_TEST_IDS.ownerSession, + status: "completed", + }), + ).resolves.toBeUndefined(); const session = await database .prepare("SELECT status FROM session WHERE id = ?") .bind(PUBLIC_API_TEST_IDS.ownerSession) @@ -376,9 +788,343 @@ describe("canonical session run terminal failure", () => { ]); }); - test("repairs an old terminal receipt without changing a newer Run", async () => { + test("recognizes a legacy materialized assistant beside a parentless tool carrier", async () => { const database = await createPublicHttpContractDatabase(); await insertLinkedRunFixture(database, "completed"); + await insertLegacyCompletedAuthorityWithCarrier(database); + const bindings = createPublicHttpTestBindings(database) as ApiBindings; + + await expect( + assertCanonicalTerminalSessionRunProjection(bindings, { + runId: RUN_ID, + sessionId: PUBLIC_API_TEST_IDS.ownerSession, + status: "completed", + }), + ).resolves.toBeUndefined(); + }); + + test("adopts a canonical completed Run that intentionally has no final stream", async () => { + const database = await createPublicHttpContractDatabase(); + await insertLinkedRunFixture(database); + await insertParentlessToolOutput(database, 1); + await commitCompletedWithoutFinalStream(database); + await database + .prepare("UPDATE session SET status = 'RUNNING', updated_at = 1 WHERE id = ?") + .bind(PUBLIC_API_TEST_IDS.ownerSession) + .run(); + await database + .prepare("UPDATE driver_instance SET status = 'stopped' WHERE id = ?") + .bind(DRIVER_ID) + .run(); + const bindings = createPublicHttpTestBindings(database) as ApiBindings; + + const repaired = await reconcileTerminalSessionRuns(bindings, { + limit: 10, + }); + + expect(repaired.reconciledSessionIds).toEqual([PUBLIC_API_TEST_IDS.ownerSession]); + expect( + await database + .prepare("SELECT status FROM session WHERE id = ?") + .bind(PUBLIC_API_TEST_IDS.ownerSession) + .first(), + ).toEqual({ status: "IDLE" }); + expect( + await database + .prepare( + "SELECT COUNT(*) AS count FROM session_message WHERE session_run_id = ? AND role = 'assistant'", + ) + .bind(RUN_ID) + .first(), + ).toEqual({ count: 1 }); + expect( + await database + .prepare("SELECT id FROM session_message WHERE session_run_id = ? AND role = 'assistant'") + .bind(RUN_ID) + .first(), + ).toEqual({ id: RUN_ID }); + }); + + test("does not report a newer stale Session projection as reconciled", async () => { + const database = await createPublicHttpContractDatabase(); + await insertLinkedRunFixture(database); + await commitCompletedWithoutFinalStream(database); + const run = await database + .prepare("SELECT updated_at FROM session_run WHERE id = ?") + .bind(RUN_ID) + .first<{ updated_at: number }>(); + if (run === null) { + throw new Error("Missing terminal Run fixture."); + } + await database + .prepare("UPDATE session SET status = 'RUNNING', updated_at = ? WHERE id = ?") + .bind(run.updated_at + 1, PUBLIC_API_TEST_IDS.ownerSession) + .run(); + await database + .prepare("UPDATE driver_instance SET status = 'stopped' WHERE id = ?") + .bind(DRIVER_ID) + .run(); + + const result = await reconcileTerminalSessionRuns( + createPublicHttpTestBindings(database) as ApiBindings, + { limit: 10 }, + ); + + expect(result).toMatchObject({ + failures: [ + { + message: expect.stringContaining("did not converge its current Session"), + runId: RUN_ID, + }, + ], + reconciledRunIds: [], + reconciledSessionIds: [], + }); + await expect( + database + .prepare("SELECT status FROM session WHERE id = ?") + .bind(PUBLIC_API_TEST_IDS.ownerSession) + .first(), + ).resolves.toEqual({ status: "RUNNING" }); + }); + + test("retries a repaired authority without changing terminal Run semantics", async () => { + const database = await createPublicHttpContractDatabase(); + await insertLinkedRunFixture(database); + await commitCompletedWithoutFinalStream(database); + await database + .prepare( + `INSERT INTO session_message ( + content_text, created_at, created_by_account_id, id, plan_json, + projection_format, role, segments_json, seq, session_id, session_run_id + ) VALUES ('not a carrier', 2, ?, ?, NULL, 'materialized', 'assistant', NULL, 1, ?, ?)`, + ) + .bind(PUBLIC_API_TEST_IDS.ownerAccount, RUN_ID, PUBLIC_API_TEST_IDS.ownerSession, RUN_ID) + .run(); + await database + .prepare("UPDATE session SET status = 'RUNNING', updated_at = 1 WHERE id = ?") + .bind(PUBLIC_API_TEST_IDS.ownerSession) + .run(); + await database + .prepare("UPDATE driver_instance SET status = 'stopped' WHERE id = ?") + .bind(DRIVER_ID) + .run(); + const bindings = createPublicHttpTestBindings(database) as ApiBindings; + const semanticRun = await database + .prepare( + `SELECT completed_at, error_code, error_details_json, error_message, + error_retryable, status, status_seq, status_source, updated_at + FROM session_run + WHERE id = ?`, + ) + .bind(RUN_ID) + .first(); + + await expect(reconcileTerminalSessionRuns(bindings, { limit: 10 })).resolves.toMatchObject({ + failures: [ + { + message: expect.stringContaining("Canonical assistant messages"), + runId: RUN_ID, + }, + ], + }); + const claimedRun = await database + .prepare( + `SELECT completed_at, error_code, error_details_json, error_message, + error_retryable, status, status_seq, status_source, + terminal_reconciliation_attempted_at, updated_at + FROM session_run + WHERE id = ?`, + ) + .bind(RUN_ID) + .first<{ terminal_reconciliation_attempted_at: number }>(); + expect(claimedRun).toEqual({ + ...semanticRun, + terminal_reconciliation_attempted_at: expect.any(Number), + }); + const attemptedAt = claimedRun?.terminal_reconciliation_attempted_at; + if (attemptedAt === undefined) { + throw new Error("Missing terminal reconciliation attempt marker."); + } + await database.prepare("DELETE FROM session_message WHERE id = ?").bind(RUN_ID).run(); + await database + .prepare("UPDATE session_run SET terminal_reconciliation_attempted_at = ? WHERE id = ?") + .bind(attemptedAt - 10 * 60_000 - 1, RUN_ID) + .run(); + + await expect(reconcileTerminalSessionRuns(bindings, { limit: 10 })).resolves.toMatchObject({ + failures: [], + reconciledSessionIds: [PUBLIC_API_TEST_IDS.ownerSession], + }); + await expect( + database + .prepare("SELECT status FROM session WHERE id = ?") + .bind(PUBLIC_API_TEST_IDS.ownerSession) + .first(), + ).resolves.toEqual({ status: "IDLE" }); + }); + + test("rotates a poisoned candidate behind the next repair batch", async () => { + const database = await createPublicHttpContractDatabase(); + await insertLinkedRunFixture(database, "completed"); + const healthyRunId = await insertHealthyFailedRunFixture(database); + await database + .prepare( + `INSERT INTO session_message ( + content_text, created_at, created_by_account_id, id, plan_json, + projection_format, role, segments_json, seq, session_id, session_run_id + ) VALUES ('not a carrier', 1, ?, ?, NULL, 'materialized', 'assistant', NULL, 1, ?, ?)`, + ) + .bind(PUBLIC_API_TEST_IDS.ownerAccount, RUN_ID, PUBLIC_API_TEST_IDS.ownerSession, RUN_ID) + .run(); + await database + .prepare("UPDATE driver_instance SET status = 'stopped' WHERE id = ?") + .bind(DRIVER_ID) + .run(); + const bindings = createPublicHttpTestBindings(database) as ApiBindings; + + await expect(reconcileTerminalSessionRuns(bindings, { limit: 1 })).resolves.toMatchObject({ + failures: [ + { + message: expect.stringContaining("invalid parentless tool carrier"), + runId: RUN_ID, + }, + ], + reconciledRunIds: [], + }); + await expect( + database + .prepare( + "SELECT source_event_id FROM session_event WHERE run_id = ? AND event_type = 'run.failed'", + ) + .bind(healthyRunId) + .first(), + ).resolves.toBeNull(); + await expect(reconcileTerminalSessionRuns(bindings, { limit: 1 })).resolves.toMatchObject({ + failures: [], + reconciledRunIds: [healthyRunId], + }); + await expect( + database + .prepare( + "SELECT source_event_id FROM session_event WHERE run_id = ? AND event_type = 'run.failed'", + ) + .bind(healthyRunId) + .first(), + ).resolves.toEqual({ + source_event_id: createSessionRunTerminalSourceId(healthyRunId, "run.failed"), + }); + await expect( + database + .prepare("SELECT last_run_id, status FROM session WHERE id = ?") + .bind(PUBLIC_API_TEST_IDS.ownerSession) + .first(), + ).resolves.toEqual({ last_run_id: healthyRunId, status: "IDLE" }); + }); + + test("rotates a malformed terminal Run before parsing the next batch", async () => { + const database = await createPublicHttpContractDatabase(); + await insertLinkedRunFixture(database, "failed"); + const healthyRunId = await insertHealthyFailedRunFixture(database); + await database + .prepare("UPDATE session_run SET error_details_json = '{' WHERE id = ?") + .bind(RUN_ID) + .run(); + await database + .prepare("UPDATE driver_instance SET status = 'stopped' WHERE id = ?") + .bind(DRIVER_ID) + .run(); + const bindings = createPublicHttpTestBindings(database) as ApiBindings; + + await expect(reconcileTerminalSessionRuns(bindings, { limit: 1 })).resolves.toMatchObject({ + failures: [{ runId: RUN_ID }], + reconciledRunIds: [], + }); + await expect(reconcileTerminalSessionRuns(bindings, { limit: 1 })).resolves.toMatchObject({ + failures: [], + reconciledRunIds: [healthyRunId], + }); + }); + + test("rejects assistant rows that contradict a canonical no-final completion", async () => { + const database = await createPublicHttpContractDatabase(); + await insertLinkedRunFixture(database); + await commitCompletedWithoutFinalStream(database); + await database + .prepare( + `INSERT INTO session_message ( + content_text, created_at, created_by_account_id, id, plan_json, + projection_format, role, segments_json, seq, session_id, session_run_id + ) VALUES ('', 2, ?, ?, NULL, 'event_stream_v3', 'assistant', NULL, 1, ?, ?)`, + ) + .bind( + PUBLIC_API_TEST_IDS.ownerAccount, + FINAL_MESSAGE_ID, + PUBLIC_API_TEST_IDS.ownerSession, + RUN_ID, + ) + .run(); + await database + .prepare("UPDATE session SET status = 'RUNNING', updated_at = 1 WHERE id = ?") + .bind(PUBLIC_API_TEST_IDS.ownerSession) + .run(); + await database + .prepare("UPDATE driver_instance SET status = 'stopped' WHERE id = ?") + .bind(DRIVER_ID) + .run(); + const bindings = createPublicHttpTestBindings(database) as ApiBindings; + + await expect(reconcileTerminalSessionRuns(bindings, { limit: 10 })).resolves.toMatchObject({ + failures: [ + { + message: expect.stringContaining("Canonical assistant messages"), + runId: RUN_ID, + }, + ], + }); + }); + + test("rejects a terminal receipt whose semantic stream authority changed", async () => { + const database = await createPublicHttpContractDatabase(); + await insertLinkedRunFixture(database); + await commitCompletedWithoutFinalStream(database); + await database + .prepare("UPDATE session_event SET stream_id = 'corrupt-progress' WHERE run_id = ?") + .bind(RUN_ID) + .run(); + const bindings = createPublicHttpTestBindings(database) as ApiBindings; + await expect( + recordCanonicalSessionRunTerminal(bindings, { + assistantMessage: null, + error: null, + runId: RUN_ID, + sessionId: PUBLIC_API_TEST_IDS.ownerSession, + source: "driver", + status: "completed", + }), + ).rejects.toThrow("terminal semantic authority is invalid"); + await database + .prepare("UPDATE session SET status = 'RUNNING', updated_at = 1 WHERE id = ?") + .bind(PUBLIC_API_TEST_IDS.ownerSession) + .run(); + await database + .prepare("UPDATE driver_instance SET status = 'stopped' WHERE id = ?") + .bind(DRIVER_ID) + .run(); + await expect(reconcileTerminalSessionRuns(bindings, { limit: 10 })).resolves.toMatchObject({ + failures: [ + { + message: expect.stringContaining("terminal semantic authority is invalid"), + runId: RUN_ID, + }, + ], + }); + }); + + test("refuses to invent an old terminal receipt once a newer Run owns the Session", async () => { + const database = await createPublicHttpContractDatabase(); + await insertLinkedRunFixture(database, "completed"); + await insertSealedAssistantAuthority(database); const bindings = createPublicHttpTestBindings(database) as ApiBindings; await database @@ -425,7 +1171,14 @@ describe("canonical session run terminal failure", () => { .bind("stopped", DRIVER_ID) .run(); - await reconcileTerminalSessionRuns(bindings, { limit: 10 }); + await expect(reconcileTerminalSessionRuns(bindings, { limit: 10 })).resolves.toMatchObject({ + failures: [ + { + message: expect.stringContaining("not safely repairable"), + runId: RUN_ID, + }, + ], + }); const session = await database .prepare("SELECT last_run_id, status FROM session WHERE id = ?") @@ -443,9 +1196,7 @@ describe("canonical session run terminal failure", () => { .bind(RUN_ID) .first<{ source_event_id: string }>(); - expect(completedEvent).toEqual({ - source_event_id: `session-run-terminal:${RUN_ID}:run.completed`, - }); + expect(completedEvent).toBeNull(); expect(await readFailureEvents(database)).toEqual([]); }); diff --git a/apps/api/tests/session-runtime-event-store.test.ts b/apps/api/tests/session-runtime-event-store.test.ts index 082ebcc2..59820be1 100644 --- a/apps/api/tests/session-runtime-event-store.test.ts +++ b/apps/api/tests/session-runtime-event-store.test.ts @@ -1,18 +1,18 @@ import { describe, expect, test } from "bun:test"; -import { createRuntimeEvent } from "@mosoo/runtime-events"; +import { createRuntimeEvent, createRuntimeEventSemanticHash } from "@mosoo/runtime-events"; import type { RuntimeEventEnvelope, RuntimeEventKind, RuntimeEventOrigin, } from "@mosoo/runtime-events"; -import { RuntimeEventPersistenceCompactor } from "../src/modules/runtime/infrastructure/driver-instance/runtime-event-persistence-compactor"; import { loadSessionAgentTaskSnapshot } from "../src/modules/sessions/infrastructure/session-agent-task-snapshot.repository"; import { persistOneRuntimeEventPerSession, persistSessionRuntimeEvents, } from "../src/modules/sessions/infrastructure/session-runtime-event-store.repository"; +import { applyDrizzleMigration } from "./helpers/drizzle-migrations"; import { SqliteD1Database } from "./helpers/sqlite-d1"; function runtimeEvent(input: { @@ -71,6 +71,32 @@ function activateRun( .run(); } +async function insertConcurrentMessageReceipt( + database: SqliteD1Database, + input: { event: RuntimeEventEnvelope; sourceEventId: string }, +): Promise { + const semanticHash = await createRuntimeEventSemanticHash(input.event); + database.execute("UPDATE session SET runtime_event_seq_cursor = 1 WHERE id = 'session-1'"); + await database + .prepare( + `INSERT INTO session_event ( + agent_id, content_text, created_at, ended_at, event_type, family, id, + occurred_at, process_status, process_type, run_id, semantic_hash, seq, + session_id, source_event_id, source, stream_id, visibility + ) VALUES (?, ?, 1000, 1000, 'message.delta', 'message', ?, 1000, + 'available', 'agent.message.delta', 'run-1', ?, 1, 'session-1', + ?, 'driver', 'message-1', 'all_consumers')`, + ) + .bind( + "01J00000000000000000000009", + (input.event.payload as { contentDelta: string }).contentDelta, + `winner:${input.sourceEventId}`, + semanticHash, + input.sourceEventId, + ) + .run(); +} + function createRuntimeEventStoreDatabase( input: { maxBoundParams?: number } = {}, ): SqliteD1Database { @@ -90,10 +116,22 @@ function createRuntimeEventStoreDatabase( ); CREATE TABLE session_run ( + completed_at integer, id text PRIMARY KEY NOT NULL, driver_instance_id text, + error_code text, + error_details_json text, + error_message text, + error_retryable integer, session_id text NOT NULL, - status text DEFAULT 'running' NOT NULL + status text DEFAULT 'running' NOT NULL, + status_changed_at integer NOT NULL DEFAULT 0, + status_event text NOT NULL DEFAULT 'run.start', + status_operation_id text, + status_seq integer NOT NULL DEFAULT 0, + status_source text NOT NULL DEFAULT 'driver', + started_at integer, + updated_at integer NOT NULL DEFAULT 0 ); CREATE TABLE session_agent_task_snapshot ( @@ -106,26 +144,86 @@ function createRuntimeEventStoreDatabase( CREATE TABLE session_event ( agent_id text NOT NULL, + artifact_attempt_id text, + artifact_manifest_json text, + artifact_manifest_sha256 text, content_text text NOT NULL, created_at integer NOT NULL, ended_at integer NOT NULL, event_type text NOT NULL, family text NOT NULL, id text PRIMARY KEY NOT NULL, + mcp_command_id text, occurred_at integer NOT NULL, process_status text NOT NULL, process_type text NOT NULL, run_id text, + semantic_hash text CHECK ( + semantic_hash IS NULL OR ( + length(semantic_hash) = 64 + AND semantic_hash = lower(semantic_hash) + AND semantic_hash NOT GLOB '*[^0-9a-f]*' + ) + ), seq integer NOT NULL, session_id text NOT NULL, source_event_id text NOT NULL, source text NOT NULL, + stream_id text, + terminal_event_json text, tool_call_id text, + tool_input_delta_json text, tool_input_json text, tool_name text, + tool_output_delta_text text, + tool_output_text text, + tool_parent_message_id text, + tool_result_message_id text, + tool_status text, tokens integer, trace_id text, - visibility text NOT NULL + visibility text NOT NULL, + CHECK (tool_input_delta_json IS NULL OR tool_input_json IS NULL), + CHECK (tool_output_delta_text IS NULL OR tool_output_text IS NULL), + CHECK ( + (terminal_event_json IS NULL AND NOT (semantic_hash IS NOT NULL AND event_type IN ('run.cancelled', 'run.completed', 'run.failed'))) + OR (terminal_event_json IS NOT NULL AND json_valid(terminal_event_json) = 1 AND semantic_hash IS NOT NULL AND event_type IN ('run.cancelled', 'run.completed', 'run.failed')) + ), + CHECK ( + (artifact_attempt_id IS NULL AND artifact_manifest_json IS NULL AND artifact_manifest_sha256 IS NULL) + OR ( + artifact_attempt_id IS NOT NULL + AND artifact_manifest_json IS NOT NULL + AND json_valid(artifact_manifest_json) = 1 + AND json_extract(artifact_manifest_json, '$.version') IS 1 + AND json_type(artifact_manifest_json, '$.captureStatus') IS 'text' + AND json_extract(artifact_manifest_json, '$.captureStatus') IN ('complete', 'omitted_file_limit', 'omitted_runtime_unavailable', 'omitted_size_limit', 'omitted_source_changed', 'omitted_source_missing') + AND json_type(artifact_manifest_json, '$.mode') IS 'text' + AND json_extract(artifact_manifest_json, '$.mode') IN ('delta', 'snapshot') + AND (json_extract(artifact_manifest_json, '$.captureStatus') = 'complete' OR json_array_length(artifact_manifest_json, '$.files') = 0) + AND json_extract(artifact_manifest_json, '$.sourceEventId') IS source_event_id + AND json_extract(artifact_manifest_json, '$.semanticHash') IS semantic_hash + AND json_type(artifact_manifest_json, '$.files') IS 'array' + AND artifact_manifest_sha256 IS NOT NULL + AND length(artifact_manifest_sha256) = 64 + AND artifact_manifest_sha256 = lower(artifact_manifest_sha256) + AND artifact_manifest_sha256 NOT GLOB '*[^0-9a-f]*' + AND semantic_hash IS NOT NULL + AND event_type IN ('file.change.updated', 'file.changed', 'run.completed') + ) + ), + CHECK (tool_status IS NULL OR tool_status IN ('running', 'completed', 'failed', 'cancelled')), + CHECK ( + mcp_command_id IS NULL OR ( + mcp_command_id = upper(mcp_command_id) + AND length(mcp_command_id) = 26 + AND substr(mcp_command_id, 1, 1) GLOB '[0-7]' + AND mcp_command_id NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*' + AND event_type = 'tool.call.updated' + AND tool_status IS NOT NULL + AND tool_status IN ('completed', 'failed', 'cancelled') + ) + ) ); CREATE UNIQUE INDEX session_event_session_seq_idx @@ -134,29 +232,25 @@ function createRuntimeEventStoreDatabase( CREATE UNIQUE INDEX session_event_session_source_idx ON session_event (session_id, source_event_id); - CREATE TRIGGER session_event_tool_identity_consistency - BEFORE INSERT ON session_event - WHEN NEW.tool_call_id IS NOT NULL AND EXISTS ( - SELECT 1 - FROM session_event AS existing - WHERE existing.session_id = NEW.session_id - AND existing.tool_call_id = NEW.tool_call_id - AND ( - ( - NEW.tool_name IS NOT NULL - AND existing.tool_name IS NOT NULL - AND NEW.tool_name <> existing.tool_name - ) - OR ( - NEW.tool_input_json IS NOT NULL - AND existing.tool_input_json IS NOT NULL - AND NEW.tool_input_json <> existing.tool_input_json - ) - ) - ) - BEGIN - SELECT RAISE(ABORT, 'session_event tool identity conflict'); - END; + CREATE UNIQUE INDEX session_event_artifact_attempt_idx + ON session_event (artifact_attempt_id) + WHERE artifact_attempt_id IS NOT NULL; + + CREATE UNIQUE INDEX session_event_mcp_terminal_winner_idx + ON session_event (session_id, mcp_command_id) + WHERE mcp_command_id IS NOT NULL; + + CREATE UNIQUE INDEX session_event_run_terminal_winner_idx + ON session_event (session_id, run_id) + WHERE semantic_hash IS NOT NULL + AND run_id IS NOT NULL + AND event_type IN ('run.cancelled', 'run.completed', 'run.failed'); + + CREATE INDEX session_event_run_stream_process_seq_idx + ON session_event (run_id, stream_id, process_type, seq); + + CREATE INDEX session_event_run_tool_call_seq_idx + ON session_event (run_id, tool_call_id, seq); CREATE TABLE session_permission_request ( created_at integer NOT NULL, @@ -185,6 +279,7 @@ function createRuntimeEventStoreDatabase( ('run-1', 'session-1'), ('run-2', 'session-2'); `); + applyDrizzleMigration(database, "0018_runtime-operation-ready-authority"); return database; } @@ -281,9 +376,67 @@ describe("session runtime event store", () => { expect(await database.prepare("SELECT COUNT(*) AS count FROM session_event").first()).toEqual({ count: 0, }); + expect( + await database + .prepare("SELECT runtime_event_seq_cursor FROM session WHERE id = 'session-1'") + .first(), + ).toEqual({ runtime_event_seq_cursor: 0 }); }); - test("rejects the receipt and snapshot when archive wins after sequence allocation", async () => { + test("rolls back a permission receipt when its viewer projection fails", async () => { + const database = createRuntimeEventStoreDatabase(); + const event = runtimeEvent({ + driverInstanceId: "driver-1", + id: "permission-atomic-event", + kind: "permission.requested", + occurredAtMs: 1_100, + payload: { + requestId: "permission-atomic", + targetItemId: "tool-call-1", + title: "Approve command", + toolCall: { kind: "shell", toolCallId: "tool-call-1" }, + }, + runId: "run-1", + }); + const persist = () => + persistSessionRuntimeEvents(database, { + records: [{ event, occurredAt: 1_100, sourceEventId: "permission-atomic-source" }], + sessionId: "session-1", + }); + database.execute(` + CREATE TRIGGER reject_permission_projection + BEFORE INSERT ON session_permission_request + BEGIN + SELECT RAISE(ABORT, 'forced permission projection failure'); + END; + `); + + await expect(persist()).rejects.toThrow("forced permission projection failure"); + expect(await database.prepare("SELECT COUNT(*) AS count FROM session_event").first()).toEqual({ + count: 0, + }); + expect( + await database + .prepare("SELECT runtime_event_seq_cursor FROM session WHERE id = 'session-1'") + .first(), + ).toEqual({ runtime_event_seq_cursor: 0 }); + + database.execute("DROP TRIGGER reject_permission_projection"); + await expect(persist()).resolves.toMatchObject({ persistedCount: 1 }); + await expect(persist()).resolves.toMatchObject({ persistedCount: 0 }); + expect( + await database + .prepare("SELECT request_id FROM session_permission_request") + .all<{ request_id: string }>(), + ).toMatchObject({ results: [{ request_id: "permission-atomic" }] }); + expect( + await database + .prepare("SELECT runtime_event_seq_cursor FROM session WHERE id = 'session-1'") + .first(), + ).toEqual({ runtime_event_seq_cursor: 1 }); + }); + + test("atomically rejects the cursor, receipt, and snapshot when archive wins", async () => { const database = createRuntimeEventStoreDatabase(); activateRun(database, { driverInstanceId: "driver-1", runId: "run-1" }); const persistBatch = database.batch.bind(database) as D1Database["batch"]; @@ -298,29 +451,30 @@ describe("session runtime event store", () => { return persistBatch(statements); }; - const result = await persistSessionRuntimeEvents(database, { - records: [ - { - event: agentTasksEvent({ - driverInstanceId: "driver-1", - id: "tasks-after-archive", - occurredAtMs: 1_000, - runId: "run-1", - taskId: "must-not-land", - }), - occurredAt: 1_000, - sourceEventId: "source-after-archive", - }, - ], - sessionId: "session-1", - }); + await expect( + persistSessionRuntimeEvents(database, { + records: [ + { + event: agentTasksEvent({ + driverInstanceId: "driver-1", + id: "tasks-after-archive", + occurredAtMs: 1_000, + runId: "run-1", + taskId: "must-not-land", + }), + occurredAt: 1_000, + sourceEventId: "source-after-archive", + }, + ], + sessionId: "session-1", + }), + ).rejects.toThrow("not writable for runtime events"); const session = await database .prepare("SELECT archived_at, runtime_event_seq_cursor FROM session WHERE id = 'session-1'") .first<{ archived_at: number | null; runtime_event_seq_cursor: number }>(); expect(archivedBeforeBatch).toBe(true); - expect(session).toEqual({ archived_at: 2, runtime_event_seq_cursor: 1 }); - expect(result.persistedCount).toBe(0); + expect(session).toEqual({ archived_at: 2, runtime_event_seq_cursor: 0 }); expect(await database.prepare("SELECT COUNT(*) AS count FROM session_event").first()).toEqual({ count: 0, }); @@ -369,7 +523,7 @@ describe("session runtime event store", () => { expect(await loadSessionAgentTaskSnapshot(database, "session-1")).toBeNull(); }); - test("does not let duplicate receipts or stale run and driver snapshots replace current state", async () => { + test("does not let exact replay receipts or stale run and driver snapshots replace current state", async () => { const database = createRuntimeEventStoreDatabase(); activateRun(database, { driverInstanceId: "driver-1", runId: "run-1" }); @@ -397,7 +551,7 @@ describe("session runtime event store", () => { id: "tasks-replay", occurredAtMs: 1_001, runId: "run-1", - taskId: "duplicate-must-not-win", + taskId: "first", }), occurredAt: 1_001, sourceEventId: "source-tasks-1", @@ -519,6 +673,11 @@ describe("session runtime event store", () => { .first<{ count: number }>(); expect(count?.count).toBe(0); + expect( + await failingDatabase + .prepare("SELECT runtime_event_seq_cursor FROM session WHERE id = 'session-1'") + .first(), + ).toEqual({ runtime_event_seq_cursor: 0 }); }); test("persists mixed source ids and skips source replays before allocating sequence", async () => { @@ -600,7 +759,7 @@ describe("session runtime event store", () => { { event: runtimeEvent({ id: "event-3", - kind: "run.completed", + kind: "run.waiting", occurredAtMs: 1_200, origin: "driver", runId: "run-1", @@ -632,7 +791,7 @@ describe("session runtime event store", () => { expect(rows.results.map((row) => row.event_type)).toEqual([ "run.started", "runtime.timing.recorded", - "run.completed", + "run.waiting", ]); expect(rows.results.map((row) => row.seq)).toEqual([1, 2, 3]); expect(rows.results.map((row) => row.source_event_id)).toEqual([ @@ -702,7 +861,95 @@ describe("session runtime event store", () => { }); }); - test("persists compacted semantic columns", async () => { + test.each([ + ["adopts an exact", false], + ["rejects a changed", true], + ] as const)("%s source winner that races the atomic batch", async (_name, changed) => { + const database = createRuntimeEventStoreDatabase(); + const candidate = runtimeEvent({ + id: "candidate", + kind: "message.delta", + occurredAtMs: 1_000, + payload: { contentDelta: changed ? "changed" : "winner", messageId: "message-1" }, + runId: "run-1", + }); + const winner = runtimeEvent({ + id: "winner", + kind: "message.delta", + occurredAtMs: 999, + payload: { contentDelta: "winner", messageId: "message-1" }, + runId: "run-1", + }); + const sourceEventId = "source-race"; + const originalBatch = database.batch.bind(database) as D1Database["batch"]; + let injected = false; + database.batch = async (statements: D1PreparedStatement[]) => { + if (!injected) { + injected = true; + await insertConcurrentMessageReceipt(database, { event: winner, sourceEventId }); + } + return originalBatch(statements); + }; + + const persistence = persistSessionRuntimeEvents(database, { + records: [{ event: candidate, occurredAt: 1_000, sourceEventId }], + sessionId: "session-1", + }); + if (changed) { + await expect(persistence).rejects.toThrow("conflicts with its durable receipt"); + } else { + await expect(persistence).resolves.toMatchObject({ persistedCount: 0 }); + } + + expect( + await database + .prepare("SELECT runtime_event_seq_cursor FROM session WHERE id = 'session-1'") + .first(), + ).toEqual({ runtime_event_seq_cursor: 1 }); + expect(await database.prepare("SELECT COUNT(*) AS count FROM session_event").first()).toEqual({ + count: 1, + }); + }); + + test("adopts an exact receipt after commit succeeds but its ACK is lost", async () => { + const database = createRuntimeEventStoreDatabase(); + const event = runtimeEvent({ + id: "commit-before-ack", + kind: "message.delta", + occurredAtMs: 1_000, + payload: { contentDelta: "durable", messageId: "message-1" }, + runId: "run-1", + }); + const input = { + records: [{ event, occurredAt: 1_000, sourceEventId: "source-commit-before-ack" }], + sessionId: "session-1" as const, + }; + const originalBatch = database.batch.bind(database) as D1Database["batch"]; + let disconnected = false; + database.batch = async (statements: D1PreparedStatement[]) => { + const result = await originalBatch(statements); + if (!disconnected) { + disconnected = true; + throw new Error("injected ACK loss"); + } + return result; + }; + + await expect(persistSessionRuntimeEvents(database, input)).rejects.toThrow("injected ACK loss"); + await expect(persistSessionRuntimeEvents(database, input)).resolves.toMatchObject({ + persistedCount: 0, + }); + expect( + await database + .prepare("SELECT runtime_event_seq_cursor FROM session WHERE id = 'session-1'") + .first(), + ).toEqual({ runtime_event_seq_cursor: 1 }); + expect(await database.prepare("SELECT COUNT(*) AS count FROM session_event").first()).toEqual({ + count: 1, + }); + }); + + test("persists terminal tool semantic columns", async () => { const database = createRuntimeEventStoreDatabase(); await persistSessionRuntimeEvents(database, { @@ -724,6 +971,21 @@ describe("session runtime event store", () => { occurredAt: 2_000, sourceEventId: "source-tool-completed", }, + { + event: runtimeEvent({ + id: "tool-cancelled", + kind: "tool.call.updated", + occurredAtMs: 2_100, + payload: { + status: "cancelled", + title: "Search", + toolCallId: "tool-2", + }, + runId: "run-1", + }), + occurredAt: 2_100, + sourceEventId: "source-tool-cancelled", + }, ], sessionId: "session-1", }); @@ -740,6 +1002,7 @@ describe("session runtime event store", () => { e.tool_input_json, e.tool_name FROM session_event e + ORDER BY e.seq `, ) .all<{ @@ -752,19 +1015,98 @@ describe("session runtime event store", () => { tool_name: string | null; }>(); - expect(rows.results).toHaveLength(1); + expect(rows.results).toHaveLength(2); expect(rows.results[0]).toMatchObject({ - content_text: "Search result: ok", + content_text: "ok", event_type: "tool.call.updated", process_status: "available", process_type: "tool.use.completed", tool_call_id: "tool-1", tool_input_json: '{"query":"mosoo"}', - tool_name: null, + tool_name: "Search", + }); + expect(rows.results[1]).toMatchObject({ + content_text: "", + event_type: "tool.call.updated", + process_status: "available", + process_type: "tool.use.completed", + tool_call_id: "tool-2", + tool_input_json: null, + tool_name: "Search", }); }); - test("accepts streamed arguments but rejects terminal tool input reuse", async () => { + test.each([ + [ + "message.cancelled", + { messageId: "message-1", role: "agent" }, + "Message updated.", + "available", + "agent.message.delta", + "message-1", + ], + [ + "message.failed", + { + error: { code: "runtime.failed", message: "Runtime failed." }, + messageId: "message-1", + role: "agent", + }, + "Message updated.", + "error", + "agent.message.delta", + "message-1", + ], + [ + "thought.cancelled", + { thoughtId: "thought-1" }, + "Agent thinking updated.", + "available", + "agent.thinking.delta", + "thought-1", + ], + ] as const)( + "persists %s as a terminal semantic row", + async (kind, payload, contentText, processStatus, processType, streamId) => { + const database = createRuntimeEventStoreDatabase(); + + await persistSessionRuntimeEvents(database, { + records: [ + { + event: runtimeEvent({ + id: "terminal-event", + kind, + occurredAtMs: 2_000, + payload, + runId: "run-1", + }), + occurredAt: 2_000, + sourceEventId: "source-terminal-event", + }, + ], + sessionId: "session-1", + }); + + expect( + await database + .prepare( + ` + SELECT content_text, event_type, process_status, process_type, stream_id + FROM session_event + `, + ) + .first(), + ).toMatchObject({ + content_text: contentText, + event_type: kind, + process_status: processStatus, + process_type: processType, + stream_id: streamId, + }); + }, + ); + + test("persists replacement tool input snapshots through terminal enrichment", async () => { const database = createRuntimeEventStoreDatabase(); const persistToolEvent = (input: { id: string; @@ -817,20 +1159,18 @@ describe("session runtime event store", () => { title: "Command exited 0", }); - await expect( - persistToolEvent({ - id: "tool-conflict", - rawInput: '{"command":"python other.py","cwd":"/workspace"}', - sourceEventId: "source-tool-conflict", - status: "completed", - title: "Command exited 0", - }), - ).rejects.toThrow("session_event tool identity conflict"); + await persistToolEvent({ + id: "tool-late-enrichment", + rawInput: '{"command":"python other.py","cwd":"/workspace"}', + sourceEventId: "source-tool-late-enrichment", + status: "completed", + title: "Command exited 0", + }); const count = await database .prepare("SELECT COUNT(*) AS count FROM session_event") .first<{ count: number }>(); - expect(count?.count).toBe(3); + expect(count?.count).toBe(4); }); test("rejects runtime event batches for a different envelope session", async () => { @@ -885,73 +1225,6 @@ describe("session runtime event store", () => { ).rejects.toThrow(); }); - test("persists compacted stream fragments as semantic rows", async () => { - const database = createRuntimeEventStoreDatabase(); - const compactor = new RuntimeEventPersistenceCompactor(); - const fragments = [ - runtimeEvent({ - id: "message-start", - kind: "message.started", - occurredAtMs: 3_000, - payload: { messageId: "message-1", role: "agent" }, - runId: "run-1", - }), - runtimeEvent({ - id: "message-delta-1", - kind: "message.delta", - occurredAtMs: 3_010, - payload: { contentDelta: "Hello ", messageId: "message-1", role: "agent" }, - runId: "run-1", - }), - runtimeEvent({ - id: "message-delta-2", - kind: "message.delta", - occurredAtMs: 3_020, - payload: { contentDelta: "world", messageId: "message-1", role: "agent" }, - runId: "run-1", - }), - runtimeEvent({ - id: "message-end", - kind: "message.completed", - occurredAtMs: 3_030, - payload: { messageId: "message-1", role: "agent" }, - runId: "run-1", - }), - ]; - const compacted = compactor.compact( - fragments.map((event) => ({ - event, - occurredAt: Date.parse(event.occurredAt), - sourceEventId: `source-${event.id}`, - })), - ); - - await persistSessionRuntimeEvents(database, { - records: compacted, - sessionId: "session-1", - }); - - const rows = await database - .prepare( - ` - SELECT content_text, event_type, process_type - FROM session_event - `, - ) - .all<{ - content_text: string; - event_type: string; - process_type: string; - }>(); - - expect(rows.results).toHaveLength(1); - expect(rows.results[0]).toMatchObject({ - content_text: "Hello world", - event_type: "message.added", - process_type: "agent.message.delta", - }); - }); - test("updates viewer projections only for inserted runtime events", async () => { const database = createRuntimeEventStoreDatabase(); const permissionEvent = runtimeEvent({ @@ -1033,6 +1306,11 @@ describe("session runtime event store", () => { title: "Approve command", }, ]); + expect( + await database + .prepare("SELECT status, status_seq FROM session_run WHERE id = 'run-1'") + .first(), + ).toEqual({ status: "waiting_input", status_seq: 1 }); expect(readiness === null ? null : JSON.parse(readiness.readiness_json)).toEqual({ checkedAt: "2026-05-08T00:00:04.010Z", issues: [], @@ -1049,6 +1327,7 @@ describe("session runtime event store", () => { payload: { requestId: "permission-1", }, + runId: "run-1", }), occurredAt: 4_020, sourceEventId: "permission-resolved-source", @@ -1062,6 +1341,11 @@ describe("session runtime event store", () => { .all<{ request_id: string }>(); expect(remainingPermissions.results).toEqual([]); + expect( + await database + .prepare("SELECT status, status_seq FROM session_run WHERE id = 'run-1'") + .first(), + ).toEqual({ status: "running", status_seq: 2 }); }); test("rejects late event batches for archived sessions without allocating sequence", async () => { @@ -1203,6 +1487,42 @@ describe("session runtime event store", () => { persistedCount: 0, skippedSessionIds: ["session-1"], }); + expect( + await database + .prepare("SELECT runtime_event_seq_cursor FROM session WHERE id = 'session-1'") + .first(), + ).toEqual({ runtime_event_seq_cursor: 1 }); + }); + + test("does not consume one-event sequence numbers when the active-run fence rejects", async () => { + const database = createRuntimeEventStoreDatabase(); + database.execute("UPDATE session_run SET status = 'failed' WHERE id = 'run-1'"); + + await expect( + persistOneRuntimeEventPerSession(database, { + records: [ + { + event: runtimeEvent({ + id: "late-one-event", + kind: "agent.task.updated", + occurredAtMs: 4_050, + payload: { status: "completed" }, + runId: "run-1", + }), + occurredAt: 4_050, + sessionId: "session-1", + }, + ], + }), + ).rejects.toThrow("atomic session or active-run fence"); + expect( + await database + .prepare("SELECT runtime_event_seq_cursor FROM session WHERE id = 'session-1'") + .first(), + ).toEqual({ runtime_event_seq_cursor: 0 }); + expect(await database.prepare("SELECT COUNT(*) AS count FROM session_event").first()).toEqual({ + count: 0, + }); }); test("rejects one-event-per-session records for a different envelope session", async () => { diff --git a/apps/api/tests/session-title-mutation.test.ts b/apps/api/tests/session-title-mutation.test.ts index e1ca1559..19099bc1 100644 --- a/apps/api/tests/session-title-mutation.test.ts +++ b/apps/api/tests/session-title-mutation.test.ts @@ -4,6 +4,7 @@ import type { AuthenticatedViewer } from "../src/modules/auth/application/viewer import type { SessionSummaryRow } from "../src/modules/sessions/application/session-summary-query.service"; import { hydrateSessionSummariesFromRows } from "../src/modules/sessions/application/session-summary-query.service"; import { + applyDurableSessionAutoTitle, autoTitleSession, renameSession, } from "../src/modules/sessions/application/session-title.service"; @@ -29,6 +30,7 @@ function createSessionTitleMutationDatabase(): SqliteD1Database { id text PRIMARY KEY NOT NULL, agent_id text NOT NULL, archived_at integer, + auto_title_event_seq integer, attributed_user_id text, created_at integer NOT NULL, creator_account_id text NOT NULL, @@ -191,6 +193,62 @@ describe("session title mutations", () => { expect(session.title).toBe("Auto title"); }); + test("applies durable auto-titles by monotonic event seq", async () => { + const database = createSessionTitleMutationDatabase(); + const input = { + creatorAccountId: VIEWER.id, + eventSeq: 5, + sessionId: SESSION_ID, + title: "Title five", + }; + + await applyDurableSessionAutoTitle(database, input); + await applyDurableSessionAutoTitle(database, { + ...input, + eventSeq: 4, + title: "Stale title", + }); + await applyDurableSessionAutoTitle(database, input); + await expect( + applyDurableSessionAutoTitle(database, { ...input, title: "Conflicting title" }), + ).rejects.toThrow("replayed with conflicting content"); + await applyDurableSessionAutoTitle(database, { + ...input, + eventSeq: 6, + title: "Title six", + }); + + expect( + await database + .prepare("SELECT auto_title_event_seq, title FROM session WHERE id = ?") + .bind(SESSION_ID) + .first<{ auto_title_event_seq: number; title: string }>(), + ).toEqual({ auto_title_event_seq: 6, title: "Title six" }); + }); + + test("does not replace a title chosen outside the durable Driver stream", async () => { + const database = createSessionTitleMutationDatabase(); + + await autoTitleSession(database, VIEWER, { + appId: APP_ID, + sessionId: SESSION_ID, + title: "Prompt title", + }); + await applyDurableSessionAutoTitle(database, { + creatorAccountId: VIEWER.id, + eventSeq: 10, + sessionId: SESSION_ID, + title: "Driver title", + }); + + expect( + await database + .prepare("SELECT auto_title_event_seq, title FROM session WHERE id = ?") + .bind(SESSION_ID) + .first<{ auto_title_event_seq: number | null; title: string }>(), + ).toEqual({ auto_title_event_seq: null, title: "Prompt title" }); + }); + test("hydrates updated summary rows with their last runs", async () => { const database = createSessionTitleMutationDatabase(); diff --git a/apps/api/tests/session-viewer-event-delivery-buffer.test.ts b/apps/api/tests/session-viewer-event-delivery-buffer.test.ts index cfebf8ae..fff9f18c 100644 --- a/apps/api/tests/session-viewer-event-delivery-buffer.test.ts +++ b/apps/api/tests/session-viewer-event-delivery-buffer.test.ts @@ -14,6 +14,8 @@ interface Deferred { interface PublishedRequest { events: AgUiSessionEvent[]; + previousRuntimeEventSeqCursor?: number; + runtimeEventSeqCursor?: number; sessionId: string; } @@ -71,10 +73,12 @@ function createDeferred(): Deferred { } function createBufferHarness(): { + batchPublishCount: () => number; buffer: SessionViewerEventDeliveryBuffer; published: PublishedRequest[]; pushAfterResponse: (callback: () => void) => void; pushResponse: (response: Promise | Response) => void; + stateSyncCount: () => number; waitForPublish: () => Promise; waitForWaitUntil: () => Promise; } { @@ -83,10 +87,47 @@ function createBufferHarness(): { const afterResponseCallbacks: Array<() => void> = []; const responses: Array | Response> = []; const waitUntilTasks: Promise[] = []; + let batchPublishCount = 0; + let stateSyncCount = 0; const sessionStub = { - async publishEvents(sessionId: string, events: AgUiSessionEvent[]): Promise { + async publishEventBatches( + sessionId: string, + batches: Array<{ + events: AgUiSessionEvent[]; + previousRuntimeEventSeqCursor: number | null; + runtimeEventSeqCursor: number | null; + }>, + ): Promise { + batchPublishCount += 1; + for (const batch of batches) { + published.push({ + events: batch.events, + ...(batch.previousRuntimeEventSeqCursor === null + ? {} + : { previousRuntimeEventSeqCursor: batch.previousRuntimeEventSeqCursor }), + ...(batch.runtimeEventSeqCursor === null + ? {} + : { runtimeEventSeqCursor: batch.runtimeEventSeqCursor }), + sessionId, + }); + } + publishWaiters.shift()?.(); + const response = await (responses.shift() ?? new Response(null, { status: 204 })); + if (!response.ok) { + throw new Error(`Session event publish failed with status ${response.status}.`); + } + afterResponseCallbacks.shift()?.(); + }, + async publishEvents( + sessionId: string, + events: AgUiSessionEvent[], + runtimeEventSeqCursor: number | null, + previousRuntimeEventSeqCursor: number | null, + ): Promise { published.push({ events, + ...(previousRuntimeEventSeqCursor === null ? {} : { previousRuntimeEventSeqCursor }), + ...(runtimeEventSeqCursor === null ? {} : { runtimeEventSeqCursor }), sessionId, }); publishWaiters.shift()?.(); @@ -99,6 +140,9 @@ function createBufferHarness(): { afterResponseCallbacks.shift()?.(); }, + async syncViewers(): Promise { + stateSyncCount += 1; + }, }; const env = { Session: { @@ -119,6 +163,7 @@ function createBufferHarness(): { }); return { + batchPublishCount: () => batchPublishCount, buffer, published, pushAfterResponse: (callback) => { @@ -127,6 +172,7 @@ function createBufferHarness(): { pushResponse: (response) => { responses.push(response); }, + stateSyncCount: () => stateSyncCount, waitForPublish: () => new Promise((resolve) => { publishWaiters.push(resolve); @@ -198,6 +244,95 @@ describe("SessionViewerEventDeliveryBuffer", () => { ]); }); + test("keeps a delayed flush alive until the buffered events are published", async () => { + const { buffer, published, waitForWaitUntil } = createBufferHarness(); + + buffer.enqueue("session-1", [ + { delta: "batched", messageId: "assistant-1", type: "TEXT_MESSAGE_CONTENT" }, + ]); + await waitForWaitUntil(); + + expect(published).toEqual([ + { + events: [{ delta: "batched", messageId: "assistant-1", type: "TEXT_MESSAGE_CONTENT" }], + sessionId: "session-1", + }, + ]); + }); + + test("keeps different durable cursor generations in separate deliveries", async () => { + const { batchPublishCount, buffer, published } = createBufferHarness(); + + buffer.enqueue( + "session-1", + [{ delta: "one", messageId: "assistant-1", type: "TEXT_MESSAGE_CONTENT" }], + 1, + 0, + ); + buffer.enqueue( + "session-1", + [{ delta: "two", messageId: "assistant-1", type: "TEXT_MESSAGE_CONTENT" }], + 2, + 1, + ); + await buffer.flush(); + + expect(batchPublishCount()).toBe(1); + expect( + published.map(({ events, previousRuntimeEventSeqCursor, runtimeEventSeqCursor }) => ({ + events, + previousRuntimeEventSeqCursor, + runtimeEventSeqCursor, + })), + ).toEqual([ + { + events: [{ delta: "one", messageId: "assistant-1", type: "TEXT_MESSAGE_CONTENT" }], + previousRuntimeEventSeqCursor: 0, + runtimeEventSeqCursor: 1, + }, + { + events: [{ delta: "two", messageId: "assistant-1", type: "TEXT_MESSAGE_CONTENT" }], + previousRuntimeEventSeqCursor: 1, + runtimeEventSeqCursor: 2, + }, + ]); + }); + + test("keeps one durable cursor generation in one atomic delivery", async () => { + const { batchPublishCount, buffer, published } = createBufferHarness(); + const events: AgUiSessionEvent[] = Array.from({ length: 128 }, (_, index) => ({ + delta: [{ op: "replace", path: `/commands/${index}`, value: index }], + type: "STATE_DELTA", + })); + + buffer.enqueue("session-1", events, 128, 0); + await buffer.flush(); + + expect(batchPublishCount()).toBe(1); + expect(published).toHaveLength(1); + expect(published[0]).toMatchObject({ + events, + previousRuntimeEventSeqCursor: 0, + runtimeEventSeqCursor: 128, + }); + }); + + test("falls back to an authoritative state sync for an oversized durable generation", async () => { + const { batchPublishCount, buffer, published, stateSyncCount } = createBufferHarness(); + const oversized = createServerCustomEvent(MOSOO_CUSTOM_EVENT.sessionTasksReplaced.name, { + driverInstanceId: "driver-1", + runId: "run-1", + tasks: [{ taskId: "oversized", title: "x".repeat(maxSerializedBatchBytes + 1) }], + }); + + buffer.enqueue("session-1", [oversized], 1, 0); + await buffer.flush(); + + expect(batchPublishCount()).toBe(0); + expect(published).toEqual([]); + expect(stateSyncCount()).toBe(1); + }); + test("flushes the first delta of a run immediately, then batches the rest", async () => { const { buffer, published, waitForWaitUntil } = createBufferHarness(); const runStarted = { @@ -228,7 +363,6 @@ describe("SessionViewerEventDeliveryBuffer", () => { buffer.enqueue("session-1", [ { delta: "there", messageId: "assistant-1", type: "TEXT_MESSAGE_CONTENT" }, ]); - await waitForWaitUntil(); expect(published).toHaveLength(1); await buffer.flush(); @@ -320,8 +454,8 @@ describe("SessionViewerEventDeliveryBuffer", () => { expect(deliveredSnapshots).toEqual([inFlightSnapshot, pendingSnapshots.at(-1)]); }); - test("splits cross-generation task snapshots into byte-bounded batches", async () => { - const { buffer, published } = createBufferHarness(); + test("bounds a cross-generation backlog behind one authoritative state sync", async () => { + const { buffer, published, stateSyncCount } = createBufferHarness(); const snapshots = Array.from({ length: 3 }, (_, index) => createLargeTaskSnapshot({ driverInstanceId: `driver-${index}`, @@ -337,15 +471,17 @@ describe("SessionViewerEventDeliveryBuffer", () => { buffer.enqueue("session-1", [snapshots[2]]); await buffer.flush(); - expect(published).toHaveLength(3); - expect(published.map((request) => request.events.length)).toEqual([1, 1, 1]); + expect(published).toHaveLength(1); + expect(published.map((request) => request.events.length)).toEqual([1]); expect( published.every((request) => serializedEventBytes(request.events) <= maxSerializedBatchBytes), ).toBe(true); + expect(stateSyncCount()).toBe(1); }); test("retries failed bounded batches before events enqueued during the failed publish", async () => { - const { buffer, published, pushResponse, waitForPublish } = createBufferHarness(); + const { buffer, published, pushResponse, stateSyncCount, waitForPublish } = + createBufferHarness(); const failedResponse = createDeferred(); const failedSnapshot = createLargeTaskSnapshot({ driverInstanceId: "driver-1", @@ -378,14 +514,14 @@ describe("SessionViewerEventDeliveryBuffer", () => { expect(published.map((request) => request.events)).toEqual([ [failedSnapshot], [failedSnapshot], - [nextSnapshot], ]); + expect(stateSyncCount()).toBe(1); expect( published.every((request) => serializedEventBytes(request.events) <= maxSerializedBatchBytes), ).toBe(true); }); - test("keeps only the latest same-generation snapshot across repeated delivery failures", async () => { + test("drops derived payloads after delivery failure instead of hot-looping", async () => { const { buffer, published, pushResponse } = createBufferHarness(); const snapshots = Array.from({ length: 5 }, (_, index) => createLargeTaskSnapshot({ @@ -408,7 +544,6 @@ describe("SessionViewerEventDeliveryBuffer", () => { [snapshots[2]], [snapshots[3]], [snapshots[4]], - [snapshots[4]], ]); expect( published.every((request) => serializedEventBytes(request.events) <= maxSerializedBatchBytes), diff --git a/apps/api/tests/session-viewer-socket-state-order.test.ts b/apps/api/tests/session-viewer-socket-state-order.test.ts index 4532ffec..d0ff3865 100644 --- a/apps/api/tests/session-viewer-socket-state-order.test.ts +++ b/apps/api/tests/session-viewer-socket-state-order.test.ts @@ -11,6 +11,7 @@ import { import { createPromiseDeferred } from "@mosoo/effects"; import type { AuthenticatedViewer } from "../src/modules/auth/application/viewer-auth.service"; +import { loadSessionViewerStateSnapshot } from "../src/modules/sessions/infrastructure/session-viewer-live-snapshot.repository"; import { writeSessionViewerSocketHeaders } from "../src/modules/sessions/infrastructure/session/socket-headers"; import { SessionViewerSocketHub } from "../src/modules/sessions/infrastructure/session/viewer-socket-hub"; import type { ApiBindings } from "../src/platform/cloudflare/worker-types"; @@ -21,6 +22,7 @@ import { PUBLIC_API_TEST_IDS, } from "./helpers/public-api-http-test-fixture"; import type { SqliteD1Database } from "./helpers/sqlite-d1"; +import { insertRuntimeEvent } from "./public-thread-api-fixtures"; const DRIVER_ID = PUBLIC_API_TEST_IDS.driverOwner; const RUN_ID = PUBLIC_API_TEST_IDS.run; @@ -61,9 +63,15 @@ function createContext(): { } { const accepted: { socket: TestSocket; tags: string[] }[] = []; const pending: Promise[] = []; + const stored = new Map(); const storage = { - delete: async () => true, + delete: async (key: string) => stored.delete(key), deleteAlarm: async () => {}, + get: async (key: string) => stored.get(key) as T | undefined, + put: async (key: string, value: unknown) => { + stored.set(key, value); + }, + setAlarm: async () => {}, }; const ctx = { acceptWebSocket(socket: TestSocket, tags: string[]) { @@ -148,6 +156,127 @@ function deferFirstTaskSnapshotRead(database: SqliteD1Database): { }; } +function mutateAfterFirstViewerSnapshotRead( + database: SqliteD1Database, + mutate: () => Promise, +): { database: D1Database; readCount: () => number } { + let mutated = false; + let readCount = 0; + + function wrapStatement(statement: D1PreparedStatement, query: string): D1PreparedStatement { + return new Proxy(statement, { + get(target, property) { + if (property === "all" || property === "first" || property === "raw") { + return async (...args: unknown[]) => { + const read = Reflect.get(target, property) as ( + ...values: unknown[] + ) => Promise; + const result = await read.apply(target, args); + if (query.includes("session_agent_task_snapshot")) { + readCount += 1; + if (!mutated) { + mutated = true; + await mutate(); + } + } + return result; + }; + } + if (property === "bind") { + return (...values: unknown[]) => wrapStatement(target.bind(...values), query); + } + const value: unknown = Reflect.get(target, property); + return typeof value === "function" ? value.bind(target) : value; + }, + }); + } + + return { + database: new Proxy(database, { + get(target, property) { + if (property === "prepare") { + return (query: string) => wrapStatement(target.prepare(query), query); + } + const value: unknown = Reflect.get(target, property); + return typeof value === "function" ? value.bind(target) : value; + }, + }), + readCount: () => readCount, + }; +} + +function failFirstViewerSnapshotRead(database: SqliteD1Database): D1Database { + let failed = false; + + function wrapStatement(statement: D1PreparedStatement, query: string): D1PreparedStatement { + return new Proxy(statement, { + get(target, property) { + if (property === "all" || property === "first" || property === "raw") { + return async (...args: unknown[]) => { + if (!failed && query.includes("session_agent_task_snapshot")) { + failed = true; + throw new Error("injected initial viewer snapshot failure"); + } + + const read = Reflect.get(target, property) as ( + ...values: unknown[] + ) => Promise; + return read.apply(target, args); + }; + } + if (property === "bind") { + return (...values: unknown[]) => wrapStatement(target.bind(...values), query); + } + const value: unknown = Reflect.get(target, property); + return typeof value === "function" ? value.bind(target) : value; + }, + }); + } + + return new Proxy(database, { + get(target, property) { + if (property === "prepare") { + return (query: string) => wrapStatement(target.prepare(query), query); + } + const value: unknown = Reflect.get(target, property); + return typeof value === "function" ? value.bind(target) : value; + }, + }); +} + +function installTestWebSocketPair(): { restore(): void; sockets: TestSocket[] } { + const sockets: TestSocket[] = []; + const originalWebSocketPair = Reflect.get(globalThis, "WebSocketPair"); + Reflect.set(globalThis, "WebSocketPair", function TestWebSocketPair() { + const client = new TestSocket(); + const server = new TestSocket(); + sockets.push(client, server); + return [client, server]; + }); + + return { + restore() { + if (originalWebSocketPair === undefined) { + Reflect.deleteProperty(globalThis, "WebSocketPair"); + } else { + Reflect.set(globalThis, "WebSocketPair", originalWebSocketPair); + } + }, + sockets, + }; +} + +function createViewerSocketRequest(): Request { + const headers = new Headers({ upgrade: "websocket" }); + writeSessionViewerSocketHeaders(headers, { + publicOrigin: "https://mosoo.ai", + appId: PUBLIC_API_TEST_IDS.app, + sessionId: SESSION_ID, + viewer: VIEWER, + }); + return new Request("https://session.internal/viewer/ws", { headers }); +} + async function createDatabase(): Promise { const database = await createPublicHttpContractDatabase(); const now = Date.now(); @@ -205,6 +334,37 @@ async function createDatabase(): Promise { } describe("session viewer socket state ordering", () => { + test("retries a viewer snapshot when its durable cursor changes mid-read", async () => { + const database = await createDatabase(); + const messageId = "01J0000000000000000000002E"; + const guarded = mutateAfterFirstViewerSnapshotRead(database, async () => { + await insertRuntimeEvent(database, { + eventId: "01J0000000000000000000002F", + kind: "message.added", + occurredAt: Date.now(), + payload: { content: "Committed during snapshot.", messageId, role: "agent" }, + runId: RUN_ID, + seq: 1, + sessionId: SESSION_ID, + }); + await database + .prepare("UPDATE session SET runtime_event_seq_cursor = 1 WHERE id = ?") + .bind(SESSION_ID) + .run(); + }); + + const snapshot = await loadSessionViewerStateSnapshot(guarded.database, { + sessionId: SESSION_ID, + viewerId: VIEWER.id, + }); + + expect(guarded.readCount()).toBe(2); + expect(snapshot.runtimeEventSeqCursor).toBe(1); + expect(snapshot.state.messages).toContainEqual( + expect.objectContaining({ content: "Committed during snapshot.", id: messageId }), + ); + }); + test("linearizes a cold initial snapshot before a concurrent task broadcast", async () => { const database = await createDatabase(); const deferredRead = deferFirstTaskSnapshotRead(database); @@ -277,6 +437,139 @@ describe("session viewer socket state ordering", () => { ); expect(state.taskSnapshot?.tasks).toEqual([{ taskId: "latest" }]); + const durableMessageId = "01J0000000000000000000002A"; + await insertRuntimeEvent(database, { + eventId: "01J0000000000000000000002B", + kind: "message.added", + occurredAt: Date.now(), + payload: { + content: "Durable replay content.", + messageId: durableMessageId, + role: "agent", + }, + runId: RUN_ID, + seq: 1, + sessionId: SESSION_ID, + }); + database.execute(` + UPDATE session + SET runtime_event_seq_cursor = 1 + WHERE id = '${SESSION_ID}'; + `); + + expect( + hub.connect( + new Request("https://session.internal/viewer/ws", { + headers: new Headers(headers), + }), + ).status, + ).toBe(101); + const secondServer = sockets[3]; + if (!secondServer) { + throw new Error("Expected a second server websocket."); + } + await Promise.all(pending); + await hub.broadcastStateSync(); + + const durableSnapshot = parseAgUiSessionEventJson(server.frames.at(-1) ?? ""); + const secondDurableSnapshot = parseAgUiSessionEventJson(secondServer.frames.at(-1) ?? ""); + expect(secondDurableSnapshot).toEqual(durableSnapshot); + expect(durableSnapshot.type).toBe("STATE_SNAPSHOT"); + if (durableSnapshot.type !== "STATE_SNAPSHOT") { + throw new Error("Expected durable state snapshot."); + } + expect(durableSnapshot.snapshot.messages).toEqual([ + expect.objectContaining({ + content: "Durable replay content.", + id: durableMessageId, + }), + ]); + expect(durableSnapshot.snapshot.viewerId).toBe(VIEWER.id); + + const framesBeforeReplay = server.frames.length; + await hub.broadcastEvents( + [ + { + delta: " must-not-repeat", + messageId: durableMessageId, + type: "TEXT_MESSAGE_CONTENT", + }, + ], + 1, + 0, + ); + expect(server.frames).toHaveLength(framesBeforeReplay); + + await insertRuntimeEvent(database, { + eventId: "01J0000000000000000000002C", + kind: "message.delta", + occurredAt: Date.now(), + payload: { + content: " Appended once.", + messageId: durableMessageId, + }, + runId: RUN_ID, + seq: 2, + sessionId: SESSION_ID, + }); + database.execute(` + UPDATE session + SET runtime_event_seq_cursor = 2 + WHERE id = '${SESSION_ID}'; + `); + await hub.broadcastEvents( + [ + { + delta: " Appended once.", + messageId: durableMessageId, + type: "TEXT_MESSAGE_CONTENT", + }, + ], + 2, + 1, + ); + + await insertRuntimeEvent(database, { + eventId: "01J0000000000000000000002D", + kind: "message.delta", + occurredAt: Date.now(), + payload: { + content: " Gap recovered.", + messageId: durableMessageId, + }, + runId: RUN_ID, + seq: 3, + sessionId: SESSION_ID, + }); + database.execute(` + UPDATE session + SET runtime_event_seq_cursor = 3 + WHERE id = '${SESSION_ID}'; + `); + await hub.broadcastEvents( + [ + { + delta: " Gap recovered.", + messageId: durableMessageId, + type: "TEXT_MESSAGE_CONTENT", + }, + ], + 3, + 0, + ); + const recoveredGapSnapshot = parseAgUiSessionEventJson(server.frames.at(-1) ?? ""); + expect(recoveredGapSnapshot).toMatchObject({ + snapshot: { + messages: [ + expect.objectContaining({ + content: "Durable replay content. Appended once. Gap recovered.", + id: durableMessageId, + }), + ], + }, + type: "STATE_SNAPSHOT", + }); + await hub.handleSocketMessage( server as unknown as WebSocket, JSON.stringify( @@ -292,6 +585,45 @@ describe("session viewer socket state ordering", () => { throw new Error("Expected reconnect state snapshot."); } expect(reconnectSnapshot.snapshot.taskSnapshot?.tasks).toEqual([{ taskId: "latest" }]); + expect(reconnectSnapshot.snapshot.messages).toEqual([ + expect.objectContaining({ + content: "Durable replay content. Appended once. Gap recovered.", + id: durableMessageId, + }), + ]); + + await insertRuntimeEvent(database, { + eventId: "01J0000000000000000000002G", + kind: "message.delta", + occurredAt: Date.now(), + payload: { + content: " Alarm recovered.", + messageId: durableMessageId, + }, + runId: RUN_ID, + seq: 4, + sessionId: SESSION_ID, + }); + database.execute(` + UPDATE session + SET runtime_event_seq_cursor = 4 + WHERE id = '${SESSION_ID}'; + `); + await hub.handleAlarm(); + + const alarmSnapshot = parseAgUiSessionEventJson(server.frames.at(-1) ?? ""); + expect(alarmSnapshot).toMatchObject({ + snapshot: { + messages: [ + expect.objectContaining({ + content: "Durable replay content. Appended once. Gap recovered. Alarm recovered.", + id: durableMessageId, + }), + ], + }, + type: "STATE_SNAPSHOT", + }); + expect(server.attachment).toMatchObject({ runtimeEventSeqCursor: 4 }); } finally { if (originalWebSocketPair === undefined) { Reflect.deleteProperty(globalThis, "WebSocketPair"); @@ -300,4 +632,123 @@ describe("session viewer socket state ordering", () => { } } }); + + test("retries the authoritative snapshot before delivering a delta after initial sync fails", async () => { + const database = await createDatabase(); + const { ctx, pending } = createContext(); + const pair = installTestWebSocketPair(); + const hub = new SessionViewerSocketHub({ + ctx, + env: { + ...createPublicHttpTestBindings(database), + DB: failFirstViewerSnapshotRead(database), + } as ApiBindings, + getSessionId: () => SESSION_ID, + rememberSessionId: () => {}, + withSessionLogContext: (operation) => operation(), + }); + + try { + expect(hub.connect(createViewerSocketRequest()).status).toBe(101); + await Promise.allSettled(pending); + const server = pair.sockets[1]; + if (!server) { + throw new Error("Expected a server websocket."); + } + expect(server.frames).toEqual([]); + expect(server.attachment).not.toHaveProperty("runtimeEventSeqCursor"); + + const messageId = "01J0000000000000000000002H"; + await insertRuntimeEvent(database, { + eventId: "01J0000000000000000000002J", + kind: "message.added", + occurredAt: Date.now(), + payload: { content: "Recovered base.", messageId, role: "agent" }, + runId: RUN_ID, + seq: 1, + sessionId: SESSION_ID, + }); + database.execute(` + UPDATE session + SET runtime_event_seq_cursor = 1 + WHERE id = '${SESSION_ID}'; + `); + + await hub.broadcastEvents( + [{ delta: "Recovered base.", messageId, type: "TEXT_MESSAGE_CONTENT" }], + 1, + 0, + ); + + expect(server.frames).toHaveLength(1); + expect(parseAgUiSessionEventJson(server.frames[0] ?? "")).toMatchObject({ + snapshot: { + messages: [expect.objectContaining({ content: "Recovered base.", id: messageId })], + }, + type: "STATE_SNAPSHOT", + }); + expect(server.attachment).toMatchObject({ runtimeEventSeqCursor: 1 }); + } finally { + pair.restore(); + } + }); + + test("recovers the Session identity from socket attachments after hibernation", async () => { + const database = await createDatabase(); + const { ctx, pending } = createContext(); + const pair = installTestWebSocketPair(); + const initialHub = new SessionViewerSocketHub({ + ctx, + env: createPublicHttpTestBindings(database) as ApiBindings, + getSessionId: () => SESSION_ID, + rememberSessionId: () => {}, + withSessionLogContext: (operation) => operation(), + }); + + try { + expect(initialHub.connect(createViewerSocketRequest()).status).toBe(101); + await Promise.all(pending); + const server = pair.sockets[1]; + if (!server) { + throw new Error("Expected a server websocket."); + } + + const messageId = "01J0000000000000000000002K"; + await insertRuntimeEvent(database, { + eventId: "01J0000000000000000000002M", + kind: "message.added", + occurredAt: Date.now(), + payload: { content: "Alarm recovery.", messageId, role: "agent" }, + runId: RUN_ID, + seq: 1, + sessionId: SESSION_ID, + }); + database.execute(` + UPDATE session + SET runtime_event_seq_cursor = 1 + WHERE id = '${SESSION_ID}'; + `); + + const recoveredSessionIds: string[] = []; + const hibernatedHub = new SessionViewerSocketHub({ + ctx, + env: createPublicHttpTestBindings(database) as ApiBindings, + getSessionId: () => null, + rememberSessionId: (sessionId) => recoveredSessionIds.push(sessionId), + withSessionLogContext: (operation) => operation(), + }); + await hibernatedHub.handleAlarm(); + + expect(recoveredSessionIds).toContain(SESSION_ID); + expect(parseAgUiSessionEventJson(server.frames.at(-1) ?? "")).toMatchObject({ + snapshot: { + messages: [expect.objectContaining({ content: "Alarm recovery.", id: messageId })], + }, + type: "STATE_SNAPSHOT", + }); + expect(server.attachment).toMatchObject({ runtimeEventSeqCursor: 1 }); + } finally { + pair.restore(); + } + }); }); diff --git a/apps/api/tests/session-viewer-state.test.ts b/apps/api/tests/session-viewer-state.test.ts index 6179bde3..5deb2864 100644 --- a/apps/api/tests/session-viewer-state.test.ts +++ b/apps/api/tests/session-viewer-state.test.ts @@ -1,8 +1,17 @@ import { describe, expect, test } from "bun:test"; import { loadSessionViewerState } from "../src/modules/sessions/infrastructure/session-viewer-live-snapshot.repository"; +import { loadViewerLiveState } from "../src/modules/sessions/infrastructure/session/viewer-live-state"; import { SqliteD1Database } from "./helpers/sqlite-d1"; +const VIEWER = { + email: "viewer@example.com", + emailVerified: true, + id: "viewer-1", + imageUrl: null, + name: "Viewer", +}; + function createSessionViewerStateDatabase(): SqliteD1Database { const database = new SqliteD1Database({ foreignKeys: false }); @@ -11,6 +20,7 @@ function createSessionViewerStateDatabase(): SqliteD1Database { archived_at integer, id text PRIMARY KEY NOT NULL, last_run_id text, + runtime_event_seq_cursor integer NOT NULL DEFAULT 0, status text NOT NULL, title text, updated_at integer NOT NULL @@ -25,6 +35,7 @@ function createSessionViewerStateDatabase(): SqliteD1Database { error_code text, error_details_json text, error_message text, + error_retryable integer, id text PRIMARY KEY NOT NULL, model text, provider text, @@ -49,10 +60,12 @@ function createSessionViewerStateDatabase(): SqliteD1Database { created_at integer NOT NULL, id text PRIMARY KEY NOT NULL, plan_json text, + projection_format text NOT NULL DEFAULT 'materialized', role text NOT NULL, segments_json text, seq integer NOT NULL, - session_id text NOT NULL + session_id text NOT NULL, + session_run_id text ); CREATE TABLE file_record ( @@ -70,6 +83,7 @@ function createSessionViewerStateDatabase(): SqliteD1Database { parent_path text NOT NULL, path text NOT NULL, purpose text NOT NULL, + runtime_event_seq integer, scope_id text NOT NULL, scope_kind text NOT NULL, session_kind text, @@ -79,6 +93,16 @@ function createSessionViewerStateDatabase(): SqliteD1Database { version integer NOT NULL ); + CREATE TABLE session_artifact_head ( + file_id text, + runtime_event_seq integer NOT NULL, + session_id text NOT NULL, + source_event_id text NOT NULL, + source_path text NOT NULL, + updated_at integer NOT NULL, + UNIQUE (session_id, source_path) + ); + CREATE TABLE session_permission_request ( created_at integer NOT NULL, driver_instance_id text NOT NULL, @@ -261,6 +285,188 @@ describe("session viewer state", () => { }); }); + test("refreshes the independently durable task snapshot when the live-state cache is stale", async () => { + const database = createSessionViewerStateDatabase(); + database.execute(` + INSERT INTO session_agent_task_snapshot ( + driver_instance_id, + run_id, + seq, + session_id, + tasks_json + ) + VALUES ('driver-1', 'run-1', 7, 'session-1', '{"tasks":[{"taskId":"stale"}]}'); + `); + const cachedState = await loadSessionViewerState(database, { + sessionId: "session-1", + viewerId: VIEWER.id, + }); + database.execute(` + UPDATE session_agent_task_snapshot + SET seq = 8, tasks_json = '{"tasks":[{"taskId":"latest"}]}' + WHERE session_id = 'session-1'; + `); + + const state = await loadViewerLiveState({ + cachedState, + database, + reconciledStaleRun: false, + sessionId: "session-1", + viewer: VIEWER, + }); + + expect(state.taskSnapshot?.tasks).toEqual([{ taskId: "latest" }]); + expect(state.messages).toEqual(cachedState.messages); + }); + + test("reloads the canonical viewer generation when a newer run broadcast was missed", async () => { + const database = createSessionViewerStateDatabase(); + database.execute(` + INSERT INTO session_agent_task_snapshot ( + driver_instance_id, + run_id, + seq, + session_id, + tasks_json + ) + VALUES ('driver-1', 'run-1', 7, 'session-1', '{"tasks":[{"taskId":"run-1"}]}'); + `); + const cachedState = await loadSessionViewerState(database, { + sessionId: "session-1", + viewerId: VIEWER.id, + }); + database.execute(` + INSERT INTO session_run ( + completed_at, + created_at, + driver_instance_id, + id, + model, + provider, + session_id, + started_at, + status, + trace_id, + trigger, + updated_at + ) + VALUES ( + NULL, + 31, + 'driver-2', + 'run-2', + 'gpt-5.4', + 'openai', + 'session-1', + 32, + 'running', + 'trace-2', + 'user_message', + 33 + ); + UPDATE session + SET last_run_id = 'run-2', updated_at = 34 + WHERE id = 'session-1'; + UPDATE session_agent_task_snapshot + SET + driver_instance_id = 'driver-2', + run_id = 'run-2', + seq = 8, + tasks_json = '{"tasks":[{"taskId":"run-2"}]}' + WHERE session_id = 'session-1'; + `); + + const state = await loadViewerLiveState({ + cachedState, + database, + reconciledStaleRun: false, + sessionId: "session-1", + viewer: VIEWER, + }); + + expect(state.run).toMatchObject({ id: "run-2", status: "running", traceId: "trace-2" }); + expect(state.infra.driverInstanceId).toBe("driver-2"); + expect(state.taskSnapshot).toEqual({ + driverInstanceId: "driver-2", + runId: "run-2", + tasks: [{ taskId: "run-2" }], + }); + }); + + test("reloads canonical terminal state when its live broadcast was missed", async () => { + const database = createSessionViewerStateDatabase(); + database.execute(` + INSERT INTO session_agent_task_snapshot ( + driver_instance_id, + run_id, + seq, + session_id, + tasks_json + ) + VALUES ('driver-1', 'run-1', 7, 'session-1', '{"tasks":[{"taskId":"running"}]}'); + `); + const cachedState = await loadSessionViewerState(database, { + sessionId: "session-1", + viewerId: VIEWER.id, + }); + database.execute(` + UPDATE session + SET status = 'IDLE', updated_at = 40 + WHERE id = 'session-1'; + UPDATE session_run + SET completed_at = 39, status = 'completed', updated_at = 39 + WHERE id = 'run-1'; + `); + + const state = await loadViewerLiveState({ + cachedState, + database, + reconciledStaleRun: false, + sessionId: "session-1", + viewer: VIEWER, + }); + + expect(state.lifecycle).toBe("IDLE"); + expect(state.run).toMatchObject({ id: "run-1", status: "completed" }); + expect(state.infra.driverInstanceId).toBeNull(); + expect(state.taskSnapshot).toBeNull(); + }); + + test.each([ + ["canonical row is removed", "DELETE FROM session_agent_task_snapshot"], + [ + "canonical run becomes terminal", + "UPDATE session_run SET status = 'completed' WHERE id = 'run-1'", + ], + ])("clears a cached task snapshot when the %s", async (_label, boundarySql) => { + const database = createSessionViewerStateDatabase(); + database.execute(` + INSERT INTO session_agent_task_snapshot ( + driver_instance_id, + run_id, + seq, + session_id, + tasks_json + ) + VALUES ('driver-1', 'run-1', 7, 'session-1', '{"tasks":[{"taskId":"stale"}]}'); + `); + const cachedState = await loadSessionViewerState(database, { + sessionId: "session-1", + viewerId: VIEWER.id, + }); + database.execute(boundarySql); + + const state = await loadViewerLiveState({ + cachedState, + database, + reconciledStaleRun: false, + sessionId: "session-1", + viewer: VIEWER, + }); + + expect(state.taskSnapshot).toBeNull(); + }); + test.each([ ["terminal run", "UPDATE session_run SET status = 'completed' WHERE id = 'run-1'"], ["rescheduling session", "UPDATE session SET status = 'RESCHEDULING' WHERE id = 'session-1'"], diff --git a/apps/api/tests/skill-package-snapshot.test.ts b/apps/api/tests/skill-package-snapshot.test.ts index e8e7123e..433dbe75 100644 --- a/apps/api/tests/skill-package-snapshot.test.ts +++ b/apps/api/tests/skill-package-snapshot.test.ts @@ -117,7 +117,7 @@ describe("publishSkillSnapshot", () => { const limitedDatabase = new BindLimitedD1Database(sqlite, 100); const bucket = new PublicApiMemoryFileBucket(); const bindings = createPublicHttpTestBindings(limitedDatabase, { - fileBucket: bucket as unknown as R2Bucket, + fileBucket: bucket, }) as ApiBindings; const published = await publishSkillSnapshot( diff --git a/apps/api/tests/sqlite-d1.test.ts b/apps/api/tests/sqlite-d1.test.ts new file mode 100644 index 00000000..3b481a52 --- /dev/null +++ b/apps/api/tests/sqlite-d1.test.ts @@ -0,0 +1,24 @@ +import { expect, test } from "bun:test"; + +import { SqliteD1Database } from "./helpers/sqlite-d1"; + +test("serialized SQLite clones isolate data and transactions", async () => { + const template = new SqliteD1Database(); + template.execute("CREATE TABLE item (id integer PRIMARY KEY)"); + const bytes = template.serialize(); + const first = new SqliteD1Database({ serialized: bytes }); + const second = new SqliteD1Database({ serialized: bytes }); + + await expect( + first.batch([ + first.prepare("INSERT INTO item (id) VALUES (1)"), + first.prepare("INSERT INTO item (id) VALUES (1)"), + ]), + ).rejects.toThrow(); + await first.prepare("INSERT INTO item (id) VALUES (2)").run(); + + await expect(first.prepare("SELECT id FROM item").all()).resolves.toMatchObject({ + results: [{ id: 2 }], + }); + await expect(second.prepare("SELECT id FROM item").all()).resolves.toMatchObject({ results: [] }); +}); diff --git a/apps/api/tests/terminal-run-release-recovery.test.ts b/apps/api/tests/terminal-run-release-recovery.test.ts new file mode 100644 index 00000000..e3ff1d5c --- /dev/null +++ b/apps/api/tests/terminal-run-release-recovery.test.ts @@ -0,0 +1,325 @@ +import { describe, expect, test } from "bun:test"; + +import type { DriverInstanceId, SessionRunId } from "@mosoo/id"; + +import { recordCanonicalSessionRunFailure } from "../src/modules/runtime/application/session-runs/session-run-terminal-failure.service"; +import { classifyReclaim } from "../src/modules/runtime/domain/session-run-reclaim-recovery"; +import { createSessionRunTerminalFailureSourceId } from "../src/modules/runtime/domain/session-run-terminal-event-id"; +import { recordDriverInstanceFailure } from "../src/modules/runtime/infrastructure/driver-instance/terminal-driver-events"; +import { + releaseTerminalDriverInstanceSessionRun, + repairFinalizedTerminalDriverRunState, +} from "../src/modules/runtime/infrastructure/driver-instance/terminal-run-release"; +import type { ApiBindings } from "../src/platform/cloudflare/worker-types"; +import { + createPublicHttpContractDatabase, + createPublicHttpTestBindings, + insertActiveSandboxSessionFixture, + insertOwnerSession, + PUBLIC_API_TEST_IDS, +} from "./helpers/public-api-http-test-fixture"; +import type { SqliteD1Database } from "./helpers/sqlite-d1"; + +const DRIVER_INSTANCE_ID = PUBLIC_API_TEST_IDS.driverOwner as DriverInstanceId; +const SESSION_RUN_ID = "01J0000000000000000000000T" as SessionRunId; +const SUCCESSOR_SESSION_RUN_ID = "01J0000000000000000000000V" as SessionRunId; + +async function createTerminalReleaseFixture(): Promise<{ + bindings: ApiBindings; + database: SqliteD1Database; +}> { + const database = await createPublicHttpContractDatabase(); + await insertOwnerSession(database); + await insertActiveSandboxSessionFixture(database, { + ownerAccountId: PUBLIC_API_TEST_IDS.ownerAccount, + sandboxId: PUBLIC_API_TEST_IDS.sandbox, + sandboxSessionId: "01J0000000000000000000000W", + sessionId: PUBLIC_API_TEST_IDS.ownerSession, + timestampMs: 1, + }); + database.execute(` + INSERT INTO driver_instance ( + id, boot_token_expires_at, boot_token_hash, connection_id, created_at, + expires_at, heartbeat_count, protocol, protocol_version, runtime, + sandbox_id, sandbox_incarnation, sandbox_session_id, status, updated_at + ) VALUES ( + '${DRIVER_INSTANCE_ID}', 1, X'01', 'terminal-recovery-connection', 1, + 1, 0, 'orpc-ws', 1, 'openai-runtime', '${PUBLIC_API_TEST_IDS.sandbox}', + 1, '${PUBLIC_API_TEST_IDS.ownerSession}', 'stopped', 1 + ); + + INSERT INTO session_run ( + id, session_id, agent_id, created_by_account_id, deployment_version_id, + deployment_version_number, driver_instance_id, trigger, status, provider, + model, runtime_id, trace_id, started_at, created_at, updated_at + ) VALUES ( + '${SESSION_RUN_ID}', '${PUBLIC_API_TEST_IDS.ownerSession}', '${PUBLIC_API_TEST_IDS.agent}', + '${PUBLIC_API_TEST_IDS.ownerAccount}', '${PUBLIC_API_TEST_IDS.deployment}', 1, + '${DRIVER_INSTANCE_ID}', 'user_prompt', 'running', 'openai', 'gpt-5.4', + 'openai-runtime', 'trace-terminal-recovery', 1, 1, 1 + ); + + UPDATE session + SET last_run_id = '${SESSION_RUN_ID}', status = 'RUNNING' + WHERE id = '${PUBLIC_API_TEST_IDS.ownerSession}'; + `); + + return { + bindings: createPublicHttpTestBindings(database) as ApiBindings, + database, + }; +} + +async function recordReclaimFailure(bindings: ApiBindings): Promise { + const outcome = await recordCanonicalSessionRunFailure(bindings, { + error: classifyReclaim({ + driverInstanceId: DRIVER_INSTANCE_ID, + driverTerminalStatus: "stopped", + reclaimReason: "socket_closed", + }), + runId: SESSION_RUN_ID, + sessionId: PUBLIC_API_TEST_IDS.ownerSession, + source: "driver", + }); + + expect(outcome.kind).toBe("failed"); +} + +async function readReleaseState(database: D1Database): Promise<{ + inactiveDeadlineAt: number | null; + sandboxUpdatedAt: number; +}> { + const row = await database + .prepare( + "SELECT inactive_deadline_at AS inactiveDeadlineAt, updated_at AS sandboxUpdatedAt FROM sandbox WHERE id = ?", + ) + .bind(PUBLIC_API_TEST_IDS.sandbox) + .first<{ + inactiveDeadlineAt: number | null; + sandboxUpdatedAt: number; + }>(); + + if (row === null) { + throw new Error("Sandbox release state was not found."); + } + + return row; +} + +describe("terminal Run release recovery", () => { + test("releases a Driver failure reported before the connection becomes ready", async () => { + const { bindings, database } = await createTerminalReleaseFixture(); + database.execute(` + UPDATE driver_instance + SET status = 'connecting', status_operation_id = NULL + WHERE id = '${DRIVER_INSTANCE_ID}'; + `); + + await recordDriverInstanceFailure(bindings, { + driverConnectionId: "terminal-recovery-connection", + driverGeneration: 0, + driverInstanceId: DRIVER_INSTANCE_ID, + sessionRunId: SESSION_RUN_ID, + error: { + code: "driver.startup_failed", + details: {}, + message: "Driver failed before ready.", + retryable: true, + }, + }); + await expect( + database + .prepare("SELECT status, status_operation_id FROM driver_instance WHERE id = ?") + .bind(DRIVER_INSTANCE_ID) + .first(), + ).resolves.toEqual({ status: "stopping", status_operation_id: SESSION_RUN_ID }); + await expect( + database + .prepare("SELECT error_code, status FROM session_run WHERE id = ?") + .bind(SESSION_RUN_ID) + .first(), + ).resolves.toEqual({ error_code: "driver.startup_failed", status: "failed" }); + }); + + test("does not release a replacement connection from an old terminal RPC", async () => { + const { bindings, database } = await createTerminalReleaseFixture(); + await recordReclaimFailure(bindings); + const beforeRelease = await readReleaseState(database); + const commandId = "01J0000000000000000000000X"; + + await database + .prepare( + `INSERT INTO driver_command ( + driver_generation, driver_instance_id, id, issued_at, kind, + payload_json, seq, status + ) VALUES (?, ?, ?, ?, 'input.start', ?, ?, 'accepted')`, + ) + .bind( + 0, + DRIVER_INSTANCE_ID, + commandId, + 1, + JSON.stringify({ + commandId, + input: { text: "continue" }, + kind: "input.start", + requestId: "replacement-command", + runId: SESSION_RUN_ID, + }), + 1, + ) + .run(); + database.execute(` + UPDATE driver_instance + SET connection_id = 'replacement-connection', status_operation_id = NULL + WHERE id = '${DRIVER_INSTANCE_ID}'; + `); + + await expect( + releaseTerminalDriverInstanceSessionRun(bindings, { + expectedDriverConnectionId: "terminal-recovery-connection", + driverGeneration: 0, + driverInstanceId: DRIVER_INSTANCE_ID, + sessionRunId: SESSION_RUN_ID, + }), + ).rejects.toThrow("lost its exact Driver ownership"); + await expect( + database + .prepare("SELECT connection_id, status_operation_id FROM driver_instance WHERE id = ?") + .bind(DRIVER_INSTANCE_ID) + .first(), + ).resolves.toEqual({ + connection_id: "replacement-connection", + status_operation_id: null, + }); + await expect( + database + .prepare("SELECT error_json, result_json, status FROM driver_command WHERE id = ?") + .bind(commandId) + .first(), + ).resolves.toEqual({ error_json: null, result_json: null, status: "accepted" }); + expect(await readReleaseState(database)).toEqual(beforeRelease); + }); + + test("does not let an old terminal Run claim a Driver owned by an active successor", async () => { + const { bindings, database } = await createTerminalReleaseFixture(); + await recordReclaimFailure(bindings); + database.execute(` + INSERT INTO session_run ( + id, session_id, agent_id, created_by_account_id, deployment_version_id, + deployment_version_number, driver_instance_id, trigger, status, provider, + model, runtime_id, trace_id, started_at, created_at, updated_at + ) VALUES ( + '${SUCCESSOR_SESSION_RUN_ID}', '${PUBLIC_API_TEST_IDS.ownerSession}', + '${PUBLIC_API_TEST_IDS.agent}', '${PUBLIC_API_TEST_IDS.ownerAccount}', + '${PUBLIC_API_TEST_IDS.deployment}', 1, '${DRIVER_INSTANCE_ID}', + 'user_prompt', 'running', 'openai', 'gpt-5.4', 'openai-runtime', + 'trace-terminal-successor', 2, 2, 2 + ); + + UPDATE session + SET last_run_id = '${SUCCESSOR_SESSION_RUN_ID}', status = 'RUNNING' + WHERE id = '${PUBLIC_API_TEST_IDS.ownerSession}'; + + UPDATE driver_instance + SET status = 'ready', status_operation_id = NULL + WHERE id = '${DRIVER_INSTANCE_ID}'; + `); + + await expect( + releaseTerminalDriverInstanceSessionRun(bindings, { + driverGeneration: 0, + driverInstanceId: DRIVER_INSTANCE_ID, + sessionRunId: SESSION_RUN_ID, + }), + ).rejects.toThrow("lost its exact Driver ownership"); + await expect( + database + .prepare("SELECT status, status_operation_id FROM driver_instance WHERE id = ?") + .bind(DRIVER_INSTANCE_ID) + .first(), + ).resolves.toEqual({ status: "ready", status_operation_id: null }); + await expect( + database + .prepare("SELECT driver_instance_id, status FROM session_run WHERE id = ?") + .bind(SUCCESSOR_SESSION_RUN_ID) + .first(), + ).resolves.toEqual({ driver_instance_id: DRIVER_INSTANCE_ID, status: "running" }); + }); + + test("repairs the lease from a complete canonical terminal projection", async () => { + const { bindings, database } = await createTerminalReleaseFixture(); + await recordReclaimFailure(bindings); + const sourceEventId = createSessionRunTerminalFailureSourceId(SESSION_RUN_ID); + + await expect( + database + .prepare("SELECT COUNT(*) AS count FROM session_event WHERE source_event_id = ?") + .bind(sourceEventId) + .first(), + ).resolves.toEqual({ count: 1 }); + + await expect( + repairFinalizedTerminalDriverRunState(bindings, { + driverGeneration: 0, + driverInstanceId: DRIVER_INSTANCE_ID, + sessionRunId: SESSION_RUN_ID, + status: "stopped", + }), + ).resolves.toMatchObject({ released: true }); + const firstRelease = await readReleaseState(database); + + await expect( + repairFinalizedTerminalDriverRunState(bindings, { + driverGeneration: 0, + driverInstanceId: DRIVER_INSTANCE_ID, + sessionRunId: SESSION_RUN_ID, + status: "stopped", + }), + ).resolves.toMatchObject({ released: true }); + await expect( + database + .prepare("SELECT COUNT(*) AS count FROM session_event WHERE source_event_id = ?") + .bind(sourceEventId) + .first(), + ).resolves.toEqual({ count: 1 }); + expect(await readReleaseState(database)).toEqual(firstRelease); + }); + + test("converges after the atomic lease release commits but its acknowledgement is lost", async () => { + const { bindings, database } = await createTerminalReleaseFixture(); + await recordReclaimFailure(bindings); + const originalBatch = database.batch; + let loseAcknowledgement = true; + database.batch = (async (statements: D1PreparedStatement[]) => { + const results = await originalBatch.call(database, statements); + + if (loseAcknowledgement) { + loseAcknowledgement = false; + throw new Error("lease release acknowledgement lost"); + } + + return results as D1Result[]; + }) as D1Database["batch"]; + + await expect( + releaseTerminalDriverInstanceSessionRun(bindings, { + driverGeneration: 0, + driverInstanceId: DRIVER_INSTANCE_ID, + sessionRunId: SESSION_RUN_ID, + }), + ).rejects.toThrow("lease release acknowledgement lost"); + const committedRelease = await readReleaseState(database); + expect(committedRelease.inactiveDeadlineAt).not.toBeNull(); + + database.batch = originalBatch; + await expect( + releaseTerminalDriverInstanceSessionRun(bindings, { + driverGeneration: 0, + driverInstanceId: DRIVER_INSTANCE_ID, + sessionRunId: SESSION_RUN_ID, + }), + ).resolves.toMatchObject({ released: true }); + expect(await readReleaseState(database)).toEqual(committedRelease); + }); +}); diff --git a/apps/api/tests/wrangler-d1-migration-atomicity.test.ts b/apps/api/tests/wrangler-d1-migration-atomicity.test.ts new file mode 100644 index 00000000..85a6dc2a --- /dev/null +++ b/apps/api/tests/wrangler-d1-migration-atomicity.test.ts @@ -0,0 +1,510 @@ +import { expect, test } from "bun:test"; +import { spawnSync } from "node:child_process"; +import { copyFileSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { fileURLToPath } from "node:url"; + +import { parseD1JsonResults } from "../bin/d1-json"; +import { + createProdSchemaCatalogFromIntrospectionRows, + DRIZZLE_MIGRATION_PROD_SCHEMA_CATALOG, + createProdSchemaIntrospectionStatements, + findProdSchemaDifferences, +} from "../bin/prod-schema-guard"; +import { + installProtocolV3CutoverSql, + installProtocolV3PostMigrationCutoverSql, + PROTOCOL_V3_CUTOVER_OBJECT_COUNT, + PROTOCOL_V3_CUTOVER_OBJECTS_SQL, + PROTOCOL_V3_POST_MIGRATION_CUTOVER_OBJECT_COUNT, +} from "../bin/protocol-v3-cutover"; +import { drizzleMigrations, getDrizzleMigration } from "./helpers/drizzle-migrations"; + +const MIGRATION_TAG = "0019_runtime-subject-operation-authority"; +const SCHEMA_MIGRATION_TAG = "0020_sandbox-backup-object-authority"; +const RELEASE_TREE_OID = "0123456789abcdef0123456789abcdef01234567"; +const migrationsSource = fileURLToPath(new URL("../../../pkgs/db/drizzle/", import.meta.url)); +const wrangler = fileURLToPath(new URL("../node_modules/.bin/wrangler", import.meta.url)); + +interface WranglerResult { + readonly output: string; + readonly status: number | null; +} + +function runWrangler(root: string, args: readonly string[]): WranglerResult { + const result = spawnSync(wrangler, args, { + cwd: root, + encoding: "utf8", + env: { + ...process.env, + CI: "1", + NO_UPDATE_NOTIFIER: "1", + TMPDIR: join(root, "tmp"), + WRANGLER_SEND_METRICS: "false", + WRANGLER_LOG_PATH: join(root, "logs"), + WRANGLER_WRITE_LOGS: "false", + XDG_CACHE_HOME: join(root, "cache"), + XDG_CONFIG_HOME: join(root, "config"), + }, + timeout: 120_000, + }); + if (result.error) throw result.error; + return { + output: `${result.stdout ?? ""}\n${result.stderr ?? ""}`, + status: result.status, + }; +} + +function runWranglerSuccessfully(root: string, args: readonly string[]): string { + const result = runWrangler(root, args); + if (result.status !== 0) { + throw new Error(`wrangler ${args.join(" ")} failed:\n${result.output}`); + } + return result.output; +} + +function localD1Args(state: string, action: "execute" | "migrations", tail: string[]): string[] { + return [ + "d1", + action, + ...(action === "migrations" ? ["apply"] : []), + "DB", + "--local", + "--persist-to", + state, + ...tail, + ]; +} + +interface MigrationStateExpectation { + readonly authorityColumnCount: number; + readonly backupAuthorityWithoutRowidCount: number; + readonly gateObjectCount: number; + readonly lastMigration: string; + readonly stagingClaimOwnerCount: number; + readonly stagingIndexCount: number; + readonly stagingTableCount: number; + readonly targetApplied: number; +} + +interface MigrationAtomicityFixture { + readonly failurePoint: string; + readonly gateSql: string; + readonly initialGateObjectCount: number; + readonly recovered: MigrationStateExpectation; + readonly rolledBack: MigrationStateExpectation; + readonly verifyLatestCatalog: boolean; +} + +const MIGRATION_ATOMICITY_CASES = [ + [ + MIGRATION_TAG, + { + failurePoint: "ALTER TABLE `driver_instance` ADD `sandbox_incarnation`", + gateSql: installProtocolV3CutoverSql(RELEASE_TREE_OID), + initialGateObjectCount: PROTOCOL_V3_CUTOVER_OBJECT_COUNT, + recovered: { + authorityColumnCount: 1, + backupAuthorityWithoutRowidCount: 0, + gateObjectCount: PROTOCOL_V3_POST_MIGRATION_CUTOVER_OBJECT_COUNT, + lastMigration: `${MIGRATION_TAG}.sql`, + stagingClaimOwnerCount: 0, + stagingIndexCount: 4, + stagingTableCount: 2, + targetApplied: 1, + }, + rolledBack: { + authorityColumnCount: 0, + backupAuthorityWithoutRowidCount: 0, + gateObjectCount: PROTOCOL_V3_CUTOVER_OBJECT_COUNT, + lastMigration: "0018_runtime-operation-ready-authority.sql", + stagingClaimOwnerCount: 0, + stagingIndexCount: 0, + stagingTableCount: 0, + targetApplied: 0, + }, + verifyLatestCatalog: false, + }, + ], + [ + SCHEMA_MIGRATION_TAG, + { + failurePoint: "CREATE TRIGGER `sandbox_backup_delete_intent_authority`", + gateSql: installProtocolV3PostMigrationCutoverSql(RELEASE_TREE_OID), + initialGateObjectCount: PROTOCOL_V3_POST_MIGRATION_CUTOVER_OBJECT_COUNT, + recovered: { + authorityColumnCount: 1, + backupAuthorityWithoutRowidCount: 5, + gateObjectCount: PROTOCOL_V3_POST_MIGRATION_CUTOVER_OBJECT_COUNT, + lastMigration: `${SCHEMA_MIGRATION_TAG}.sql`, + stagingClaimOwnerCount: 1, + stagingIndexCount: 4, + stagingTableCount: 2, + targetApplied: 1, + }, + rolledBack: { + authorityColumnCount: 1, + backupAuthorityWithoutRowidCount: 0, + gateObjectCount: PROTOCOL_V3_POST_MIGRATION_CUTOVER_OBJECT_COUNT, + lastMigration: `${MIGRATION_TAG}.sql`, + stagingClaimOwnerCount: 0, + stagingIndexCount: 4, + stagingTableCount: 2, + targetApplied: 0, + }, + verifyLatestCatalog: true, + }, + ], +] as const satisfies readonly (readonly [string, MigrationAtomicityFixture])[]; + +const SCHEMA_MIGRATION_STAGING_SEED_SQL = ` +INSERT INTO sandbox ( + agent_id, app_id, claim_expires_at, claim_owner, created_at, id, incarnation, + kind, network_constraints_hash, operation_kind, owner_account_id, status, + status_operation_id, subject_id, subject_kind, updated_at +) VALUES ( + '01J0000000000000000000000B', '01J0000000000000000000000C', 2000000000000, + 'live-owner', 1, '01J0000000000000000000000A', 2, 'pet', '${"0".repeat(64)}', + 'hibernate', '01J0000000000000000000000D', 'backing_up', + '01J0000000000000000000000E', '01J0000000000000000000000B', 'agent', 2 +); +INSERT INTO sandbox_backup_staging ( + actual_backup_id, created_at, dir, driver_generation, driver_instance_id, id, + operation_id, sandbox_id, sandbox_incarnation, session_run_id, ttl_seconds, + updated_at, updates_subject_backup, workspace_session_id +) VALUES + ('01J0000000000000000000000M', 10, '/workspace/live', NULL, NULL, + '01J0000000000000000000000G', '01J0000000000000000000000E', + '01J0000000000000000000000A', 2, NULL, 60, 11, 1, NULL), + ('01J0000000000000000000000N', 20, '/workspace/stale', NULL, NULL, + '01J0000000000000000000000H', '01J0000000000000000000000F', + '01J0000000000000000000000A', 2, NULL, 90, 21, 0, NULL), + ('01J0000000000000000000000P', 30, '/workspace/terminal', 3, + '01J0000000000000000000000K', '01J0000000000000000000000J', NULL, + '01J0000000000000000000000A', 2, '01J0000000000000000000000Q', + 120, 31, 0, '01J0000000000000000000000R'); +INSERT INTO sandbox_backup ( + created_at, dir, id, keep, operation_id, sandbox_id, sandbox_incarnation, + session_run_id, staging_id, status, ttl_seconds, updated_at, workspace_session_id +) VALUES ( + 40, '/workspace/ready', '01J0000000000000000000000V', 1, + '01J0000000000000000000000E', '01J0000000000000000000000A', 2, NULL, + '01J0000000000000000000000W', 'ready', 180, 41, NULL +); +INSERT INTO api_command ( + attempt_count, claim_expires_at, claim_owner, created_at, dedupe_key, + delivery_generation, id, kind, payload_json, status, updated_at +) VALUES ( + 4, 2000000000000, 'environment-owner', 42, 'environment-artifact-migration-seed', + 3, '01J0000000000000000000000X', 'environment_package_artifact_build', + '{"appId":"01J0000000000000000000000Y","inputDigest":"${"c".repeat(64)}"}', + 'running', 43 +); +INSERT INTO environment_package_artifact_backup_staging ( + actual_backup_id, app_id, attempt_count, claim_owner, command_id, created_at, + delivery_generation, dir, input_digest, paths_json, updated_at +) VALUES ( + '01J0000000000000000000000Z', '01J0000000000000000000000Y', 4, + 'environment-owner', '01J0000000000000000000000X', 42, 3, + '/workspace/.mosoo/environment-artifacts/${"c".repeat(64)}', '${"c".repeat(64)}', + '{"executable":["/workspace/.mosoo/environment-artifacts/${"c".repeat(64)}/bin"],"node":[],"python":[]}', + 43 +); +`; + +function executeLocalD1Json(root: string, state: string, sql: string) { + return parseD1JsonResults( + runWranglerSuccessfully( + root, + localD1Args(state, "execute", ["--command", sql, "--json", "--yes"]), + ), + ); +} + +function expectExactCutoverGate(root: string, state: string, expectedObjectCount: number): void { + expect(executeLocalD1Json(root, state, PROTOCOL_V3_CUTOVER_OBJECTS_SQL)).toEqual([ + [{ exact_object_count: expectedObjectCount, object_count: expectedObjectCount }], + ]); +} + +function expectMigrationState( + root: string, + state: string, + migrationTag: string, + expected: MigrationStateExpectation, +): void { + expect( + executeLocalD1Json( + root, + state, + `SELECT value FROM rollback_probe WHERE id = 1; + SELECT count(*) AS count FROM pragma_table_info('sandbox') WHERE name = 'incarnation'; + SELECT count(*) AS count FROM pragma_table_info('api_command') + WHERE name = 'delivery_generation'; + SELECT count(*) AS count FROM pragma_table_info('app_deployment') + WHERE name = 'active_script_name'; + SELECT count(*) AS count FROM sqlite_master + WHERE type = 'table' + AND name IN ('sandbox_backup_staging', 'environment_package_artifact_backup_staging'); + SELECT count(*) AS count FROM pragma_table_info('sandbox_backup_staging') + WHERE name = 'claim_owner'; + SELECT count(*) AS count FROM sqlite_master + WHERE type = 'index' + AND tbl_name = 'sandbox_backup_staging' + AND name IN ('sandbox_backup_staging_updated_idx', + 'sandbox_backup_staging_actual_idx', + 'sandbox_backup_staging_terminal_checkpoint_idx', + 'sandbox_backup_staging_operation_checkpoint_idx'); + SELECT count(*) AS count FROM pragma_table_list + WHERE name IN ('environment_package_artifact_backup', + 'environment_package_artifact_backup_staging', + 'sandbox_backup', + 'sandbox_backup_delete_intent', + 'sandbox_backup_staging') + AND wr = 1; + ${PROTOCOL_V3_CUTOVER_OBJECTS_SQL} + SELECT enabled, release_tree_oid FROM __protocol_v3_cutover WHERE id = 1; + SELECT count(*) AS count FROM d1_migrations WHERE name = '${migrationTag}.sql'; + SELECT name FROM d1_migrations ORDER BY id DESC LIMIT 1;`, + ), + ).toEqual([ + [{ value: 1 }], + [{ count: expected.authorityColumnCount }], + [{ count: expected.authorityColumnCount }], + [{ count: expected.authorityColumnCount }], + [{ count: expected.stagingTableCount }], + [{ count: expected.stagingClaimOwnerCount }], + [{ count: expected.stagingIndexCount }], + [{ count: expected.backupAuthorityWithoutRowidCount }], + [ + { + exact_object_count: expected.gateObjectCount, + object_count: expected.gateObjectCount, + }, + ], + [{ enabled: 1, release_tree_oid: RELEASE_TREE_OID }], + [{ count: expected.targetApplied }], + [{ name: expected.lastMigration }], + ]); +} + +function expectSchemaMigrationAuthorityRows(root: string, state: string, migrated: boolean): void { + const claimOwner = migrated ? "claim_owner" : "NULL AS claim_owner"; + expect( + executeLocalD1Json( + root, + state, + `SELECT actual_backup_id, ${claimOwner}, created_at, dir, driver_generation, + driver_instance_id, id, operation_id, sandbox_id, sandbox_incarnation, + session_run_id, ttl_seconds, updated_at, updates_subject_backup, + workspace_session_id + FROM sandbox_backup_staging ORDER BY id; + SELECT created_at, dir, id, keep, operation_id, sandbox_id, sandbox_incarnation, + session_run_id, staging_id, status, ttl_seconds, updated_at, workspace_session_id + FROM sandbox_backup ORDER BY id; + SELECT actual_backup_id, app_id, attempt_count, claim_owner, command_id, created_at, + delivery_generation, dir, input_digest, paths_json, updated_at + FROM environment_package_artifact_backup_staging ORDER BY command_id; + PRAGMA foreign_key_check`, + ), + ).toEqual([ + [ + { + actual_backup_id: "01J0000000000000000000000M", + claim_owner: migrated ? "live-owner" : null, + created_at: 10, + dir: "/workspace/live", + driver_generation: null, + driver_instance_id: null, + id: "01J0000000000000000000000G", + operation_id: "01J0000000000000000000000E", + sandbox_id: "01J0000000000000000000000A", + sandbox_incarnation: 2, + session_run_id: null, + ttl_seconds: 60, + updated_at: 11, + updates_subject_backup: 1, + workspace_session_id: null, + }, + { + actual_backup_id: "01J0000000000000000000000N", + claim_owner: migrated ? "__legacy_stale__" : null, + created_at: 20, + dir: "/workspace/stale", + driver_generation: null, + driver_instance_id: null, + id: "01J0000000000000000000000H", + operation_id: "01J0000000000000000000000F", + sandbox_id: "01J0000000000000000000000A", + sandbox_incarnation: 2, + session_run_id: null, + ttl_seconds: 90, + updated_at: 21, + updates_subject_backup: 0, + workspace_session_id: null, + }, + { + actual_backup_id: "01J0000000000000000000000P", + claim_owner: null, + created_at: 30, + dir: "/workspace/terminal", + driver_generation: 3, + driver_instance_id: "01J0000000000000000000000K", + id: "01J0000000000000000000000J", + operation_id: null, + sandbox_id: "01J0000000000000000000000A", + sandbox_incarnation: 2, + session_run_id: "01J0000000000000000000000Q", + ttl_seconds: 120, + updated_at: 31, + updates_subject_backup: 0, + workspace_session_id: "01J0000000000000000000000R", + }, + ], + [ + { + created_at: 40, + dir: "/workspace/ready", + id: "01J0000000000000000000000V", + keep: 1, + operation_id: "01J0000000000000000000000E", + sandbox_id: "01J0000000000000000000000A", + sandbox_incarnation: 2, + session_run_id: null, + staging_id: "01J0000000000000000000000W", + status: "ready", + ttl_seconds: 180, + updated_at: 41, + workspace_session_id: null, + }, + ], + [ + { + actual_backup_id: "01J0000000000000000000000Z", + app_id: "01J0000000000000000000000Y", + attempt_count: 4, + claim_owner: "environment-owner", + command_id: "01J0000000000000000000000X", + created_at: 42, + delivery_generation: 3, + dir: `/workspace/.mosoo/environment-artifacts/${"c".repeat(64)}`, + input_digest: "c".repeat(64), + paths_json: JSON.stringify({ + executable: [`/workspace/.mosoo/environment-artifacts/${"c".repeat(64)}/bin`], + node: [], + python: [], + }), + updated_at: 43, + }, + ], + [], + ]); +} + +function expectLatestSchemaCatalog(root: string, state: string): void { + runWranglerSuccessfully( + root, + localD1Args(state, "execute", [ + "--command", + "DROP TABLE rollback_probe; DROP TABLE __protocol_v3_migration_intent", + "--yes", + ]), + ); + const tableNames = DRIZZLE_MIGRATION_PROD_SCHEMA_CATALOG.tables.map(({ name }) => name); + const catalogOutput = runWranglerSuccessfully( + root, + localD1Args(state, "execute", [ + "--command", + createProdSchemaIntrospectionStatements(tableNames).join(";\n"), + "--json", + "--yes", + ]), + ); + expect( + findProdSchemaDifferences( + DRIZZLE_MIGRATION_PROD_SCHEMA_CATALOG, + createProdSchemaCatalogFromIntrospectionRows(parseD1JsonResults(catalogOutput), tableNames), + ), + ).toEqual([]); +} + +test.each(MIGRATION_ATOMICITY_CASES)( + "Wrangler rolls back and can reapply %s after a middle failure", + (migrationTag, fixture) => { + const root = mkdtempSync(join(tmpdir(), "mosoo-wrangler-migration-")); + try { + const migrations = join(root, "migrations"); + const state = join(root, "state"); + for (const directory of ["cache", "config", "migrations", "tmp"]) { + mkdirSync(join(root, directory)); + } + writeFileSync(join(root, "worker.ts"), "export default {};\n"); + writeFileSync( + join(root, "wrangler.toml"), + `name = "migration-atomicity" +main = "worker.ts" +compatibility_date = "2026-08-30" + +[[d1_databases]] +binding = "DB" +database_name = "migration-atomicity" +database_id = "00000000-0000-0000-0000-000000000000" +migrations_dir = "migrations" +`, + ); + + const target = getDrizzleMigration(migrationTag); + for (const migration of drizzleMigrations.slice(0, target.index)) { + copyFileSync( + join(migrationsSource, `${migration.tag}.sql`), + join(migrations, `${migration.tag}.sql`), + ); + } + runWranglerSuccessfully(root, localD1Args(state, "migrations", [])); + + const setup = join(root, "setup.sql"); + writeFileSync( + setup, + `${migrationTag === SCHEMA_MIGRATION_TAG ? SCHEMA_MIGRATION_STAGING_SEED_SQL : ""} +${fixture.gateSql} +CREATE TABLE rollback_probe (id integer PRIMARY KEY, value integer NOT NULL); +INSERT INTO rollback_probe (id, value) VALUES (1, 1); +`, + ); + runWranglerSuccessfully(root, localD1Args(state, "execute", ["--file", setup, "--yes"])); + expectExactCutoverGate(root, state, fixture.initialGateObjectCount); + + const source = readFileSync(join(migrationsSource, `${migrationTag}.sql`), "utf8"); + expect(source.split(fixture.failurePoint)).toHaveLength(2); + writeFileSync( + join(migrations, `${migrationTag}.sql`), + source.replace( + fixture.failurePoint, + `UPDATE rollback_probe SET value = 2 WHERE id = 1;--> statement-breakpoint +SELECT * FROM __intentional_mid_migration_failure;--> statement-breakpoint +${fixture.failurePoint}`, + ), + ); + + const failed = runWrangler(root, localD1Args(state, "migrations", [])); + expect(failed.status).not.toBe(0); + expect(failed.output).toContain("__intentional_mid_migration_failure"); + expectMigrationState(root, state, migrationTag, fixture.rolledBack); + if (migrationTag === SCHEMA_MIGRATION_TAG) { + expectSchemaMigrationAuthorityRows(root, state, false); + } + + writeFileSync(join(migrations, `${migrationTag}.sql`), source); + runWranglerSuccessfully(root, localD1Args(state, "migrations", [])); + expectMigrationState(root, state, migrationTag, fixture.recovered); + if (migrationTag === SCHEMA_MIGRATION_TAG) { + expectSchemaMigrationAuthorityRows(root, state, true); + } + if (fixture.verifyLatestCatalog) expectLatestSchemaCatalog(root, state); + } finally { + rmSync(root, { force: true, recursive: true }); + } + }, + 120_000, +); diff --git a/apps/api/wrangler.toml b/apps/api/wrangler.toml index b7ad6d57..f09d929c 100644 --- a/apps/api/wrangler.toml +++ b/apps/api/wrangler.toml @@ -31,6 +31,11 @@ POSTHOG_API_HOST = "https://us.i.posthog.com" [[send_email]] name = "AUTH_EMAIL" +[[workflows]] +binding = "APP_DEPLOYMENT_WORKFLOW" +name = "mosoo-app-deployment-dev" +class_name = "AppDeploymentWorkflow" + [secrets] required = [ "BETTER_AUTH_SECRET", @@ -137,6 +142,7 @@ BACKUP_BUCKET_NAME = "mosoo-stage-sandbox-state" SANDBOX_STATE_BUCKET_NAME = "mosoo-stage-sandbox-state" SANDBOX_FILE_BUCKET_LOCAL = "false" MOSOO_APP_DEPLOYMENT_DOMAIN = "apps-stage.mosoo.ai" +MOSOO_APP_DISPATCH_NAMESPACE = "mosoo-app-deployments-stage" MOSOO_ACCOUNT_CONCURRENT_SANDBOX_LIMIT = "5" MOSOO_DEPLOYMENT_MODE = "cloud" MOSOO_ENVIRONMENT = "production" @@ -159,6 +165,19 @@ required = [ "GOOGLE_OAUTH_CLIENT_SECRET", ] +[[env.stage.routes]] +zone_name = "mosoo.ai" +pattern = "*.apps-stage.mosoo.ai/*" + +[[env.stage.dispatch_namespaces]] +binding = "APP_DEPLOYMENT_DISPATCHER" +namespace = "mosoo-app-deployments-stage" + +[[env.stage.workflows]] +binding = "APP_DEPLOYMENT_WORKFLOW" +name = "mosoo-app-deployment-stage" +class_name = "AppDeploymentWorkflow" + [[env.stage.durable_objects.bindings]] name = "DriverConnection" class_name = "DriverConnection" @@ -254,6 +273,7 @@ BACKUP_BUCKET_NAME = "mosoo-sandbox-state" SANDBOX_STATE_BUCKET_NAME = "mosoo-sandbox-state" SANDBOX_FILE_BUCKET_LOCAL = "false" MOSOO_APP_DEPLOYMENT_DOMAIN = "apps.mosoo.ai" +MOSOO_APP_DISPATCH_NAMESPACE = "mosoo-app-deployments-prod" MOSOO_ACCOUNT_CONCURRENT_SANDBOX_LIMIT = "5" MOSOO_DEPLOYMENT_MODE = "cloud" MOSOO_ENVIRONMENT = "production" @@ -284,6 +304,19 @@ pattern = "cloud.mosoo.ai/api/*" zone_name = "mosoo.ai" pattern = "try.mosoo.ai/api/*" +[[env.prod.routes]] +zone_name = "mosoo.ai" +pattern = "*.apps.mosoo.ai/*" + +[[env.prod.dispatch_namespaces]] +binding = "APP_DEPLOYMENT_DISPATCHER" +namespace = "mosoo-app-deployments-prod" + +[[env.prod.workflows]] +binding = "APP_DEPLOYMENT_WORKFLOW" +name = "mosoo-app-deployment-prod" +class_name = "AppDeploymentWorkflow" + [[env.prod.durable_objects.bindings]] name = "DriverConnection" class_name = "DriverConnection" diff --git a/apps/driver b/apps/driver index 446f136e..5e661259 160000 --- a/apps/driver +++ b/apps/driver @@ -1 +1 @@ -Subproject commit 446f136ef1ba752d6f1b142e5bcee5e48273b965 +Subproject commit 5e661259e853663e561fed167aa495e792450045 diff --git a/apps/web/package.json b/apps/web/package.json index e0bf3cce..8ee407ae 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -6,15 +6,15 @@ "dev": "vp dev", "build": "vp build", "preview": "vp preview", - "deploy": "vp run build && vp exec wrangler deploy --env prod", + "deploy": "vp run build && vp exec wrangler deploy --env prod --experimental-provision=false", "lint": "vp lint .", "tc": "vp exec tsc --noEmit", - "test": "vp exec bun test tests" + "test": "vp exec bun test tests --isolate" }, "dependencies": { "@assistant-ui/react": "0.15.14", "@base-ui/react": "^1.7.0", - "@cloudflare/sandbox": "0.12.6", + "@cloudflare/sandbox": "0.12.9", "@hugeicons/core-free-icons": "^4.2.3", "@hugeicons/react": "^1.1.9", "@lobehub/icons-static-svg": "^1.90.0", @@ -30,11 +30,9 @@ "@xterm/addon-fit": "^0.11.0", "@xterm/xterm": "^6.0.0", "better-auth": "^1.6.28", - "boring-avatars": "^2.0.4", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "lucide-react": "^1.31.0", - "motion": "^12.40.0", "react": "^19.2.8", "react-dom": "^19.2.8", "react-markdown": "^10.1.0", @@ -55,7 +53,7 @@ "jsdom": "^29.1.1", "typescript": "^6.0.3", "vite": "^8.2.1", - "vite-plus": "^0.1.24", - "wrangler": "^4.123.0" + "vite-plus": "^0.3.0", + "wrangler": "^4.127.1" } } diff --git a/apps/web/src/domains/runtime/session-stream/session-stream-actions.ts b/apps/web/src/domains/runtime/session-stream/session-stream-actions.ts index 0bf3fa3a..36ad90ca 100644 --- a/apps/web/src/domains/runtime/session-stream/session-stream-actions.ts +++ b/apps/web/src/domains/runtime/session-stream/session-stream-actions.ts @@ -63,22 +63,23 @@ export function useSessionStreamActions(input: UseSessionStreamActionsInput): { streaming: boolean; syncSession: () => Promise; } { + const { activeSessionIdRef, sendViewerEvent } = input; const syncSession = useCallback(async (): Promise => { - const activeSessionId = input.activeSessionIdRef.current; + const activeSessionId = activeSessionIdRef.current; if (!isTruthy(activeSessionId)) { return; } const options: SendViewerEventOptions = { maxAttempts: 2 }; - await input.sendViewerEvent( + await sendViewerEvent( activeSessionId, createViewerCustomEvent("mosoo.session.sync.request", { reason: "manual", }), options, ); - }, [input.activeSessionIdRef, input.sendViewerEvent]); + }, [activeSessionIdRef, sendViewerEvent]); const sendUserMessage = useCallback( async (message: { attachmentIds?: string[]; diff --git a/apps/web/src/domains/runtime/session-stream/session-stream-socket.ts b/apps/web/src/domains/runtime/session-stream/session-stream-socket.ts index e47434eb..6b0ac112 100644 --- a/apps/web/src/domains/runtime/session-stream/session-stream-socket.ts +++ b/apps/web/src/domains/runtime/session-stream/session-stream-socket.ts @@ -11,7 +11,7 @@ import type { MosooViewerCustomEvent, } from "@mosoo/ag-ui-session"; import { createPromiseDeferred, ignorePromiseRejection } from "@mosoo/effects"; -import { useCallback, useEffect, useRef, useState } from "react"; +import { useCallback, useEffect, useLayoutEffect, useRef, useState } from "react"; import type { MutableRefObject } from "react"; import { isTruthy } from "../../../shared/lib/truthiness"; @@ -78,16 +78,22 @@ export function useSessionStreamSocket( ); const activeSessionIdRef = useRef(sessionId); const liveStateRef = useRef(snapshot.liveState); + const reconnectTimeoutRef = useRef | null>(null); const renderSchedulerRef = useRef(null); const socketRef = useRef(null); - activeSessionIdRef.current = sessionId; + useLayoutEffect(() => { + activeSessionIdRef.current = sessionId; + + return () => { + activeSessionIdRef.current = null; + }; + }, [sessionId]); let scopedSnapshot = snapshot; if (snapshot.sessionId !== sessionId) { scopedSnapshot = createSessionStreamSnapshot(sessionId); - liveStateRef.current = scopedSnapshot.liveState; setSnapshot(scopedSnapshot); } @@ -115,19 +121,21 @@ export function useSessionStreamSocket( [], ); - const queueSocketEvents = useCallback( - (targetSessionId: string, events: AgUiSessionEvent[]) => { - if (events.length === 0) { - return; - } + const queueSocketEvents = useCallback((targetSessionId: string, events: AgUiSessionEvent[]) => { + if (events.length === 0) { + return; + } - renderSchedulerRef.current ??= new SessionStreamRenderScheduler(applyScheduledEvents); - renderSchedulerRef.current.enqueueMany(targetSessionId, events); - }, - [applyScheduledEvents], - ); + renderSchedulerRef.current ??= new SessionStreamRenderScheduler(applyScheduledEvents); + renderSchedulerRef.current.enqueueMany(targetSessionId, events); + }, []); const closeSocket = useCallback((reason: string) => { + if (reconnectTimeoutRef.current !== null) { + globalThis.clearTimeout(reconnectTimeoutRef.current); + reconnectTimeoutRef.current = null; + } + const { current } = socketRef; if (!current) { @@ -153,7 +161,7 @@ export function useSessionStreamSocket( }, []); const connectToSession = useCallback( - async (targetSessionId: string): Promise => { + async function connectToSession(targetSessionId: string): Promise { if (!isTruthy(appId)) { throw new Error("App id is required to open a session stream."); } @@ -232,20 +240,24 @@ export function useSessionStreamSocket( rescheduleStartedAt: new Date().toISOString(), }), ]); - globalThis.setTimeout(() => { + reconnectTimeoutRef.current = globalThis.setTimeout(() => { + reconnectTimeoutRef.current = null; + if (activeSessionIdRef.current !== targetSessionId) { return; } - void connectToSession(targetSessionId).then((nextSocket) => { - nextSocket.send( - JSON.stringify( - createViewerCustomEvent("mosoo.session.sync.request", { - reason: "reconnect", - }), - ), - ); - }); + void connectToSession(targetSessionId) + .then((nextSocket) => { + nextSocket.send( + JSON.stringify( + createViewerCustomEvent("mosoo.session.sync.request", { + reason: "reconnect", + }), + ), + ); + }) + .catch(ignorePromiseRejection); }, 350); } }); @@ -313,6 +325,7 @@ export function useSessionStreamSocket( void connectToSession(sessionId).catch(ignorePromiseRejection); return () => { + renderSchedulerRef.current?.clear(); closeSocket("session.effect.cleanup"); }; }, [closeSocket, connectToSession, appId, sessionId]); diff --git a/apps/web/src/features/help/help-menu.tsx b/apps/web/src/features/help/help-menu.tsx index 283a723e..e0f7aabd 100644 --- a/apps/web/src/features/help/help-menu.tsx +++ b/apps/web/src/features/help/help-menu.tsx @@ -1,5 +1,5 @@ import { HelpCircle } from "lucide-react"; -import { lazy, Suspense, useEffect, useState } from "react"; +import { lazy, Suspense, useCallback, useEffect, useState } from "react"; import { useTranslation } from "@/shared/i18n"; import { cn } from "@/shared/lib/class-names"; @@ -37,10 +37,10 @@ export function HelpMenu({ // fetched on a normal page load. const [hasOpened, setHasOpened] = useState(false); - function openHelp(): void { + const openHelp = useCallback((): void => { setHasOpened(true); setOpen(true); - } + }, []); function handleOpenChange(nextOpen: boolean): void { if (nextOpen) { @@ -72,7 +72,7 @@ export function HelpMenu({ return () => { globalThis.removeEventListener("keydown", handleKeyDown); }; - }, [shortcutEnabled]); + }, [openHelp, shortcutEnabled]); const button = (
- {showSettings ? ( + {settingsOpen ? ( { + setShowSettings(open); + if (!open && settingsParam === "1") { + setSearchParams( + (current) => { + const nextParams = new URLSearchParams(current); + nextParams.delete("settings"); + return nextParams; + }, + { replace: true }, + ); + } + }} canManageAccess={canManageAgentAccess} /> diff --git a/apps/web/src/routes/agent/components/editor/form-view.tsx b/apps/web/src/routes/agent/components/editor/form-view.tsx index e7875248..6c8add7c 100644 --- a/apps/web/src/routes/agent/components/editor/form-view.tsx +++ b/apps/web/src/routes/agent/components/editor/form-view.tsx @@ -1,4 +1,4 @@ -import { useRef } from "react"; +import { useEffect, useRef } from "react"; import type { ReactElement } from "react"; import { cn } from "@/shared/lib/class-names"; @@ -66,23 +66,18 @@ function useSectionNavigation(input: { environment: null, integrations: null, }); - const scrolledFocusRef = useRef(null); - if (input.focusSection === null) { - scrolledFocusRef.current = null; - } + useEffect(() => { + if (input.focusSection !== null) { + sectionRefs.current[input.focusSection]?.scrollIntoView({ + behavior: "smooth", + block: "start", + }); + } + }, [input.focusSection]); function setSectionRef(sectionId: AgentFormSectionId, node: HTMLDivElement | null): void { sectionRefs.current[sectionId] = node; - - if ( - node !== null && - input.focusSection === sectionId && - scrolledFocusRef.current !== sectionId - ) { - scrolledFocusRef.current = sectionId; - node.scrollIntoView({ behavior: "smooth", block: "start" }); - } } const activeRings = input.highlightedSections ?? new Set(); diff --git a/apps/web/src/routes/agent/components/editor/use-auto-save.ts b/apps/web/src/routes/agent/components/editor/use-auto-save.ts index 9dafa321..19bb2036 100644 --- a/apps/web/src/routes/agent/components/editor/use-auto-save.ts +++ b/apps/web/src/routes/agent/components/editor/use-auto-save.ts @@ -1,5 +1,5 @@ import type { AgentConfigChangePlan } from "@mosoo/contracts/agent-config-change-plan"; -import { useEffect, useRef } from "react"; +import { useEffect, useEffectEvent, useRef } from "react"; import type { AgentEditorModel } from "./use-model"; @@ -18,8 +18,7 @@ export function isAutoSaveEligible(changePlan: AgentConfigChangePlan): boolean { export function useAgentEditorAutoSave(model: AgentEditorModel): void { const { snapshotHash, dirty, saving, changePlan, readOnly, save } = model; const eligible = isAutoSaveEligible(changePlan); - const saveRef = useRef(save); - saveRef.current = save; + const saveLatestDraft = useEffectEvent(save); // Tracks the snapshot we last attempted to flush. A retry of the exact same // draft after a failure would just refire the same validation/network error, // so we wait for the user to type something else before trying again. @@ -36,7 +35,7 @@ export function useAgentEditorAutoSave(model: AgentEditorModel): void { const timer = globalThis.setTimeout(() => { lastAttemptedHashRef.current = snapshotHash; - void saveRef.current(); + void saveLatestDraft(); }, AUTO_SAVE_DEBOUNCE_MS); return () => { diff --git a/apps/web/src/routes/agent/components/terminal-mode.tsx b/apps/web/src/routes/agent/components/terminal-mode.tsx index 357f7fa5..cd29e6e0 100644 --- a/apps/web/src/routes/agent/components/terminal-mode.tsx +++ b/apps/web/src/routes/agent/components/terminal-mode.tsx @@ -4,7 +4,7 @@ import { getAgentKindRuntimePolicy } from "@mosoo/contracts/agent"; import { FitAddon } from "@xterm/addon-fit"; import { Terminal as XTermTerminal } from "@xterm/xterm"; import { Circle, RefreshCw, Terminal as TerminalIcon, TriangleAlert } from "lucide-react"; -import { useCallback, useEffect, useRef, useSyncExternalStore } from "react"; +import { useEffect, useRef, useState } from "react"; import type { ReactElement, RefObject } from "react"; import "@xterm/xterm/css/xterm.css"; @@ -27,44 +27,6 @@ interface OwnerDebugTerminalConnectionSnapshot { connectionState: ConnectionState; } -interface OwnerDebugTerminalConnectionStore { - getSnapshot: () => OwnerDebugTerminalConnectionSnapshot; - setSnapshot: (snapshot: OwnerDebugTerminalConnectionSnapshot) => void; - subscribe: (listener: () => void) => () => void; -} - -function createOwnerDebugTerminalConnectionStore(): OwnerDebugTerminalConnectionStore { - let snapshot: OwnerDebugTerminalConnectionSnapshot = { - connectionError: null, - connectionState: "connecting", - }; - const listeners = new Set<() => void>(); - - return { - getSnapshot: () => snapshot, - setSnapshot: (nextSnapshot) => { - if ( - nextSnapshot.connectionError === snapshot.connectionError && - nextSnapshot.connectionState === snapshot.connectionState - ) { - return; - } - - snapshot = nextSnapshot; - for (const listener of listeners) { - listener(); - } - }, - subscribe: (listener) => { - listeners.add(listener); - - return () => { - listeners.delete(listener); - }; - }, - }; -} - function buildOwnerDebugTerminalWebSocketUrl(input: { agentId: string; origin: string }): string { return new URL( `/api/agent/${encodeURIComponent(input.agentId)}/owner-debug-terminal/ws`, @@ -73,26 +35,20 @@ function buildOwnerDebugTerminalWebSocketUrl(input: { agentId: string; origin: s } function useOwnerDebugTerminalController(agentId: string): OwnerDebugTerminalController { - const agentIdRef = useRef(agentId); const containerRef = useRef(null); const hasConnectedOnceRef = useRef(false); const preserveReconnectBufferRef = useRef(false); const sandboxAddonRef = useRef(null); - const connectionStoreRef = useRef(null); - connectionStoreRef.current ??= createOwnerDebugTerminalConnectionStore(); - const connectionStore = connectionStoreRef.current; - const { connectionError, connectionState } = useSyncExternalStore( - connectionStore.subscribe, - connectionStore.getSnapshot, - connectionStore.getSnapshot, - ); + const [{ connectionError, connectionState }, setConnectionSnapshot] = + useState({ + connectionError: null, + connectionState: "connecting", + }); useEffect(() => { const container = containerRef.current; if (container === null) { - return () => { - /* Empty */ - }; + return; } let frameId: number | null = null; @@ -130,7 +86,7 @@ function useOwnerDebugTerminalController(agentId: string): OwnerDebugTerminalCon hasConnectedOnceRef.current = true; preserveReconnectBufferRef.current = false; } - connectionStore.setSnapshot({ + setConnectionSnapshot({ connectionError: error?.message ?? null, connectionState: state === "disconnected" && !error ? "connecting" : state, }); @@ -152,10 +108,10 @@ function useOwnerDebugTerminalController(agentId: string): OwnerDebugTerminalCon try { fitAddon.fit(); } catch (error) { - connectionStore.setSnapshot({ - ...connectionStore.getSnapshot(), + setConnectionSnapshot((current) => ({ + ...current, connectionError: error instanceof Error ? error.message : "Terminal resize failed.", - }); + })); } } @@ -175,7 +131,7 @@ function useOwnerDebugTerminalController(agentId: string): OwnerDebugTerminalCon sandboxAddonRef.current = sandboxAddon; scheduleFit(); terminal.focus(); - sandboxAddon.connect({ sandboxId: agentIdRef.current }); + sandboxAddon.connect({ sandboxId: agentId }); const resizeObserver = globalThis.ResizeObserver === undefined @@ -201,22 +157,22 @@ function useOwnerDebugTerminalController(agentId: string): OwnerDebugTerminalCon removeReconnectClearGuard(); terminal.dispose(); }; - }, [connectionStore]); + }, [agentId]); - const reconnect = useCallback(() => { + function reconnect(): void { const sandboxAddon = sandboxAddonRef.current; if (sandboxAddon === null) { return; } - connectionStore.setSnapshot({ + setConnectionSnapshot({ connectionError: null, connectionState: "connecting", }); preserveReconnectBufferRef.current = hasConnectedOnceRef.current; sandboxAddon.disconnect(); - sandboxAddon.connect({ sandboxId: agentIdRef.current }); - }, [connectionStore]); + sandboxAddon.connect({ sandboxId: agentId }); + } return { connectionError, diff --git a/apps/web/src/routes/agent/components/use-agent-session-panel-model.ts b/apps/web/src/routes/agent/components/use-agent-session-panel-model.ts index 6736493e..3c6ba882 100644 --- a/apps/web/src/routes/agent/components/use-agent-session-panel-model.ts +++ b/apps/web/src/routes/agent/components/use-agent-session-panel-model.ts @@ -1,7 +1,7 @@ import type { SessionType } from "@mosoo/contracts/session"; import { ignorePromiseRejection } from "@mosoo/effects"; import { useQuery } from "@tanstack/react-query"; -import { useEffect, useMemo, useRef, useState } from "react"; +import { useEffect, useEffectEvent, useMemo, useRef, useState } from "react"; import type { KeyboardEvent } from "react"; import { useSessionStream } from "@/domains/runtime/use-session-stream"; @@ -150,53 +150,50 @@ export function useAgentSessionPanelModel( ); const layout = useSessionChatLayoutState(stream.messages, permissionScrollSignal); - // Fresh messages for the sweep interval without keying the effect on - // stream.messages — that identity changes every animation frame during - // streaming and would tear the interval down per frame. - const streamMessagesRef = useRef(stream.messages); - streamMessagesRef.current = stream.messages; - // Reconcile the optimistic overlay against server truth at render time, so // the frame that first contains the echoed message never also paints the // pending bubble (a post-commit effect alone leaves a duplicate frame). The // session filter runs here too so a pending entry bound to another session - // can never ghost into the newly selected session's thread mid-switch. - const reconciledPendingSends = useMemo( - () => + // can never ghost into the newly selected session's thread mid-switch. TTL + // expiry belongs to the external timer below; the send timestamp keeps this + // render-time pass pure while still reconciling echoes immediately. + const reconciledPendingSends = useMemo(() => { + const sessionPendingSends = prunePendingSendsForSession(pendingSends, activeSessionId); + + return prunePendingSends( + sessionPendingSends, + stream.messages, + sessionPendingSends.at(-1)?.createdAtMs ?? 0, + ); + }, [activeSessionId, pendingSends, stream.messages]); + + const sweepPendingSends = useEffectEvent(() => { + setPendingSends((current) => prunePendingSends( - prunePendingSendsForSession(pendingSends, activeSessionId), + prunePendingSendsForSession(current, activeSessionId), stream.messages, Date.now(), ), - [activeSessionId, pendingSends, stream.messages], - ); - - useEffect(() => { - setPendingSends((current) => prunePendingSendsForSession(current, activeSessionId)); - }, [activeSessionId]); + ); + }); // State GC + TTL sweep: gates and visuals read the render-time reconciled - // value, so state only needs to catch up eventually — eagerly when the - // overlay changes, then every sweep tick so a stuck entry expires even when - // no further events arrive and the composer can never stay blocked forever. + // value, so state only needs to catch up on each sweep tick. This expires a + // stuck entry even when no further events arrive, so the composer cannot + // stay blocked forever. useEffect(() => { if (pendingSends.length === 0) { return; } - const sweep = (): void => { - setPendingSends((current) => - prunePendingSends(current, streamMessagesRef.current, Date.now()), - ); - }; - - sweep(); - const interval = globalThis.setInterval(sweep, PENDING_SEND_SWEEP_INTERVAL_MS); + const interval = globalThis.setInterval(() => { + sweepPendingSends(); + }, PENDING_SEND_SWEEP_INTERVAL_MS); return () => { globalThis.clearInterval(interval); }; - }, [pendingSends]); + }, [pendingSends.length]); async function refreshSessions(): Promise { if (input.appId === null) { @@ -496,10 +493,6 @@ export function useAgentSessionPanelModel( }); setInputValue(""); - if (layout.inputRef.current) { - layout.inputRef.current.style.height = "auto"; - } - if (shouldAutoTitle) { const title = createSessionAutoTitle(typedText); const titledSessionId = sessionId; diff --git a/apps/web/src/routes/app-overview/deploy/deploy-console-data.ts b/apps/web/src/routes/app-overview/deploy/deploy-console-data.ts index 18f88191..e966de15 100644 --- a/apps/web/src/routes/app-overview/deploy/deploy-console-data.ts +++ b/apps/web/src/routes/app-overview/deploy/deploy-console-data.ts @@ -35,7 +35,7 @@ export interface BoundAgentVM { /** Short console labels for the detected deploy target. */ export const DEPLOY_TARGET_LABELS: Record = { - cloudflare_pages: "static", + cloudflare_static_assets: "static", cloudflare_worker: "worker", }; diff --git a/apps/web/src/routes/app-overview/deploy/local-preview-url.ts b/apps/web/src/routes/app-overview/deploy/local-preview-url.ts index 1832453a..7263d51b 100644 --- a/apps/web/src/routes/app-overview/deploy/local-preview-url.ts +++ b/apps/web/src/routes/app-overview/deploy/local-preview-url.ts @@ -82,17 +82,23 @@ async function checkLocalDeploymentPreview( export function useLocalDeploymentPreview(): LocalDeploymentPreviewState { const url = getLocalDeploymentPreviewUrl(); const [refreshNonce, setRefreshNonce] = useState(0); - const [status, setStatus] = useState( - url === null ? "unavailable" : "checking", - ); + const [result, setResult] = useState<{ + refreshNonce: number; + status: LocalDeploymentPreviewStatus; + url: string; + } | null>(null); + const status = + url === null + ? "unavailable" + : result?.url === url && result.refreshNonce === refreshNonce + ? result.status + : "checking"; const refresh = useCallback(() => { - setStatus(url === null ? "unavailable" : "checking"); setRefreshNonce((current) => current + 1); - }, [url]); + }, []); useEffect(() => { if (url === null) { - setStatus("unavailable"); return; } @@ -101,12 +107,9 @@ export function useLocalDeploymentPreview(): LocalDeploymentPreviewState { const previewUrl = url; async function refreshStatus(): Promise { - setStatus((current) => - current === "online" || current === "offline" ? current : "checking", - ); const nextStatus = await checkLocalDeploymentPreview(previewUrl); if (active) { - setStatus(nextStatus); + setResult({ refreshNonce, status: nextStatus, url: previewUrl }); } } diff --git a/apps/web/src/routes/cost/cost-models-panel.tsx b/apps/web/src/routes/cost/cost-models-panel.tsx index 1820acde..a21a8fa6 100644 --- a/apps/web/src/routes/cost/cost-models-panel.tsx +++ b/apps/web/src/routes/cost/cost-models-panel.tsx @@ -148,13 +148,14 @@ function ModelDonut({ models, totalCost }: { models: CostModelRow[]; totalCost: } let cursor = 0; - const slices = models.map((model, index) => { + const slices: string[] = []; + + for (const [index, model] of models.entries()) { const start = cursor; const span = (model.totalCostUsd / totalCost) * 100; cursor += span; - - return `${vendorColor(index)} ${start}% ${cursor}%`; - }); + slices.push(`${vendorColor(index)} ${start}% ${cursor}%`); + } return (
diff --git a/apps/web/src/routes/integrations/skills/skill-detail-dialog.tsx b/apps/web/src/routes/integrations/skills/skill-detail-dialog.tsx index 4c5b4bc7..54e8ab1b 100644 --- a/apps/web/src/routes/integrations/skills/skill-detail-dialog.tsx +++ b/apps/web/src/routes/integrations/skills/skill-detail-dialog.tsx @@ -81,14 +81,15 @@ export function SkillDetailDialog({ onOpenChange, registry, skill }: Props) { const { t } = useTranslation(); const [state, dispatch] = useReducer(skillDetailDialogReducer, SKILL_DETAIL_DIALOG_INITIAL_STATE); const { actionError, content, contentError, contentLoading, detail, forking, showDelete } = state; + const { getSkillDetail, getSkillSource } = registry; useEffect(() => { const abortController = new AbortController(); void (async () => { try { const [nextDetail, text] = await Promise.all([ - registry.getSkillDetail(skill.id), - registry.getSkillSource(skill.id), + getSkillDetail(skill.id), + getSkillSource(skill.id), ]); if (abortController.signal.aborted) { return; @@ -106,7 +107,7 @@ export function SkillDetailDialog({ onOpenChange, registry, skill }: Props) { return () => { abortController.abort(); }; - }, [registry.getSkillDetail, registry.getSkillSource, skill.id]); + }, [getSkillDetail, getSkillSource, skill.id]); const body = useMemo(() => stripSkillFrontmatter(content), [content]); diff --git a/apps/web/src/routes/integrations/skills/skills-sh-catalog.tsx b/apps/web/src/routes/integrations/skills/skills-sh-catalog.tsx index 4cf8c248..4566983a 100644 --- a/apps/web/src/routes/integrations/skills/skills-sh-catalog.tsx +++ b/apps/web/src/routes/integrations/skills/skills-sh-catalog.tsx @@ -10,7 +10,7 @@ import { TrendingUp, } from "lucide-react"; import type { LucideIcon } from "lucide-react"; -import { useEffect, useMemo, useReducer, useState } from "react"; +import { useMemo, useReducer, useState } from "react"; import { useSkillsShCatalogQuery } from "@/domains/skill/query/skill-queries"; import { useTranslation } from "@/shared/i18n"; @@ -97,8 +97,10 @@ export function SkillsShCatalog({ const { t } = useTranslation(); const [state, dispatch] = useReducer(skillsShCatalogReducer, SKILLS_SH_CATALOG_INITIAL_STATE); const [confirmingSkill, setConfirmingSkill] = useState(null); - const { availableOnly, error, installingId, page, view } = state; const trimmedSearch = search.trim(); + const [previousSearch, setPreviousSearch] = useState(trimmedSearch); + const { availableOnly, error, installingId, page: storedPage, view } = state; + const page = previousSearch === trimmedSearch ? storedPage : 0; const catalogQuery = useSkillsShCatalogQuery({ availableOnly, enabled: isTruthy(registry.appId), @@ -108,15 +110,16 @@ export function SkillsShCatalog({ view, }); - useEffect(() => { - dispatch({ type: "resetPage" }); - }, [trimmedSearch]); - const installedNames = useMemo( () => new Set(registry.personal.map((skill) => skill.name.trim().toLowerCase())), [registry.personal], ); + if (previousSearch !== trimmedSearch) { + setPreviousSearch(trimmedSearch); + dispatch({ type: "resetPage" }); + } + async function handleInstall(skill: SkillsShCatalogSkill) { if (installingId !== null) { return; diff --git a/apps/web/src/routes/org/org-settings.route.tsx b/apps/web/src/routes/org/org-settings.route.tsx index 993a7d2a..e9a9af4b 100644 --- a/apps/web/src/routes/org/org-settings.route.tsx +++ b/apps/web/src/routes/org/org-settings.route.tsx @@ -1,5 +1,5 @@ import { Check, Loader2 } from "lucide-react"; -import { useEffect, useState } from "react"; +import { useState } from "react"; import { useAppSession } from "@/app/session-provider"; import { renameOrganization } from "@/domains/organization/api/organization-client"; @@ -13,14 +13,17 @@ export function OrgSettingsPage() { const { t } = useTranslation(); const { activeOrganization, organizationsLoading, refreshOrganizations } = useAppSession(); - const [name, setName] = useState(activeOrganization?.name ?? ""); + const organizationName = activeOrganization?.name ?? ""; + const [name, setName] = useState(organizationName); + const [previousOrganizationName, setPreviousOrganizationName] = useState(organizationName); const [saving, setSaving] = useState(false); const [saved, setSaved] = useState(false); const [error, setError] = useState(null); - useEffect(() => { - setName(activeOrganization?.name ?? ""); - }, [activeOrganization?.name]); + if (previousOrganizationName !== organizationName) { + setPreviousOrganizationName(organizationName); + setName(organizationName); + } const trimmedName = name.trim(); const dirty = activeOrganization !== null && trimmedName !== activeOrganization.name; diff --git a/apps/web/src/routes/threads/model/read-sync.ts b/apps/web/src/routes/threads/model/read-sync.ts index d124052d..18942699 100644 --- a/apps/web/src/routes/threads/model/read-sync.ts +++ b/apps/web/src/routes/threads/model/read-sync.ts @@ -12,11 +12,11 @@ export function useSelectedThreadReadSync({ selectedThread: ThreadListItem | null; }): void { const pendingReadMarkerRef = useRef(null); - const completedReadMarkersRef = useRef | null>(null); - completedReadMarkersRef.current ??= new Set(); - const completedReadMarkers = completedReadMarkersRef.current; + const completedReadMarkersRef = useRef(new Set()); useEffect(() => { + const completedReadMarkers = completedReadMarkersRef.current; + if (selectedThread === null || selectedThread.read) { return; } @@ -47,5 +47,5 @@ export function useSelectedThreadReadSync({ } void markSelectedThreadRead(); - }, [completedReadMarkers, markRead, onError, selectedThread]); + }, [markRead, onError, selectedThread]); } diff --git a/apps/web/src/shared/ui/session-events/drawer-core.tsx b/apps/web/src/shared/ui/session-events/drawer-core.tsx index cff72f2e..cfd368c4 100644 --- a/apps/web/src/shared/ui/session-events/drawer-core.tsx +++ b/apps/web/src/shared/ui/session-events/drawer-core.tsx @@ -92,9 +92,8 @@ export function SessionEventDrawerCore>(() => new Set()); const [expansionTouched, setExpansionTouched] = useState(false); const [scrollTop, setScrollTop] = useState(0); - const eventRefs = useRef | null>(null); - eventRefs.current ??= new Map(); - const eventRefMap = eventRefs.current; + const [viewportHeight, setViewportHeight] = useState(420); + const eventRefs = useRef(new Map()); const listRef = useRef(null); const initialScrollCompletedRef = useRef(false); const selectedId = selectedEventId ?? focusEventId ?? events[0]?.id ?? null; @@ -119,7 +118,6 @@ export function SessionEventDrawerCore { listRef.current?.scrollTo({ top: offset }); - eventRefMap.get(selectedId)?.scrollIntoView({ block: "nearest" }); + eventRefs.current.get(selectedId)?.scrollIntoView({ block: "nearest" }); }); } } @@ -161,7 +159,7 @@ export function SessionEventDrawerCore { listRef.current = node; if (node !== null) { + setViewportHeight(node.clientHeight); scrollInitialSelection(); } }} onScroll={(event) => { setScrollTop(event.currentTarget.scrollTop); + setViewportHeight(event.currentTarget.clientHeight); }} className="min-h-0 flex-1 overflow-y-auto pr-1" > @@ -235,12 +235,12 @@ export function SessionEventDrawerCore { if (node) { - eventRefMap.set(event.id, node); + eventRefs.current.set(event.id, node); if (event.id === selectedId) { scrollInitialSelection(); } } else { - eventRefMap.delete(event.id); + eventRefs.current.delete(event.id); } }} > diff --git a/apps/web/tests/deployment-status.test.ts b/apps/web/tests/deployment-status.test.ts index 58aa4238..70cccbe9 100644 --- a/apps/web/tests/deployment-status.test.ts +++ b/apps/web/tests/deployment-status.test.ts @@ -4,6 +4,7 @@ import { readFileSync } from "node:fs"; import type { AppDeployment, AppDeploymentRun, AppDeploymentRunStatus } from "@mosoo/contracts/app"; import type { AppDeploymentOverview } from "../src/domains/app/api/app-deployment-client"; +import { DEPLOY_TARGET_LABELS } from "../src/routes/app-overview/deploy/deploy-console-data"; import { toDeployConsoleState } from "../src/routes/app-overview/deploy/deploy-console-mapping"; import { toDeploymentRunOutcome, @@ -33,6 +34,13 @@ function readSource(path: string): string { } describe("deployment status presentation", () => { + test("labels the two Workers for Platforms targets", () => { + expect(DEPLOY_TARGET_LABELS).toEqual({ + cloudflare_static_assets: "static", + cloudflare_worker: "worker", + }); + }); + test("collapses executor phases into three user-facing outcomes", () => { const cases: Array<[AppDeploymentRunStatus, ReturnType]> = [ ["queued", "deploying"], diff --git a/apps/web/tests/session-stream-socket.test.tsx b/apps/web/tests/session-stream-socket.test.tsx new file mode 100644 index 00000000..c279f6e2 --- /dev/null +++ b/apps/web/tests/session-stream-socket.test.tsx @@ -0,0 +1,128 @@ +import { afterEach, beforeEach, describe, expect, test } from "bun:test"; + +import { JSDOM } from "jsdom"; +import { act } from "react"; +import type { Root } from "react-dom/client"; + +import { useSessionStreamSocket } from "../src/domains/runtime/session-stream/session-stream-socket"; + +class TestWebSocket { + public static readonly CLOSED = 3; + public static readonly CLOSING = 2; + public static readonly CONNECTING = 0; + public static readonly OPEN = 1; + public static readonly instances: TestWebSocket[] = []; + + readonly #listeners = new Map void>>(); + public readyState = TestWebSocket.CONNECTING; + + public constructor(public readonly url: string) { + TestWebSocket.instances.push(this); + } + + public addEventListener(type: string, listener: () => void): void { + const listeners = this.#listeners.get(type) ?? new Set(); + listeners.add(listener); + this.#listeners.set(type, listeners); + } + + public close(): void { + this.readyState = TestWebSocket.CLOSED; + for (const listener of this.#listeners.get("close") ?? []) { + listener(); + } + } + + public disconnect(): void { + this.close(); + } + + public send(): void {} +} + +let dom: JSDOM; +let root: Root | null = null; +let pendingFrames: Set; + +function Harness(): null { + useSessionStreamSocket("app-1", "session-1"); + return null; +} + +beforeEach(() => { + dom = new JSDOM("
", { + url: "http://localhost/session-1", + }); + pendingFrames = new Set(); + TestWebSocket.instances.length = 0; + + Object.defineProperties(globalThis, { + IS_REACT_ACT_ENVIRONMENT: { configurable: true, value: true }, + WebSocket: { configurable: true, value: TestWebSocket }, + document: { configurable: true, value: dom.window.document }, + location: { configurable: true, value: dom.window.location }, + window: { configurable: true, value: dom.window }, + }); + globalThis.HTMLElement = dom.window.HTMLElement; + globalThis.Node = dom.window.Node; + globalThis.requestAnimationFrame = () => { + const handle = pendingFrames.size + 1; + pendingFrames.add(handle); + return handle; + }; + globalThis.cancelAnimationFrame = (handle) => { + pendingFrames.delete(handle); + }; +}); + +afterEach(async () => { + if (root !== null) { + await act(async () => { + root?.unmount(); + }); + } + + root = null; + dom.window.close(); + delete globalThis.window; + delete globalThis.document; + delete globalThis.location; + delete globalThis.HTMLElement; + delete globalThis.Node; + delete globalThis.requestAnimationFrame; + delete globalThis.cancelAnimationFrame; + delete (globalThis as { WebSocket?: unknown }).WebSocket; + Object.defineProperty(globalThis, "IS_REACT_ACT_ENVIRONMENT", { + configurable: true, + value: undefined, + }); +}); + +describe("session stream socket", () => { + test("cancels reconnect and rendering work after an unmount", async () => { + const { createRoot } = await import("react-dom/client"); + const container = document.querySelector("#app"); + + if (!(container instanceof HTMLElement)) { + throw new Error("Expected the test root element."); + } + + root = createRoot(container); + await act(async () => { + root?.render(); + }); + + expect(TestWebSocket.instances).toHaveLength(1); + + await act(async () => { + TestWebSocket.instances[0]?.disconnect(); + root?.unmount(); + }); + root = null; + + await new Promise((resolve) => setTimeout(resolve, 400)); + + expect(TestWebSocket.instances).toHaveLength(1); + expect(pendingFrames).toHaveLength(0); + }); +}); diff --git a/bun.lock b/bun.lock index dafa3d36..4e2b808a 100644 --- a/bun.lock +++ b/bun.lock @@ -5,21 +5,21 @@ "": { "name": "mosoo", "devDependencies": { - "@graphql-codegen/cli": "^7.2.0", + "@graphql-codegen/cli": "^7.3.1", "@graphql-codegen/client-preset": "^6.1.3", "@graphql-codegen/schema-ast": "^6.1.0", - "@j178/prek": "^0.4.13", + "@j178/prek": "^0.5.0", "graphql": "^17.0.2", - "knip": "^6.32.2", + "knip": "^6.33.0", "react-doctor": "0.9.12", - "tsx": "^4.23.12", - "vite-plus": "^0.1.24", + "tsx": "^4.23.13", + "vite-plus": "^0.3.0", }, }, "apps/api": { "name": "@mosoo/api", "dependencies": { - "@cloudflare/sandbox": "0.12.6", + "@cloudflare/sandbox": "0.12.9", "@mosoo/ag-ui-session": "workspace:*", "@mosoo/agent-driver": "workspace:*", "@mosoo/agent-package": "workspace:*", @@ -36,21 +36,21 @@ "@orpc/server": "^1.15.0", "arktype": "^2.2.0", "better-auth": "1.6.28", - "cloudflare": "7.0.0", + "cloudflare": "7.1.0", "drizzle-orm": "^0.45.2", "fflate": "^0.8.3", "graphql": "^17.0.2", "graphql-yoga": "^5.21.3", "hono": "^4.12.32", + "ignore": "7.0.7", "jsonc-parser": "3.3.1", "smol-toml": "1.8.0", - "xstate": "^5.32.0", }, "devDependencies": { "@types/node": "^25.8.0", "typescript": "^6.0.3", - "vite-plus": "^0.1.23", - "wrangler": "^4.123.0", + "vite-plus": "^0.3.0", + "wrangler": "^4.127.1", }, }, "apps/driver": { @@ -62,18 +62,18 @@ "dependencies": { "@agentclientprotocol/sdk": "1.4.0", "@anthropic-ai/claude-agent-sdk": "0.3.251", - "@anthropic-ai/sdk": "0.121.0", + "@anthropic-ai/sdk": "0.122.0", "@modelcontextprotocol/client": "^2.0.0", "@orpc/client": "^1.15.0", "fflate": "^0.8.3", "vestig": "^0.24.1", - "zod": "^4.4.3", + "zod": "4.5.4", }, "devDependencies": { - "@openai/codex": "0.150.1", + "@openai/codex": "0.151.0", "@types/bun": "1.4.0", "@types/node": "^26.3.0", - "opencode-ai": "1.18.23", + "opencode-ai": "1.18.25", "typescript": "^7.0.2", "vite-plus": "0.3.0", }, @@ -83,7 +83,7 @@ "dependencies": { "@assistant-ui/react": "0.15.14", "@base-ui/react": "^1.7.0", - "@cloudflare/sandbox": "0.12.6", + "@cloudflare/sandbox": "0.12.9", "@hugeicons/core-free-icons": "^4.2.3", "@hugeicons/react": "^1.1.9", "@lobehub/icons-static-svg": "^1.90.0", @@ -99,11 +99,9 @@ "@xterm/addon-fit": "^0.11.0", "@xterm/xterm": "^6.0.0", "better-auth": "^1.6.28", - "boring-avatars": "^2.0.4", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "lucide-react": "^1.31.0", - "motion": "^12.40.0", "react": "^19.2.8", "react-dom": "^19.2.8", "react-markdown": "^10.1.0", @@ -124,8 +122,8 @@ "jsdom": "^29.1.1", "typescript": "^6.0.3", "vite": "^8.2.1", - "vite-plus": "^0.1.24", - "wrangler": "^4.123.0", + "vite-plus": "^0.3.0", + "wrangler": "^4.127.1", }, }, "e2e": { @@ -150,7 +148,7 @@ }, "devDependencies": { "typescript": "^6.0.3", - "vite-plus": "^0.1.23", + "vite-plus": "^0.3.0", }, }, "pkgs/agent-package": { @@ -161,7 +159,7 @@ }, "devDependencies": { "typescript": "^6.0.3", - "vite-plus": "^0.1.23", + "vite-plus": "^0.3.0", }, }, "pkgs/contracts": { @@ -172,7 +170,7 @@ }, "devDependencies": { "typescript": "^6.0.3", - "vite-plus": "^0.1.23", + "vite-plus": "^0.3.0", }, }, "pkgs/db": { @@ -186,7 +184,7 @@ "@types/node": "^25.8.0", "drizzle-kit": "^0.31.10", "typescript": "^6.0.3", - "vite-plus": "^0.1.23", + "vite-plus": "^0.3.0", }, }, "pkgs/design-tokens": { @@ -196,14 +194,14 @@ "name": "@mosoo/development-auth", "devDependencies": { "typescript": "^6.0.3", - "vite-plus": "^0.1.23", + "vite-plus": "^0.3.0", }, }, "pkgs/effects": { "name": "@mosoo/effects", "devDependencies": { "typescript": "^6.0.3", - "vite-plus": "^0.1.23", + "vite-plus": "^0.3.0", }, }, "pkgs/id": { @@ -213,7 +211,7 @@ }, "devDependencies": { "typescript": "^6.0.3", - "vite-plus": "^0.1.23", + "vite-plus": "^0.3.0", }, }, "pkgs/observability": { @@ -223,7 +221,7 @@ }, "devDependencies": { "typescript": "^6.0.3", - "vite-plus": "^0.1.23", + "vite-plus": "^0.3.0", }, }, "pkgs/public-api-client": { @@ -234,7 +232,7 @@ "devDependencies": { "@types/bun": "^1.3.14", "typescript": "^6.0.3", - "vite-plus": "^0.1.23", + "vite-plus": "^0.3.0", }, }, "pkgs/runtime-catalog": { @@ -245,7 +243,7 @@ "devDependencies": { "jsonc-parser": "3.3.1", "typescript": "^6.0.3", - "vite-plus": "^0.1.23", + "vite-plus": "^0.3.0", }, }, "pkgs/runtime-events": { @@ -257,7 +255,7 @@ }, "devDependencies": { "typescript": "^6.0.3", - "vite-plus": "^0.1.23", + "vite-plus": "^0.3.0", }, }, "pkgs/session-policy": { @@ -268,7 +266,7 @@ }, "devDependencies": { "typescript": "^6.0.3", - "vite-plus": "^0.1.23", + "vite-plus": "^0.3.0", }, }, "pkgs/skill-package": { @@ -279,7 +277,7 @@ }, "devDependencies": { "typescript": "^6.0.3", - "vite-plus": "^0.1.23", + "vite-plus": "^0.3.0", }, }, }, @@ -308,7 +306,7 @@ "@anthropic-ai/claude-agent-sdk-win32-x64": ["@anthropic-ai/claude-agent-sdk-win32-x64@0.3.251", "", { "os": "win32", "cpu": "x64" }, "sha512-/jmtFIvfF0UFMxSZ8WV2Aus8aGA3+8Ft6AHvWuBJkY5jM2ubfqi6GnBjKCAL831TTllp2rKZlViANtWWRBvpvg=="], - "@anthropic-ai/sdk": ["@anthropic-ai/sdk@0.121.0", "", { "dependencies": { "json-schema-to-ts": "^3.1.1", "standardwebhooks": "^1.0.0" }, "peerDependencies": { "zod": "^3.25.0 || ^4.0.0" }, "optionalPeers": ["zod"], "bin": { "anthropic-ai-sdk": "bin/cli" } }, "sha512-WFzwcH8l49CZHv0vQ9QQT51VJ1/DLDQfNSs9awpGlnKBdaSnqtdZa+tw/3cxxosTPIgK8uw1GqsJ2dSbCBD1Lg=="], + "@anthropic-ai/sdk": ["@anthropic-ai/sdk@0.122.0", "", { "dependencies": { "json-schema-to-ts": "^3.1.1", "standardwebhooks": "^1.0.0" }, "peerDependencies": { "zod": "^3.25.0 || ^4.0.0" }, "optionalPeers": ["zod"], "bin": { "anthropic-ai-sdk": "bin/cli" } }, "sha512-GGPNftt0caaz9MDlmNQGHX8855Ojaduyy5pm9Sm1h7HalCn0cWNb5/bweadJF+4yzbal+QL6ztBa09WAAOzLmQ=="], "@apm-js-collab/code-transformer": ["@apm-js-collab/code-transformer@0.18.0", "", { "dependencies": { "@types/estree": "^1.0.8", "astring": "^1.9.0", "esquery": "^1.7.0", "meriyah": "^6.1.4", "semifies": "^1.0.0", "source-map": "^0.6.0" }, "bin": { "code-transformer": "cli.js" } }, "sha512-aN3Oq8r1J3gPJtCwErP664gM0+HhM1I1lujPr9TMTCcEl/joQQbpGpeMdts9B1+W2wHMsvioDMv5F4PvMWE6gw=="], @@ -412,19 +410,19 @@ "@cloudflare/kv-asset-handler": ["@cloudflare/kv-asset-handler@0.5.0", "", {}, "sha512-jxQYkj8dSIzc0cD6cMMNdOc1UVjqSqu8BZdor5s8cGjW2I8BjODt/kWPVdY+u9zj3ms75Q5qaZgnxUad83+eAg=="], - "@cloudflare/sandbox": ["@cloudflare/sandbox@0.12.6", "", { "dependencies": { "@cloudflare/containers": "^0.3.5", "aws4fetch": "^1.0.20", "capnweb": "^0.8.0", "hono": "^4.13.0" }, "peerDependencies": { "@openai/agents": "^0.3.3", "@opencode-ai/sdk": "^1.1.40", "@xterm/xterm": ">=5.0.0" }, "optionalPeers": ["@openai/agents", "@opencode-ai/sdk", "@xterm/xterm"] }, "sha512-UmA1XH6TZp9VKArSnTAUd2HuTw3f29qfDY6q2lAmdPQZYTFHxY/0Dn1LzjxhMiKZkyyQhvOHBHOYnwga9sVlag=="], + "@cloudflare/sandbox": ["@cloudflare/sandbox@0.12.9", "", { "dependencies": { "@cloudflare/containers": "^0.3.5", "aws4fetch": "^1.0.20", "capnweb": "^0.8.0", "hono": "^4.13.0" }, "peerDependencies": { "@openai/agents": "^0.3.3", "@opencode-ai/sdk": "^1.1.40", "@xterm/xterm": ">=5.0.0" }, "optionalPeers": ["@openai/agents", "@opencode-ai/sdk", "@xterm/xterm"] }, "sha512-JlCQ8adVaHT3TrZO13X6US2LJTyQ4YvoEZpfh5EewRqcxPfMHZNJPB/1dsRdiFqmtzpz+lMxGazSUa2H/g2IKg=="], "@cloudflare/unenv-preset": ["@cloudflare/unenv-preset@2.16.1", "", { "peerDependencies": { "unenv": "2.0.0-rc.24", "workerd": ">1.20260305.0 <2.0.0-0" }, "optionalPeers": ["workerd"] }, "sha512-ECxObrMfyTl5bhQf/lZCXwo5G6xX9IAUo+nDMKK4SZ8m4Jvvxp52vilxyySSWh2YTZz8+HQ07qGH/2rEom1vDw=="], - "@cloudflare/workerd-darwin-64": ["@cloudflare/workerd-darwin-64@1.20260811.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-i5jqz+ywtOefr0AJbiAc8qxBLfSim/B0WJG7aW3B+pWnoVfMJdUQvi+BWcFKZJ0MoCci3KadTx6g31VfuEEqpQ=="], + "@cloudflare/workerd-darwin-64": ["@cloudflare/workerd-darwin-64@1.20260828.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-CVd+xPhqUESg8Xhq09TZx0wl4FSirfJGOzvbPz2yHhBIvmNHFFQkSN3rkd7wEwnhQQk37Xi0/aD6ykPLJbmGiQ=="], - "@cloudflare/workerd-darwin-arm64": ["@cloudflare/workerd-darwin-arm64@1.20260811.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-NoOUM/nvaDdm2Onlnz33FikWjtatzulNtvwvy4xs0IrHaTCHwC0c8NwIt6s+AI13FkDs02/vm2I3GTPLCT9+hQ=="], + "@cloudflare/workerd-darwin-arm64": ["@cloudflare/workerd-darwin-arm64@1.20260828.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-5HDPXRM152vU5JveByGFk34X57TVyIsfp4cabepAf45DC0MKvm52ucJqAjW1h8bvW4X+zRw9GU35OHF9FEC9Ww=="], - "@cloudflare/workerd-linux-64": ["@cloudflare/workerd-linux-64@1.20260811.1", "", { "os": "linux", "cpu": "x64" }, "sha512-sdYq2jL1AD1supa3fsi5O4zTB28wSjvTHj7Migh6/ts8EROPdvrSwv+rdGHhv8HJNAz/wbIAY3wZsi1Rw4uUIg=="], + "@cloudflare/workerd-linux-64": ["@cloudflare/workerd-linux-64@1.20260828.1", "", { "os": "linux", "cpu": "x64" }, "sha512-MQ1Ll9P7F72HHUKizbb7BlDfbY8fRoNMpbIpZoU6uKsSkneFICWSKv6UlgU9EQZ+w0i7TMa12iUgJ8l29eRI9A=="], - "@cloudflare/workerd-linux-arm64": ["@cloudflare/workerd-linux-arm64@1.20260811.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-RIRv4shbu1kg05sD+DHTpSFCNnb5Dl2SkPDMUykqZa508tkPqe7VVw7gO0Q5msTBGyL0FfFrLuRxwwfA8u5Sow=="], + "@cloudflare/workerd-linux-arm64": ["@cloudflare/workerd-linux-arm64@1.20260828.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-FBTaUQ1xcU9jcp4OyBPcH8x0QiFvc1iuZL2GkD8zp2q1WyTVHYOptRDQUU+cuHjt0rQ2EIKVPBjahPxfa0joBw=="], - "@cloudflare/workerd-windows-64": ["@cloudflare/workerd-windows-64@1.20260811.1", "", { "os": "win32", "cpu": "x64" }, "sha512-g6VquwjASlYAibcNW/0E6Zszht4qLkmnXOGwIjjRHl2A0Qz48kVeMcGvyH6eA0G9U3OzZojjYFpP+YeyQmmdjw=="], + "@cloudflare/workerd-windows-64": ["@cloudflare/workerd-windows-64@1.20260828.1", "", { "os": "win32", "cpu": "x64" }, "sha512-yvr77hC7dUbvK5K+SCg062kkPq3sx+drV1PcgHslzHDYcJBtT0V3X80qLE49LW1vq2svaeNmsVQS+vHsqWu8cQ=="], "@cspotcode/source-map-support": ["@cspotcode/source-map-support@0.8.1", "", { "dependencies": { "@jridgewell/trace-mapping": "0.3.9" } }, "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw=="], @@ -538,7 +536,7 @@ "@graphql-codegen/add": ["@graphql-codegen/add@7.1.0", "", { "dependencies": { "@graphql-codegen/plugin-helpers": "^7.1.0", "tslib": "^2.8.0" }, "peerDependencies": { "graphql": "^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" } }, "sha512-bytJg1kel5zfgK3JSYbGwtpbNe6F9OPZSR6DiMDe9RVxblAgl6w4zEEPd/mM3rhNJ1VmGYLbNnf5e1eUfXQEbg=="], - "@graphql-codegen/cli": ["@graphql-codegen/cli@7.2.0", "", { "dependencies": { "@babel/generator": "^7.18.13", "@babel/template": "^7.18.10", "@babel/types": "^7.18.13", "@graphql-codegen/client-preset": "^6.1.0", "@graphql-codegen/core": "^6.2.0", "@graphql-codegen/plugin-helpers": "^7.1.0", "@graphql-tools/apollo-engine-loader": "^8.0.28", "@graphql-tools/code-file-loader": "^8.1.28", "@graphql-tools/git-loader": "^8.0.32", "@graphql-tools/github-loader": "^9.0.6", "@graphql-tools/graphql-file-loader": "^8.1.11", "@graphql-tools/json-file-loader": "^8.0.26", "@graphql-tools/load": "^8.1.8", "@graphql-tools/merge": "^9.0.6", "@graphql-tools/url-loader": "^9.0.6", "@graphql-tools/utils": "^11.2.0", "@inquirer/prompts": "^8.3.2", "@whatwg-node/fetch": "^0.10.0", "chalk": "^5.6.0", "cosmiconfig": "^9.0.0", "debounce": "^3.0.0", "detect-indent": "^7.0.0", "graphql-config": "^5.1.6", "is-glob": "^4.0.1", "jiti": "^2.3.0", "json-to-pretty-yaml": "^1.2.2", "listr2": "^10.2.1", "log-symbols": "^7.0.0", "micromatch": "^4.0.5", "shell-quote": "^1.7.3", "string-env-interpolation": "^1.0.1", "ts-log": "^3.0.0", "tslib": "^2.4.0", "yaml": "^2.3.1", "yargs": "^18.0.0" }, "peerDependencies": { "@parcel/watcher": "^2.1.0", "graphql": "^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" }, "optionalPeers": ["@parcel/watcher"], "bin": { "gql-gen": "esm/bin.js", "graphql-codegen": "esm/bin.js", "graphql-code-generator": "esm/bin.js", "graphql-codegen-esm": "esm/bin.js", "graphql-codegen-cjs": "cjs/bin.js" } }, "sha512-JPJw2vquEIpO3b8XJyxFVTrYi6WRn/OKu/SlzQA+IwAVT7GZPeG+AHmfRXAvpVMj31899nTpQYEQGUxx3ZqubQ=="], + "@graphql-codegen/cli": ["@graphql-codegen/cli@7.3.1", "", { "dependencies": { "@babel/generator": "^7.18.13", "@babel/template": "^7.18.10", "@babel/types": "^7.18.13", "@graphql-codegen/client-preset": "^6.1.3", "@graphql-codegen/core": "^6.2.0", "@graphql-codegen/plugin-helpers": "^7.2.1", "@graphql-tools/apollo-engine-loader": "^8.0.28", "@graphql-tools/code-file-loader": "^8.1.28", "@graphql-tools/git-loader": "^8.0.32", "@graphql-tools/github-loader": "^9.0.6", "@graphql-tools/graphql-file-loader": "^8.1.11", "@graphql-tools/json-file-loader": "^8.0.26", "@graphql-tools/load": "^8.1.8", "@graphql-tools/merge": "^9.0.6", "@graphql-tools/url-loader": "^9.0.6", "@graphql-tools/utils": "^11.2.0", "@inquirer/prompts": "^8.3.2", "@whatwg-node/fetch": "^0.10.0", "chalk": "^5.6.0", "cosmiconfig": "^9.0.0", "debounce": "^3.0.0", "detect-indent": "^7.0.0", "graphql-config": "^5.1.6", "is-glob": "^4.0.1", "jiti": "^2.3.0", "json-to-pretty-yaml": "^1.2.2", "listr2": "^10.2.1", "log-symbols": "^7.0.0", "micromatch": "^4.0.5", "shell-quote": "^1.7.3", "string-env-interpolation": "^1.0.1", "ts-log": "^3.0.0", "tslib": "^2.4.0", "yaml": "^2.3.1", "yargs": "^18.0.0" }, "peerDependencies": { "@parcel/watcher": "^2.1.0", "graphql": "^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" }, "optionalPeers": ["@parcel/watcher"], "bin": { "gql-gen": "esm/bin.js", "graphql-codegen": "esm/bin.js", "graphql-codegen-cjs": "cjs/bin.js", "graphql-codegen-esm": "esm/bin.js", "graphql-code-generator": "esm/bin.js" } }, "sha512-xDEI/Jpw2bmLyHJ3KDO6UavOKxujSO08p8Bz31NwRNhvKnVpy2N5tiYZhnhIBCcCqUb7PfTt11dr5EWGZGgB4w=="], "@graphql-codegen/client-preset": ["@graphql-codegen/client-preset@6.1.3", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.20.2", "@babel/template": "^7.20.7", "@graphql-codegen/add": "^7.1.0", "@graphql-codegen/gql-tag-operations": "^6.1.0", "@graphql-codegen/plugin-helpers": "^7.1.0", "@graphql-codegen/typed-document-node": "^7.1.0", "@graphql-codegen/typescript": "^6.1.0", "@graphql-codegen/typescript-operations": "^6.1.6", "@graphql-codegen/visitor-plugin-common": "^7.2.5", "@graphql-tools/documents": "^1.0.0", "@graphql-tools/utils": "^11.2.0", "@graphql-typed-document-node/core": "3.2.0", "tslib": "^2.8.0" }, "peerDependencies": { "graphql": "^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0", "graphql-sock": "^1.0.0" }, "optionalPeers": ["graphql-sock"] }, "sha512-bIuJiirdwzx784bnqZsoC+wEzCyhr0T+tbVhwuZRudyZXPeLm+7/tn/FTAMqZhDGgSr6hVnpcU41abdBNn2zag=="], @@ -546,7 +544,7 @@ "@graphql-codegen/gql-tag-operations": ["@graphql-codegen/gql-tag-operations@6.1.0", "", { "dependencies": { "@graphql-codegen/plugin-helpers": "^7.1.0", "@graphql-codegen/visitor-plugin-common": "^7.2.0", "@graphql-tools/utils": "^11.2.0", "auto-bind": "^5.0.0", "tslib": "^2.8.0" }, "peerDependencies": { "graphql": "^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" } }, "sha512-AmMcZFwonufvWJnQm7I0lBxKpAm+35BcCrOOvUlBoviohiR17aPoTGAOaNAEtpcpI86lnZ9m9AXUdiKMdm8nnQ=="], - "@graphql-codegen/plugin-helpers": ["@graphql-codegen/plugin-helpers@7.1.0", "", { "dependencies": { "@graphql-tools/utils": "^11.2.0", "change-case-all": "^2.1.0", "common-tags": "1.8.2", "import-from": "4.0.0", "tslib": "^2.8.0" }, "peerDependencies": { "graphql": "^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" } }, "sha512-ieJH7kZ5oSZKBPJs7CvHMrFY/CLYLklqv74ir93qMwRna6geZsbIMoJzTDBXohxcQTITiProiYSGrEtZjIpYGg=="], + "@graphql-codegen/plugin-helpers": ["@graphql-codegen/plugin-helpers@7.2.1", "", { "dependencies": { "@graphql-tools/utils": "^11.2.0", "change-case-all": "^2.1.0", "common-tags": "1.8.2", "import-from": "4.0.0", "tslib": "^2.8.0" }, "peerDependencies": { "graphql": "^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" } }, "sha512-Jw2HPaioMEa4DKG7sgqzL1uWQiIWQu774FwKXx4p83/y3o5GgnRMRdTFtuQQ7Tq6PYyrI5ZITi1jAxeX/Vh2Uw=="], "@graphql-codegen/schema-ast": ["@graphql-codegen/schema-ast@6.1.0", "", { "dependencies": { "@graphql-codegen/plugin-helpers": "^7.1.0", "@graphql-tools/utils": "^11.2.0", "tslib": "^2.8.0" }, "peerDependencies": { "graphql": "^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" } }, "sha512-/xuGkM5gUNFRoaQLumKbENdX7Hc8ha49z9OXsEZY8E+46mMjqzXGF0NtCJ892cmoX7EUgI5c8T+LZqS2upx2Aw=="], @@ -724,41 +722,41 @@ "@inquirer/type": ["@inquirer/type@4.0.7", "", { "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-t28inv14nMQ1PhKpsJPY+kEs/c00qzeCOS2gTNRyTjG5d6qsVA2fItxW4hkvGZ5lvanGLdtCzVIx5dwdRpN1+g=="], - "@j178/prek": ["@j178/prek@0.4.13", "", { "optionalDependencies": { "@j178/prek-android-arm64": "0.4.13", "@j178/prek-darwin-arm64": "0.4.13", "@j178/prek-darwin-x64": "0.4.13", "@j178/prek-linux-arm-gnueabihf": "0.4.13", "@j178/prek-linux-arm-musleabihf": "0.4.13", "@j178/prek-linux-arm64-gnu": "0.4.13", "@j178/prek-linux-arm64-musl": "0.4.13", "@j178/prek-linux-armv7-musleabihf": "0.4.13", "@j178/prek-linux-ia32-gnu": "0.4.13", "@j178/prek-linux-ia32-musl": "0.4.13", "@j178/prek-linux-riscv64-gnu": "0.4.13", "@j178/prek-linux-s390x-gnu": "0.4.13", "@j178/prek-linux-x64-gnu": "0.4.13", "@j178/prek-linux-x64-musl": "0.4.13", "@j178/prek-win32-arm64-msvc": "0.4.13", "@j178/prek-win32-ia32-msvc": "0.4.13", "@j178/prek-win32-x64-msvc": "0.4.13" }, "bin": { "prek": "bin/prek.js" } }, "sha512-8Q5XeuBX935Y0gSdAKi8OyRUwNwx4W6iKOXIzbRKmfTDXPTkq+ndc7u15v2qbo+CJ/2K5u26v9RJgQYurf7q7g=="], + "@j178/prek": ["@j178/prek@0.5.0", "", { "optionalDependencies": { "@j178/prek-android-arm64": "0.5.0", "@j178/prek-darwin-arm64": "0.5.0", "@j178/prek-darwin-x64": "0.5.0", "@j178/prek-linux-arm-gnueabihf": "0.5.0", "@j178/prek-linux-arm-musleabihf": "0.5.0", "@j178/prek-linux-arm64-gnu": "0.5.0", "@j178/prek-linux-arm64-musl": "0.5.0", "@j178/prek-linux-armv7-musleabihf": "0.5.0", "@j178/prek-linux-ia32-gnu": "0.5.0", "@j178/prek-linux-ia32-musl": "0.5.0", "@j178/prek-linux-riscv64-gnu": "0.5.0", "@j178/prek-linux-s390x-gnu": "0.5.0", "@j178/prek-linux-x64-gnu": "0.5.0", "@j178/prek-linux-x64-musl": "0.5.0", "@j178/prek-win32-arm64-msvc": "0.5.0", "@j178/prek-win32-ia32-msvc": "0.5.0", "@j178/prek-win32-x64-msvc": "0.5.0" }, "bin": { "prek": "bin/prek.js" } }, "sha512-n6+ErzhtUQLJl5gXpCS/gKFw4xzR2LY391DkRpwGXR2FTJ38mhiseuxf/lXhpiKQEknLBge2ZVAFAZog2qAIpw=="], - "@j178/prek-android-arm64": ["@j178/prek-android-arm64@0.4.13", "", { "os": "android", "cpu": "arm64" }, "sha512-xiQ6B4VHCfj0XTUYZ+O6vCIA0uHP68GDVjc9yEcit4biEMm8KKRrEe1EYTNE/9V5Mmb1tC5yVW+y7Cn9kjK06g=="], + "@j178/prek-android-arm64": ["@j178/prek-android-arm64@0.5.0", "", { "os": "android", "cpu": "arm64" }, "sha512-quyKIm52bzZQTY0RNiGbJ0w2ELzp8rznBbXApWLe3u6LLP3WNgdIHsHlyurquy5vgUguCs3UZ67Rjf7/X04EdA=="], - "@j178/prek-darwin-arm64": ["@j178/prek-darwin-arm64@0.4.13", "", { "os": "darwin", "cpu": "arm64" }, "sha512-YzGxjRDOjjiyESrBKDiuqS9HskcUrv3NRHaXObWKEKSwYoezXv7/lT0VRls9EfzJljRkHBNHGH9OmYMXwK+daQ=="], + "@j178/prek-darwin-arm64": ["@j178/prek-darwin-arm64@0.5.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-gR2WVJJLYKbGm2hvol/Vd+V/XweH2Am/Nxth0EcnbTKBf1jtxsVv1iy5UAOP2ye0rCNujQNOJ3v1eSOLQXgqeg=="], - "@j178/prek-darwin-x64": ["@j178/prek-darwin-x64@0.4.13", "", { "os": "darwin", "cpu": "x64" }, "sha512-8H/KmSXyerVbf+rE8t5a8JjpvRJGVYCGsi0lGze48W7Alpj2d41Dha9Gd6HMPoh/mpaffJUKj0Vn44bTuZzJ6Q=="], + "@j178/prek-darwin-x64": ["@j178/prek-darwin-x64@0.5.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-j6Bx31w2W644Ptpv3PxwqdDorgYqk/Bxr4hvLshnukXYeJq9YNpu7Y1IBOE9P5uKFGwUsYCzzrSwZiBbqz6RdA=="], - "@j178/prek-linux-arm-gnueabihf": ["@j178/prek-linux-arm-gnueabihf@0.4.13", "", { "os": "linux", "cpu": "arm" }, "sha512-Nv8PtEmvmugRhew4FzUNG/ARJ57wGSyNm6MoSbZM6sEFa+spdRGEp6r8+S5VwTm4/sss+BjMB8rI5qjZLDbI1A=="], + "@j178/prek-linux-arm-gnueabihf": ["@j178/prek-linux-arm-gnueabihf@0.5.0", "", { "os": "linux", "cpu": "arm" }, "sha512-XAhOo40oSuRtY9KdGO1aMY3Je4/EI2pGF2fZUAu+cc5PUHOphnwYhBA2eSXGFqtGPPTxx/zwAOnK/IY3SfrPnA=="], - "@j178/prek-linux-arm-musleabihf": ["@j178/prek-linux-arm-musleabihf@0.4.13", "", { "os": "linux", "cpu": "arm" }, "sha512-gUi8ozJ9AmkguhZqEdcdXhkgmBXa4Hl39L2LMrTsoqhUStLl9V7kC10vVompAnN1fSwJapB35jMbVusCPtcP0g=="], + "@j178/prek-linux-arm-musleabihf": ["@j178/prek-linux-arm-musleabihf@0.5.0", "", { "os": "linux", "cpu": "arm" }, "sha512-ZqMdctoF3ZvRCHsgdb5n+zqgYnZ8MTtWnN+Z4LesvnUl7vH8hY1egPPmMxtwL61fUzje++Cb2YJoV5DtSetWeQ=="], - "@j178/prek-linux-arm64-gnu": ["@j178/prek-linux-arm64-gnu@0.4.13", "", { "os": "linux", "cpu": "arm64" }, "sha512-pgkZEVWKtANLzMkWWH2M8r2Umu+3wqeubZ4A+pc4ErOwzC3wNfgEyBroj/lUHJ3iaHtsKSqgasAFd43Z9hOfLg=="], + "@j178/prek-linux-arm64-gnu": ["@j178/prek-linux-arm64-gnu@0.5.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-0KqyBfJX0oV9uuq00kh0/aFffkM/pP6u4N57k+5iUW6aScZh3gxGZSooS5WbOypVsC8wSy6GMksIhlS1oJp/dQ=="], - "@j178/prek-linux-arm64-musl": ["@j178/prek-linux-arm64-musl@0.4.13", "", { "os": "linux", "cpu": "arm64" }, "sha512-Em8VcdIze0xXrGIcHpBz9TKh3LXKaqOLEAEGPzNlNdS6HfwQqCyC8u1/vVvFNjqgEK4j6yxAvgWiA/Ib/uvjkw=="], + "@j178/prek-linux-arm64-musl": ["@j178/prek-linux-arm64-musl@0.5.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-la4mkGWexlfy1rwM6jHC6lUTlQ3VGqWdewefnFGeCY1Q1J+YWKlmN9Co6HuHtPF68IJLGYjAZ46Wv54zxuXoLQ=="], - "@j178/prek-linux-armv7-musleabihf": ["@j178/prek-linux-armv7-musleabihf@0.4.13", "", { "os": "linux", "cpu": "arm" }, "sha512-6f79cb1o3ILBLNCMnoFY5f390T4Wj8YH0YYhTgL5Dh8Fhpzd3VUBO2wSBU2BJw2NoRN0FxyQeaOtWQ59v9U0kw=="], + "@j178/prek-linux-armv7-musleabihf": ["@j178/prek-linux-armv7-musleabihf@0.5.0", "", { "os": "linux", "cpu": "arm" }, "sha512-AC8MG7785tNHch0LENLLDvVqjKzlBsPis+EMdXj7Etb7DCWaxm2EphCEtzETIbi2QHnZhJLo1v3H+0YK2a78Bg=="], - "@j178/prek-linux-ia32-gnu": ["@j178/prek-linux-ia32-gnu@0.4.13", "", { "os": "linux", "cpu": "ia32" }, "sha512-Q9wDpZkRQ8M4nD+EEuk+yHfNqDN/AH7ksAkLYu8h+Vmc77nOrs6XK/Qbyun7zVogfyDzXneCoJzk0oqkZp92JA=="], + "@j178/prek-linux-ia32-gnu": ["@j178/prek-linux-ia32-gnu@0.5.0", "", { "os": "linux", "cpu": "ia32" }, "sha512-2Yf4DIa9p4Pc3b8fH1zOv9TvywGZ/f6AKiTLjFiGAXTOq0oMqr6iRSvybe4EwH036tj419MLK7ccvCUDgDT4wQ=="], - "@j178/prek-linux-ia32-musl": ["@j178/prek-linux-ia32-musl@0.4.13", "", { "os": "linux", "cpu": "ia32" }, "sha512-7KP4LwTZ7kpUt+7yyTewW34QzFtoSNLb8gKR3GolaxjWAiGtZm+l7kCVdle8lOJe5kgr0vuR+THUcpx15UcJ0g=="], + "@j178/prek-linux-ia32-musl": ["@j178/prek-linux-ia32-musl@0.5.0", "", { "os": "linux", "cpu": "ia32" }, "sha512-fM7Qj57zQvggAogA/ufpWzRyWbYWG5suFEQxSRQncZHWN5EH7sFnQgMYiVL7UmoafZWXkgGYng19bbafU6EN6Q=="], - "@j178/prek-linux-riscv64-gnu": ["@j178/prek-linux-riscv64-gnu@0.4.13", "", { "os": "linux", "cpu": "none" }, "sha512-I9dbxn7Z/lxHQffbsJvIRT/NlAWnwPLlmjq3QsbQFgcJAg4qKLXTN2JNXw2v+9s3y/B4ihfqXv9jLHbWUvFG5w=="], + "@j178/prek-linux-riscv64-gnu": ["@j178/prek-linux-riscv64-gnu@0.5.0", "", { "os": "linux", "cpu": "none" }, "sha512-0BAoNsPDQMXvfhAtjdn/2aUXWJaAeFPQwDXwEzNmbmCXHdFTl5Ox0ziZTrxr9mwWDEK6ju9oOXWKbSsUWKPi5w=="], - "@j178/prek-linux-s390x-gnu": ["@j178/prek-linux-s390x-gnu@0.4.13", "", { "os": "linux", "cpu": "s390x" }, "sha512-qutbzlPaFh+w3jn2qU/phDoNgR2mmOaH5PBFtcN6mdjBLDQ0PylRksFCGkXer45regolKIrMWEu/g9qwiKEF+Q=="], + "@j178/prek-linux-s390x-gnu": ["@j178/prek-linux-s390x-gnu@0.5.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-QzEDiy5J8W6LZCfe+0BJzwKyn5bQMkhiNOcldT3+UP83nhps5f+LevalZ3+/VTR6I6b2U+dHJxVEvoo3MVW/9A=="], - "@j178/prek-linux-x64-gnu": ["@j178/prek-linux-x64-gnu@0.4.13", "", { "os": "linux", "cpu": "x64" }, "sha512-IuDdTzd28992PcZn6bMfWEQ5Z5D9dDCDv/6s9ANJZNv76vQM0Zh+ksLU5lalCnEtfHMq3cuXdcQCiMwm14Q6+Q=="], + "@j178/prek-linux-x64-gnu": ["@j178/prek-linux-x64-gnu@0.5.0", "", { "os": "linux", "cpu": "x64" }, "sha512-xJ/EWgbJ9KZ0d6ow2HpGcoQb10eS1ye79lvRwIt4MHbG33QK6w4oO8TVK7jane6Ql1tlMRMXwmvWpbLy5gcciw=="], - "@j178/prek-linux-x64-musl": ["@j178/prek-linux-x64-musl@0.4.13", "", { "os": "linux", "cpu": "x64" }, "sha512-kVBSPcfrDDT7PdQ+jVtch3MxSc+AjXSmTQ4F2uD4iRH8lrJgeAhIbNZGEM/udAFyfz7cVjw7VyGJKZPvbH/nOw=="], + "@j178/prek-linux-x64-musl": ["@j178/prek-linux-x64-musl@0.5.0", "", { "os": "linux", "cpu": "x64" }, "sha512-A2LqYn/D45ICYMOHTt6OPCrW1Fd7Ta5FlRMqAsdRFZRja0Ar5435hHo/dHjpeBYpsI6MtiQVdwu20c7/KETmqQ=="], - "@j178/prek-win32-arm64-msvc": ["@j178/prek-win32-arm64-msvc@0.4.13", "", { "os": "win32", "cpu": "arm64" }, "sha512-LbJ8L3HDgdLsCmNEFTdttri/4FYRA1CKwFD0DOgXDhPiVyB4Sastu1LwZYPM60CB4kw+b97T0qfFzZfMHr0wZQ=="], + "@j178/prek-win32-arm64-msvc": ["@j178/prek-win32-arm64-msvc@0.5.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-VXoFjrPebIdH6zEV1A7rxCb5no/uMwA/Jt0AcHG6WE6m8an3xL3MimiasV/YoH72XBrvjbKfO6JA+6yKE9xrjQ=="], - "@j178/prek-win32-ia32-msvc": ["@j178/prek-win32-ia32-msvc@0.4.13", "", { "os": "win32", "cpu": "ia32" }, "sha512-3jmxuCBSZBmS9ID3eJd5UzjR2O8VnIPytVdJfq0GtcZFZ83z/hxehllP8x3yEZPTdpvM3ueFeHKCMjSwzlEmvg=="], + "@j178/prek-win32-ia32-msvc": ["@j178/prek-win32-ia32-msvc@0.5.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-pCFvlkGEqYP72chBp0A5NGwfBcU5tHn0wTuiEsxwL7uohiis/GigIX8k+QFj9UGYBf0d5Yo7uRyj6UfWgYXm4w=="], - "@j178/prek-win32-x64-msvc": ["@j178/prek-win32-x64-msvc@0.4.13", "", { "os": "win32", "cpu": "x64" }, "sha512-Zgo2xPLgROHCzx6/ROnW2tIHxNZXUVyJo2zfukXIX/e8qKmmChCGJ/oVOQ7qQc+RsxwG839zbauP1qZ8xyniww=="], + "@j178/prek-win32-x64-msvc": ["@j178/prek-win32-x64-msvc@0.5.0", "", { "os": "win32", "cpu": "x64" }, "sha512-BDIuZ+/sDFbTIDVksnv8so5walDyho6CscqVYgD1FFCn2XbUJfg3HogD5lS84Z3rxYsCEWfYh/w88lbaKBWREg=="], "@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.13", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA=="], @@ -828,19 +826,19 @@ "@nodelib/fs.walk": ["@nodelib/fs.walk@1.2.8", "", { "dependencies": { "@nodelib/fs.scandir": "2.1.5", "fastq": "^1.6.0" } }, "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg=="], - "@openai/codex": ["@openai/codex@0.150.1", "", { "optionalDependencies": { "@openai/codex-darwin-arm64": "npm:@openai/codex@0.150.1-darwin-arm64", "@openai/codex-darwin-x64": "npm:@openai/codex@0.150.1-darwin-x64", "@openai/codex-linux-arm64": "npm:@openai/codex@0.150.1-linux-arm64", "@openai/codex-linux-x64": "npm:@openai/codex@0.150.1-linux-x64", "@openai/codex-win32-arm64": "npm:@openai/codex@0.150.1-win32-arm64", "@openai/codex-win32-x64": "npm:@openai/codex@0.150.1-win32-x64" }, "bin": { "codex": "bin/codex.js" } }, "sha512-knrbhpJH3mEULAVStcZW4F5WEt9MQhBj6KFOonBSIUGTLcHlu9CE7FRmr95E33y94+sWNZSeVBBV/kYvlfgxkQ=="], + "@openai/codex": ["@openai/codex@0.151.0", "", { "optionalDependencies": { "@openai/codex-darwin-arm64": "npm:@openai/codex@0.151.0-darwin-arm64", "@openai/codex-darwin-x64": "npm:@openai/codex@0.151.0-darwin-x64", "@openai/codex-linux-arm64": "npm:@openai/codex@0.151.0-linux-arm64", "@openai/codex-linux-x64": "npm:@openai/codex@0.151.0-linux-x64", "@openai/codex-win32-arm64": "npm:@openai/codex@0.151.0-win32-arm64", "@openai/codex-win32-x64": "npm:@openai/codex@0.151.0-win32-x64" }, "bin": { "codex": "bin/codex.js" } }, "sha512-mhtWmOZRdmWD1jPbLDnQb59BsaVP/V+lXe/OFNR9ZcLZU0UCiBwn98Fcav1ss7sDIlHkuqj6nWd44IPeXoOhJA=="], - "@openai/codex-darwin-arm64": ["@openai/codex@0.150.1-darwin-arm64", "", { "os": "darwin", "cpu": "arm64" }, "sha512-Z614kKSI3/p+YMotBJMxCc4kvRCYEgsVmnaCG6U8HDraqTFxYcQYQ79B4/GBCBS88/KtacHI6qzjA+4Mu5BynA=="], + "@openai/codex-darwin-arm64": ["@openai/codex@0.151.0-darwin-arm64", "", { "os": "darwin", "cpu": "arm64" }, "sha512-g7YzpaCZGCw19R/gly3vRPjnLqaW7JcBAu2WQQ6e8PIlvBPmS/gMplIUURMgNO6gi8LsPzdlQtLqkwoeOOlIdg=="], - "@openai/codex-darwin-x64": ["@openai/codex@0.150.1-darwin-x64", "", { "os": "darwin", "cpu": "x64" }, "sha512-vcvJ7Q4IP2vA5ZR3v9Pj9VLKtlhD7efujxHqqW/NJ8F2/XqTD4iPjErV07190Om3wONW48yploTgul16QUk9Mg=="], + "@openai/codex-darwin-x64": ["@openai/codex@0.151.0-darwin-x64", "", { "os": "darwin", "cpu": "x64" }, "sha512-0y+g8TVpP+Fn10mjoKYXER6qYjn29w7xBUsbPXJ6Accu/FoM4Qp4WbKXQPmE0G0yUACTQVZRjzTSsdWUezNgkg=="], - "@openai/codex-linux-arm64": ["@openai/codex@0.150.1-linux-arm64", "", { "os": "linux", "cpu": "arm64" }, "sha512-U0BY5UDsCy1up/O2YzGtVtDbcumNerVyZVVx55PW9gGQsFOL0utOyBJiG/UiocFAXix8JNvNJxYMRt2Jx0FJKg=="], + "@openai/codex-linux-arm64": ["@openai/codex@0.151.0-linux-arm64", "", { "os": "linux", "cpu": "arm64" }, "sha512-CsLgFeX4TQ6I2Gdrxd2r5UbgIbDLCdtcLAlnMYjr06bCL057MTNGec7Ewb3+Z2DBiMuXCljdTBGqLOePkMV0sQ=="], - "@openai/codex-linux-x64": ["@openai/codex@0.150.1-linux-x64", "", { "os": "linux", "cpu": "x64" }, "sha512-B3Bu5MF/G9qFhbSMrdyZz9ocknXOo45DnOdWrREJaxWTjiuUO1ogoDR0/ttdc29JRFkbbXs3aC+Td8vIx3X0oA=="], + "@openai/codex-linux-x64": ["@openai/codex@0.151.0-linux-x64", "", { "os": "linux", "cpu": "x64" }, "sha512-xcVyY1FtwvVYhh2JBmz8fX8CQqFAxO/lxJ2IXsh8x5uwxZVHVl5fZHFHf8JdRaOGG0vpkYmu/DKKVoLd56/DDQ=="], - "@openai/codex-win32-arm64": ["@openai/codex@0.150.1-win32-arm64", "", { "os": "win32", "cpu": "arm64" }, "sha512-8I7N3ppKS1ql9GUr2RHS5Gw+KkRCKfIMIDDK/brxq6HizLgHoDK4CIR0OTvEgdX3Yh/2reBbxr9P4L1e+61e+Q=="], + "@openai/codex-win32-arm64": ["@openai/codex@0.151.0-win32-arm64", "", { "os": "win32", "cpu": "arm64" }, "sha512-zDWzOoh9wHm+Om1Nhn7os47rAVeSGPh0SnM3YOttdq6iPJz2zn4vBnbGUZjeih1qW/3mvNF3Oyd4owlaHmphmg=="], - "@openai/codex-win32-x64": ["@openai/codex@0.150.1-win32-x64", "", { "os": "win32", "cpu": "x64" }, "sha512-1P1tscFnw4A4ip/WN1R5WYPdfuAlxLctPpcE1QAPQnOtbkJR5NX8da4kNTQmiDN/Flx3TLJPDTRHP9Jn435eJg=="], + "@openai/codex-win32-x64": ["@openai/codex@0.151.0-win32-x64", "", { "os": "win32", "cpu": "x64" }, "sha512-sLT7xvID3jhU6tkzcwRPnMEclKRwUPbpo0mtfxIF9KpdZH3VJV7sM2/kXWXyvUM7Zt/YeyOaeATTEysbRz8Yog=="], "@opentelemetry/api": ["@opentelemetry/api@1.9.1", "", {}, "sha512-gLyJlPHPZYdAk1JENA9LeHejZe1Ti77/pTeFm/nMXmQH/HFZlcS/O2XJB+L8fkbrNSqhdtlvjBVjxwUYanNH5Q=="], @@ -880,47 +878,47 @@ "@orpc/standard-server-peer": ["@orpc/standard-server-peer@1.15.0", "", { "dependencies": { "@orpc/shared": "1.15.0", "@orpc/standard-server": "1.15.0" } }, "sha512-fsTN+FrdPkseVx99yRenGJYSp0xOrEk9fODk8oh3zzricNYzlZ8GGgCYtDNgt9vFIR/J05dc+Nk76KhcPBQpLw=="], - "@oxc-parser/binding-android-arm-eabi": ["@oxc-parser/binding-android-arm-eabi@0.143.0", "", { "os": "android", "cpu": "arm" }, "sha512-n9uozULWflPqBtdmI8lAabLqGKNgLVNN0ZH8HfgCwpKGNtzRzauB76jTiW/3YLkcA7N1zskpi9GdVnZuu1SAvg=="], + "@oxc-parser/binding-android-arm-eabi": ["@oxc-parser/binding-android-arm-eabi@0.147.0", "", { "os": "android", "cpu": "arm" }, "sha512-fOtoGvIoirkvxQVw9J1WJPxz571XPgLsPf9uhRD+PJteUnvrJHMDmK9pw2yZEGGyismtRoEsp+JcXUdF/JDMDw=="], - "@oxc-parser/binding-android-arm64": ["@oxc-parser/binding-android-arm64@0.143.0", "", { "os": "android", "cpu": "arm64" }, "sha512-9BbdjHETk6O3zH/DDid9IgBtF0GlpLabNKN231uraXpRDSfY+iiZxTP5bk1Z63GBownVdhdINFIeddmMz4MzpQ=="], + "@oxc-parser/binding-android-arm64": ["@oxc-parser/binding-android-arm64@0.147.0", "", { "os": "android", "cpu": "arm64" }, "sha512-emjQHOYJaomo4ykaXQ1EItunr/I94Nk01oqBmU4dSkKSTupIDx6OysVDf2e8Eytm77rb+4ZxzgElyWP7rcEX7A=="], - "@oxc-parser/binding-darwin-arm64": ["@oxc-parser/binding-darwin-arm64@0.143.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-gh+6ecoHUy4/sUcolBl/1qPXKBbYNxFY0Pk0ujgQvINTMSftJY7o4yb8gOkDJPeZeB8+a+u7xTe6umoP8N5HFA=="], + "@oxc-parser/binding-darwin-arm64": ["@oxc-parser/binding-darwin-arm64@0.147.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-kXvBPJL7RmDPJ2mze/vXPPVQimCDtFr9OFLjf7dyhV5Dx64cgcXh9KKrA1sMWvCObvJll9CZZUO0FBlFwD0l6A=="], - "@oxc-parser/binding-darwin-x64": ["@oxc-parser/binding-darwin-x64@0.143.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-qd1hl2d+lXgHv/VQ/M9qm8TrMC5T4RqDBwtOnl+1D0QMjwcz+8AaB4JSg8STgeag0GP6a6L74XEGAsrTSJWNzQ=="], + "@oxc-parser/binding-darwin-x64": ["@oxc-parser/binding-darwin-x64@0.147.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-mgFF8pLU6R64LbT27lSrtVRspVC/3IcZ0qyIikzmi78Y3Ik2OPnlAHHI0UEBRcC3qmNgtjaef7zkFt7/uPxIcw=="], - "@oxc-parser/binding-freebsd-x64": ["@oxc-parser/binding-freebsd-x64@0.143.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-M5XXcNa7aOqLPKTR41msfghKu2yQ4xWvCm11/gwU0JzOzHNk5sgW//rVEjJ+LO48+VDAMzXTSzurUVxIDKwozw=="], + "@oxc-parser/binding-freebsd-x64": ["@oxc-parser/binding-freebsd-x64@0.147.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-v38aiF11qufOTBcCAKL4skgQf0zJ4NEvRlivq7B5kHrlyvjCLjvNrMtNWDTz1SDUL6/xVsJRmLDxv2e+Cp4oWw=="], - "@oxc-parser/binding-linux-arm-gnueabihf": ["@oxc-parser/binding-linux-arm-gnueabihf@0.143.0", "", { "os": "linux", "cpu": "arm" }, "sha512-T/GXusuOkPNQhCQCSBbcU/N8j0rAypuDBl1IyFK+lyYT594XsVz80clPC/OtbSSpBGyJxj8uYEfctxVuxVYoww=="], + "@oxc-parser/binding-linux-arm-gnueabihf": ["@oxc-parser/binding-linux-arm-gnueabihf@0.147.0", "", { "os": "linux", "cpu": "arm" }, "sha512-AeIiBbwUaP0H1+4/qGW9l5qHecS/+XA5iMuieVcGb1T+tyc2dVGspFW13BWk/XrLsiGP/CiDJTJqAPLLCzZHkw=="], - "@oxc-parser/binding-linux-arm-musleabihf": ["@oxc-parser/binding-linux-arm-musleabihf@0.143.0", "", { "os": "linux", "cpu": "arm" }, "sha512-oKu4RcBlXSqo3OC62dp6YTnQaZIurNDpCX3BnAM3+bJxt7s8J2TJKMnC0UYer1qhlRaDCg6wkTaTw+2IlsZ12w=="], + "@oxc-parser/binding-linux-arm-musleabihf": ["@oxc-parser/binding-linux-arm-musleabihf@0.147.0", "", { "os": "linux", "cpu": "arm" }, "sha512-/41MKPW4RgPY4DJco0NCF0RYX3IMZaVlRNMNzvhaxRavc7tN3Txm+qllZbh0aMRs0VHdgUlbI8TcAOiTai4TKg=="], - "@oxc-parser/binding-linux-arm64-gnu": ["@oxc-parser/binding-linux-arm64-gnu@0.143.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-WJBbD186AZmMGaSIhlktC+rPl8L3peCTXAh88Ih9uEvK0en2mPojGyCGYiL6mHtV1RPV3JyfJW5t6n5hh0lXhA=="], + "@oxc-parser/binding-linux-arm64-gnu": ["@oxc-parser/binding-linux-arm64-gnu@0.147.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-bmpw/RPhVXgZbtb3xBDuwW5s8+LvZYdqcDSX/sP2ltL77aTio3DP/B5ZTwwgoJ6Mr9vJs4RrmgEKW9XkLNUU1g=="], - "@oxc-parser/binding-linux-arm64-musl": ["@oxc-parser/binding-linux-arm64-musl@0.143.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-t1AcYOwEzgceadT4v5e+vaCCb0AncCA3v5AyzfBAz/tMq11qzVccXKzNHtkWdjBsgvTKwRkaUF3QvT4kot8vcQ=="], + "@oxc-parser/binding-linux-arm64-musl": ["@oxc-parser/binding-linux-arm64-musl@0.147.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-gd7VX/FDVOw6mjQcu45iIcp4QkgybgJwh3a0OFG2NxmPCj628mQWD96QGu1kK8+mZF9qK4b/gIEyC63vQoB7+Q=="], - "@oxc-parser/binding-linux-ppc64-gnu": ["@oxc-parser/binding-linux-ppc64-gnu@0.143.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-RsnO/NoD8376LMJq8JS8TwI0ieNaFRTuNe2GVJntQg6gwZNMENZsEbknHdVwjpOmxdGLGodcwaGSbAeRr5Bgjw=="], + "@oxc-parser/binding-linux-ppc64-gnu": ["@oxc-parser/binding-linux-ppc64-gnu@0.147.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-HnAzcfki7dSUNHf510Q2NmbJlz8Ys7rn8l9l588Pkx0tYe1BHLZnmELIgqizJ4WPhHGSwN8Ce+B/menVxS3odA=="], - "@oxc-parser/binding-linux-riscv64-gnu": ["@oxc-parser/binding-linux-riscv64-gnu@0.143.0", "", { "os": "linux", "cpu": "none" }, "sha512-48fSVfR9TZi5CASZFyv0VC6z6BCoeihFsX031mAD/oSH7d9PYsPgIqza7d9mjP7Z2KTEpTFyH6SIu0Ui6R1vdg=="], + "@oxc-parser/binding-linux-riscv64-gnu": ["@oxc-parser/binding-linux-riscv64-gnu@0.147.0", "", { "os": "linux", "cpu": "none" }, "sha512-qlkOL6wT44U+fT5s/+sR6Shx0OdwvQF83JyIPZUxG/ovqZF5/7atOtjH+JPZ5/7ATQLbFBBSmghcy/+2NVB/ew=="], - "@oxc-parser/binding-linux-riscv64-musl": ["@oxc-parser/binding-linux-riscv64-musl@0.143.0", "", { "os": "linux", "cpu": "none" }, "sha512-T8CpdD+SfE01DnIOD4HpVxu0ZJOfMJ/VhCvikKfaXAxkZ+9veyLM/D2hpi7Y2hFUyPmVQO3FNZHmYzV/WlVR4g=="], + "@oxc-parser/binding-linux-riscv64-musl": ["@oxc-parser/binding-linux-riscv64-musl@0.147.0", "", { "os": "linux", "cpu": "none" }, "sha512-DlefD7L7sMXs/3hIBH23Egk0phj8kG0SA81dVGOQ3S1ekjOlmTLH2E+F2Thwfh1slKx6aH+lNc5fQYAw0GU7/g=="], - "@oxc-parser/binding-linux-s390x-gnu": ["@oxc-parser/binding-linux-s390x-gnu@0.143.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-QLdeMsCcacenPEFsfxnBUDF1y6opyz5+fmOz9bfD5Y7fiGCMupUCuB3KTPQhNwshIG1P9fPqar9MHxuBDd4bwQ=="], + "@oxc-parser/binding-linux-s390x-gnu": ["@oxc-parser/binding-linux-s390x-gnu@0.147.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-Xqpagk/031IvZ4svrk2FF01YEqM/iN3MJV3SVZadKg/CsGlDCGoREqKHXYnoV5+8SfGe/m6RM1szXFLusTu/Uw=="], - "@oxc-parser/binding-linux-x64-gnu": ["@oxc-parser/binding-linux-x64-gnu@0.143.0", "", { "os": "linux", "cpu": "x64" }, "sha512-659ujfqLy6k7cuH3sbzhd8b+ztSq+i6E2E9pG78Q0BmHjAExfGIdgc8cGgMdwAozDXeZFHkJ+LXYJdWsaGdgyw=="], + "@oxc-parser/binding-linux-x64-gnu": ["@oxc-parser/binding-linux-x64-gnu@0.147.0", "", { "os": "linux", "cpu": "x64" }, "sha512-QioQOeUbI4ATUr0S2z88uA3Cds2R3Mm5Ge7U8XNYtlTb2GJF3rWlcj70z0AJhhOlbdm0YgVjqPBldUNbFylDIg=="], - "@oxc-parser/binding-linux-x64-musl": ["@oxc-parser/binding-linux-x64-musl@0.143.0", "", { "os": "linux", "cpu": "x64" }, "sha512-/Mw/9j4TfZcnKphPrzOE6t4MMknXadcAAuVUlDRTF/ETWB5xOgQvOJV2Mh9We/bWxZdoxaGAdc+hy4GuYwQ2yQ=="], + "@oxc-parser/binding-linux-x64-musl": ["@oxc-parser/binding-linux-x64-musl@0.147.0", "", { "os": "linux", "cpu": "x64" }, "sha512-NXy1tv/OdC+pPTwf9RiCZWPK53V/Xq/2cjSnjOSyKopajdaDqMIkgtDY+jXZemp2e8px5FeWfY2L2LwhKZQovg=="], - "@oxc-parser/binding-openharmony-arm64": ["@oxc-parser/binding-openharmony-arm64@0.143.0", "", { "os": "none", "cpu": "arm64" }, "sha512-8rIKWR2BFuifbIK/1XB9wTaSdtuJ25dlE7ZQYDnEwj/2xH2vHsxnvIjHT3ZjSVuLLwGGlSslIG/fbOJ8TV8rTw=="], + "@oxc-parser/binding-openharmony-arm64": ["@oxc-parser/binding-openharmony-arm64@0.147.0", "", { "os": "none", "cpu": "arm64" }, "sha512-GpGWZ6oKz4bjCWW9Mz5pCaGPyk2Aaze6zEoaslIQqpSLtpx5pXj/ap5gUNb5Jn2LIbqWyjkGLX9yv3NMuNcBVQ=="], - "@oxc-parser/binding-win32-arm64-msvc": ["@oxc-parser/binding-win32-arm64-msvc@0.143.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-5U9kQYMfRRI6Zq7KDxgbIP0RMnKrfn3gLepRMgJuRkPSUALTiRCk9d/uyhb4lGDjUdzwK7mBkKqhLgzBPCmLpQ=="], + "@oxc-parser/binding-win32-arm64-msvc": ["@oxc-parser/binding-win32-arm64-msvc@0.147.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-a8mlt7CC8z7LUdCfaxhff4kCd+vSjE+NEFL0cxA8ukfuSnvAto/pWTjytW4BuVLnQGcCVdHJwcRKOfs++H+tjw=="], - "@oxc-parser/binding-win32-ia32-msvc": ["@oxc-parser/binding-win32-ia32-msvc@0.143.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-25P7AaHk4R88Yv2XH4gToDVmh0cOu+bEURQU10CRrmvgabfRArSGAP5osmwUKeSUHj0VS50upbpbRWWW/m7mHA=="], + "@oxc-parser/binding-win32-ia32-msvc": ["@oxc-parser/binding-win32-ia32-msvc@0.147.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-M5ViVDBcFLnl2632AuuWuP35zEL5oikK1jTx8r3+902VEDeSNHxFZAB6RZyfZ7MU6Oi5QTOG8MmMkIeHsudSaw=="], - "@oxc-parser/binding-win32-x64-msvc": ["@oxc-parser/binding-win32-x64-msvc@0.143.0", "", { "os": "win32", "cpu": "x64" }, "sha512-ORMh3JE1s6V7ySicdRK7vgaDQnn5o+UHg9ct989PlWHbel8O9ARrmWXM6kZjrBMtNucxNayQ8g69G0VfWzhANw=="], + "@oxc-parser/binding-win32-x64-msvc": ["@oxc-parser/binding-win32-x64-msvc@0.147.0", "", { "os": "win32", "cpu": "x64" }, "sha512-DUaE13OwnUSlHpLZNcC/nuT10ivlWqc5EZgsfgXuAmWYw0r3nDxGeLD1zlGwYwIgVk1/ZMAxoXpV+05stvbHaA=="], - "@oxc-project/runtime": ["@oxc-project/runtime@0.133.0", "", {}, "sha512-PkvjA1Lq5++V5S1E6Patr92ZVcieE6EalDr1VJTqv4BnjZdOUC4W3p8k1wMXSd5/2aFP4b/A6N5sg2Bkzcr9vQ=="], + "@oxc-project/runtime": ["@oxc-project/runtime@0.146.0", "", {}, "sha512-lbXHIpZ1MmK6zuw5txlMdIZ2waLVUIU5Gnm3sEuwJOiqDfQfbtjeHscatmeBoxbv8+If9LFM6PGh/3DcDWYIYw=="], - "@oxc-project/types": ["@oxc-project/types@0.133.0", "", {}, "sha512-KzkdCd6Uxqnf6l3HOw1xfatAlUURA0g14cvBYFyJ5SaNOQbOUvBr9PKArcPcrNIeRsBdgcUzOGrhKveVpvOIGA=="], + "@oxc-project/types": ["@oxc-project/types@0.146.0", "", {}, "sha512-XC0QsnnhVe7sLIWmYmdPw7x5P0h4W8vUU3Nv1ySgWXtvCz8NizoAEpGXA0sOYoJQV2Rl13LgURAHQ5cI5ILCSA=="], "@oxc-resolver/binding-android-arm-eabi": ["@oxc-resolver/binding-android-arm-eabi@11.24.2", "", { "os": "android", "cpu": "arm" }, "sha512-y09e0L0SRI2OA2tUIrjBgoV3eH5hvUKXNkJqXmNo5V2WxIjyC7I7aJfRLMEVpA8yi95f90gFDvO0VMgrDw+vwA=="], @@ -960,55 +958,55 @@ "@oxc-resolver/binding-win32-x64-msvc": ["@oxc-resolver/binding-win32-x64-msvc@11.24.2", "", { "os": "win32", "cpu": "x64" }, "sha512-UqGPmo56KDfLlfXFAFIrNflHT8tFxWGEivWg3Zeyp4Uy2NlKN1FGPr6/BxcLGG3+kZ6Wp14g5Uj+n71boqZfiw=="], - "@oxfmt/binding-android-arm-eabi": ["@oxfmt/binding-android-arm-eabi@0.52.0", "", { "os": "android", "cpu": "arm" }, "sha512-17EMSJnQ9g+upVHrAUYDMfH5lvRKQ9Nvg8WtEoH72oDr1VpWz+7/o3tD97U1EToen2YAQ/68JmtDYkQUi20dfQ=="], + "@oxfmt/binding-android-arm-eabi": ["@oxfmt/binding-android-arm-eabi@0.64.0", "", { "os": "android", "cpu": "arm" }, "sha512-o6uzh/jTOQeAY5TdkAeXdqv7MBRcPxiRA08zrcBtkKj5cSu/FMu0Hl7Q6Fi1KCKyCWZ6lJVjBzdsJvsKltUsGQ=="], - "@oxfmt/binding-android-arm64": ["@oxfmt/binding-android-arm64@0.52.0", "", { "os": "android", "cpu": "arm64" }, "sha512-A2G1IdwGEW2lLJkIxcvuirRH1CzSl/e0NX11zTlW1gvxJThfwbI/BEoaKrTNpm7M2FchvIf6guvIQU7d5iz+OQ=="], + "@oxfmt/binding-android-arm64": ["@oxfmt/binding-android-arm64@0.64.0", "", { "os": "android", "cpu": "arm64" }, "sha512-jRGSUeeP7p3Gynw2YaCVtjBIA6ZxY6bEB/ES5i54OhqmRTyuVg7ZgstEtzgq6GOAJd+2QZ5pvf+bFfmW5Mp9cw=="], - "@oxfmt/binding-darwin-arm64": ["@oxfmt/binding-darwin-arm64@0.52.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-f9+bLvOYxy7NttCLFTvQ7afmqDOWY4wIP9xdvfj5trQ1qj6f2UFAGwZESlfsMjvJNTyRpXfIlOanCI9FOvoeQA=="], + "@oxfmt/binding-darwin-arm64": ["@oxfmt/binding-darwin-arm64@0.64.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-JINwtU2lW7nOFSqi+H2qplipNUqah9Gc1jgGmB82kTD4UnZrZIVxCJ9qEmFiKfjNq27gYLFhrUb0to86aCwMjw=="], - "@oxfmt/binding-darwin-x64": ["@oxfmt/binding-darwin-x64@0.52.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-YSTB9sJ5nnQd/Q0ddHkgof0ZCHPAnWZT1IW2SJ8omz7CP7KluJhO1fNHrpqdxCtpztJwSs4hY1uAee35wKxxaw=="], + "@oxfmt/binding-darwin-x64": ["@oxfmt/binding-darwin-x64@0.64.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-gCmuswrgrOSajV4HCRFkVCGIruPq8bjYuPYgSE2WQB3mD6XrdyZ3JMSRZCkQ8zCxOyGWriBo6QoZ5nmMHQ1BfA=="], - "@oxfmt/binding-freebsd-x64": ["@oxfmt/binding-freebsd-x64@0.52.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-NIrRNTTPCs4UbmVs0bxLSCDlLCtIRMJIXklNKaXa5Oj2/K1UIMBvgE8+uPVo01Io3N9HF0+GAX+aAHjUgZS7vA=="], + "@oxfmt/binding-freebsd-x64": ["@oxfmt/binding-freebsd-x64@0.64.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-Ab8g7a38pT0MMImjh7anRSTve6buWBIlcXIFBYa5xl4s6UxEgKSc2xOOhbGtLwvXnEi2PsEDGoJh3oUU7xkehQ=="], - "@oxfmt/binding-linux-arm-gnueabihf": ["@oxfmt/binding-linux-arm-gnueabihf@0.52.0", "", { "os": "linux", "cpu": "arm" }, "sha512-JXUCde8mn3GpgQouz2PXUokgy/uT1QrRJBL2s983VWcSQp62wTFYiNXgTKdeo1Jgbr0IgUnKKvzIk/YBlj/nVQ=="], + "@oxfmt/binding-linux-arm-gnueabihf": ["@oxfmt/binding-linux-arm-gnueabihf@0.64.0", "", { "os": "linux", "cpu": "arm" }, "sha512-BgvS3CoQ+Xy2deoZqEN8JVKabcCZi2RxA3yant8G9OAv9KuPJ9TCjHkqigzdHUVwErZxEP5d2bzLIEyKYyBDLg=="], - "@oxfmt/binding-linux-arm-musleabihf": ["@oxfmt/binding-linux-arm-musleabihf@0.52.0", "", { "os": "linux", "cpu": "arm" }, "sha512-psbUXaRZ+V8DaXz10Qf7LSHtdtdKAmC8fxXgeU608jjzrmWK4quamZMOpl6sf+dikoFHA85uE93Q0BqxrCdQrQ=="], + "@oxfmt/binding-linux-arm-musleabihf": ["@oxfmt/binding-linux-arm-musleabihf@0.64.0", "", { "os": "linux", "cpu": "arm" }, "sha512-QXpNxwoMj0YvnceCNZadNSden3bIcnvjn/sDp/rwZhRoZoZYGpHvtPyhGsdJz9uvT9GkaMW7SsLddurU56dt8w=="], - "@oxfmt/binding-linux-arm64-gnu": ["@oxfmt/binding-linux-arm64-gnu@0.52.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-Jw7MgWUU9lcLCcy82updISP3EthTlfvAwR6gWNxPzqly7+fLvOi2gHQE9xXQjpqaVLm/8P+gOzlv9ODuoVlaaw=="], + "@oxfmt/binding-linux-arm64-gnu": ["@oxfmt/binding-linux-arm64-gnu@0.64.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-BBgH3I1ppDsI5pZ4Pdhw0ceYxwVCfbU/bZEBCeZ6caRS9x0ZabErxubP7riGUn11PXZBhe8DYdjkDKP1FlVQ5w=="], - "@oxfmt/binding-linux-arm64-musl": ["@oxfmt/binding-linux-arm64-musl@0.52.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-wZg6bLjDvh2KibyI3QFUYo8GTXneIFsd0JvehtvJiUmQ8WRPERgxd/VM4ctWb86U5FT1FkqgS8/wZKVB+AZScg=="], + "@oxfmt/binding-linux-arm64-musl": ["@oxfmt/binding-linux-arm64-musl@0.64.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-v19HSjC/BGXdt26qEvKZtwAHgGmQ2Agcap2kQP+KIqoRZqivVzYth3ui2dJA1i+6/fjpjga85lIOaJJjQ/bOOw=="], - "@oxfmt/binding-linux-ppc64-gnu": ["@oxfmt/binding-linux-ppc64-gnu@0.52.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-IngE8uxhNvxcMrLjZNDo9xNLY7rEK33AKnaMd2B46he1e/mz2CfcW6If/U1wUjdRZddm1QzQaciqZkuMkdh1FA=="], + "@oxfmt/binding-linux-ppc64-gnu": ["@oxfmt/binding-linux-ppc64-gnu@0.64.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-PElLnOo4xFTBZrxPhgTIj0eHqZXwEBQoNWtb7facUV170T0B0FRET0iNbb3LUeLWTybkUW+vsdyv4ihOdyXGyw=="], - "@oxfmt/binding-linux-riscv64-gnu": ["@oxfmt/binding-linux-riscv64-gnu@0.52.0", "", { "os": "linux", "cpu": "none" }, "sha512-H3+DdFMv/efN3Efmhsv18jDrpiWWqKG7wsfAlQBqAt6z/E2Bx+TwEj2Nowe51CPOWB8/mFBC2dAMSgVFLvvowA=="], + "@oxfmt/binding-linux-riscv64-gnu": ["@oxfmt/binding-linux-riscv64-gnu@0.64.0", "", { "os": "linux", "cpu": "none" }, "sha512-Qzsg15n4F5CH+MorcRW4MkAEMiLzXmeG+DiDSbP/bBTqCmWOH3K9DHryNrve+JHlV0txS+B6Z9P5Xz+cmWeL+g=="], - "@oxfmt/binding-linux-riscv64-musl": ["@oxfmt/binding-linux-riscv64-musl@0.52.0", "", { "os": "linux", "cpu": "none" }, "sha512-zji+1kb7lJKohSDjzC1IsS+K/cKRs1hdVf0ZH0VbdbiakmtLvN9twBoXo/k8VdjFax7kfo+DyPxS7vv52br1aw=="], + "@oxfmt/binding-linux-riscv64-musl": ["@oxfmt/binding-linux-riscv64-musl@0.64.0", "", { "os": "linux", "cpu": "none" }, "sha512-/GZ358wnQ/Ez4UVnCcZIi56JkY0sOdZ+B108pqXKqZz3jLS59F4KEAB1Qv3fRlObrFEk+3L2vUQ/xoPx+3vjXw=="], - "@oxfmt/binding-linux-s390x-gnu": ["@oxfmt/binding-linux-s390x-gnu@0.52.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-hcLBYedpCy7ToUvvBidWk7+11Yhg1oAZ4+6hKPic/mQI6NaqXJSXMps5nFlwUuX2ewhtLZZDPg63TI042qGKBg=="], + "@oxfmt/binding-linux-s390x-gnu": ["@oxfmt/binding-linux-s390x-gnu@0.64.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-/C9We3DXegowfLXtVCYHeNiU9azwCDr5cQkEtCVlc74vyn+lLQSPApJ1CZmxAduqeq/Oi3gQ+IVptyhCaTMtkQ=="], - "@oxfmt/binding-linux-x64-gnu": ["@oxfmt/binding-linux-x64-gnu@0.52.0", "", { "os": "linux", "cpu": "x64" }, "sha512-IDO2loXK2OtTOhSPchU9MW25mWL2QCDGdJbjN8MXKZVS80qXe5gMTwQWu/gMJ3juoBHbkuUZNB2N1LHzNT7DoA=="], + "@oxfmt/binding-linux-x64-gnu": ["@oxfmt/binding-linux-x64-gnu@0.64.0", "", { "os": "linux", "cpu": "x64" }, "sha512-91KM2CeRWscIEHlj1NsW2WSnzGeq1Ehq+39bfDowTdkn+fcvK/x4Y1RcyqT7glyBjZio0ldkeCG6Usj3v7ASog=="], - "@oxfmt/binding-linux-x64-musl": ["@oxfmt/binding-linux-x64-musl@0.52.0", "", { "os": "linux", "cpu": "x64" }, "sha512-mAV2Hjn0SatJ+KoAzKUC3eJhdJ8wv+3m1KyuS0dTsbF0c5weq+QrCt/DRZZM+uj/XiKzCDEUKYsBF30e2qkcyw=="], + "@oxfmt/binding-linux-x64-musl": ["@oxfmt/binding-linux-x64-musl@0.64.0", "", { "os": "linux", "cpu": "x64" }, "sha512-gw7uEk9I+7zoT1EYLra1eWArIzNcz8e3jkv+Noo2+o2T7wPvsNSQbfoa4DSfZlvn1i6mJ05RiZ4/omaXPDNhQg=="], - "@oxfmt/binding-openharmony-arm64": ["@oxfmt/binding-openharmony-arm64@0.52.0", "", { "os": "none", "cpu": "arm64" }, "sha512-vd4npaUIwChxp7XzkqmepBWTT9YMcSe/NBApVGPC30/lLyOVaV3dvma1SKo03t8O73BPRAG7EyJzGlN5cJM5hQ=="], + "@oxfmt/binding-openharmony-arm64": ["@oxfmt/binding-openharmony-arm64@0.64.0", "", { "os": "none", "cpu": "arm64" }, "sha512-HYHFf616FHSPSO07c09mjmXBfQ73wIVM3m0txOiooa5XZkGoxFd6B14PVj0LB0DXIqJ6wAO/dDR/NX/5UUaqnw=="], - "@oxfmt/binding-win32-arm64-msvc": ["@oxfmt/binding-win32-arm64-msvc@0.52.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-k2sz6gWQdMfh5HPpIS+Bw/0UEV/kaK2xuqJRrWL233sEHx9WLlsmvlPFM4HUNThkYbSN0U0vPW7LVKZWDS8hPQ=="], + "@oxfmt/binding-win32-arm64-msvc": ["@oxfmt/binding-win32-arm64-msvc@0.64.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-uQjFp081IZSWD6VAofX2iO2z01awAdHmfC+NrieWIPKrT2hZKQDyq/U18M7ifC0sm0Wz8aHY/p6+FDYIzs/CrQ=="], - "@oxfmt/binding-win32-ia32-msvc": ["@oxfmt/binding-win32-ia32-msvc@0.52.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-rhke69GTcArodLHpjMTfNnvjTEBryDeZcUCKK/VjXDMtfTULl6QRh0ymX5/hbCUv2WjYm9h/QbW++q2vE15gWQ=="], + "@oxfmt/binding-win32-ia32-msvc": ["@oxfmt/binding-win32-ia32-msvc@0.64.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-lNM6byTAQ881jugzFu8juJTbNRgsUTlswMA6pJmwi1XDvmIqnnb49lcUAs5gz94fCJLrVN+/X3s3jOKqx23WIQ=="], - "@oxfmt/binding-win32-x64-msvc": ["@oxfmt/binding-win32-x64-msvc@0.52.0", "", { "os": "win32", "cpu": "x64" }, "sha512-q5xL7oeXkZdEtNZWBdvehJcmt+GRu9l2bK40yJs1jJXlqq+r0Hygb1rTjq+FM2o/2xyt4cufH6KRplHp3Jjsvw=="], + "@oxfmt/binding-win32-x64-msvc": ["@oxfmt/binding-win32-x64-msvc@0.64.0", "", { "os": "win32", "cpu": "x64" }, "sha512-BtmbtL/QjMtF1a6C3CqoDluH2IfB6fJt62E+B9RFfUPtFk4Iz9PFS6+y/SzzOvSxc7aUk2Kphwg7Dh8lMbwu6g=="], - "@oxlint-tsgolint/darwin-arm64": ["@oxlint-tsgolint/darwin-arm64@0.23.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-gOs9PVr2wEg4ox9z0aJo+RKhhImW86YL5N6yav8BK/rgPsIrwN/igSZ+pbRr723NFvUNKde9fgMhRA6JrXAOZw=="], + "@oxlint-tsgolint/darwin-arm64": ["@oxlint-tsgolint/darwin-arm64@7.0.2001", "", { "os": "darwin", "cpu": "arm64" }, "sha512-CUJEdbSZ54+Xy9OXqOhWLTKZKV0BBiV7C2i/ygyVmXtkUNXx5YCzN8DpSSshTAKktoL7S+tnQ/ftFG/i7X896w=="], - "@oxlint-tsgolint/darwin-x64": ["@oxlint-tsgolint/darwin-x64@0.23.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-kjJ8B+7n4tB9VJdxS5A9GdJt6/bYpzbu4lXp2uO1S3sRmCB5gDEABlGoiePNApRWaW+xqL4b4xgiE727jSLhuA=="], + "@oxlint-tsgolint/darwin-x64": ["@oxlint-tsgolint/darwin-x64@7.0.2001", "", { "os": "darwin", "cpu": "x64" }, "sha512-pXfBb5BqONCcgrXQNUZWXgiYmRSWJzd97S8i41VVOh6ut0tyo+cJ5FKFpczDHxiVNfj/3e7c9B4MtztNdpIVCw=="], - "@oxlint-tsgolint/linux-arm64": ["@oxlint-tsgolint/linux-arm64@0.23.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-6dCZuKNu135seMXilkRk9SpCx6i1XgmiipYGalLij5WVRX6ZYS8c4xI7preN/zv9fCXhsQclTIMDu2Y/cytTjw=="], + "@oxlint-tsgolint/linux-arm64": ["@oxlint-tsgolint/linux-arm64@7.0.2001", "", { "os": "linux", "cpu": "arm64" }, "sha512-roP7zujb/QDPzDwEKsFFpzNHHy91/Y7oX9vQXk78ekyZtcQj1QXDIMH33gjDdHBfRl4K9pZ36xhRgrP4Zr+R8A=="], - "@oxlint-tsgolint/linux-x64": ["@oxlint-tsgolint/linux-x64@0.23.0", "", { "os": "linux", "cpu": "x64" }, "sha512-3bdilnyA7kmSTjK27rvjIjSxL5SIg3wt7vwNiRkouWB83ytssyKnuGvxSYJxgMEmFpSutzaBzcCUM2jDtPGcgA=="], + "@oxlint-tsgolint/linux-x64": ["@oxlint-tsgolint/linux-x64@7.0.2001", "", { "os": "linux", "cpu": "x64" }, "sha512-UDezNqdECVmngu2TPnjaS1YoAmcTaBoI5lV9vk3VahBxoi+I5r9k3iJTT7qZoYWOXTD/7T7bNcwRgrocR6BscQ=="], - "@oxlint-tsgolint/win32-arm64": ["@oxlint-tsgolint/win32-arm64@0.23.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-j+OEp44SVYiQ+ZD+uttsX7u6L9SvmbbQ77SO1pSFCcJlsVMeCk8qZsjhKfGKuT/jIA+ipOJMVs/+pqUfObBWNw=="], + "@oxlint-tsgolint/win32-arm64": ["@oxlint-tsgolint/win32-arm64@7.0.2001", "", { "os": "win32", "cpu": "arm64" }, "sha512-uJZhqB6pdXLuN+AD1F5082byyQti/NPmJA77GtcFlmT2HzRelqbNls3SaIqxpjdFgvSBF9g0yOKGBkGFg7kX8Q=="], - "@oxlint-tsgolint/win32-x64": ["@oxlint-tsgolint/win32-x64@0.23.0", "", { "os": "win32", "cpu": "x64" }, "sha512-5MyjFuqf+g8OUPJBSGWHJtmoWnzFJYyOg4To9WMQshZYEWig/vtu7JtJ03VWnzHv9LJkAUeApY0gVCOywFR/iQ=="], + "@oxlint-tsgolint/win32-x64": ["@oxlint-tsgolint/win32-x64@7.0.2001", "", { "os": "win32", "cpu": "x64" }, "sha512-FkDRm8hx9OwzGQqyWG1tO5QrTLRApff9DzSgpz9QZau37BR8d1VYKOxMLGf6shPZntJFoTwIIJYT68VndYDCog=="], "@oxlint/binding-android-arm-eabi": ["@oxlint/binding-android-arm-eabi@1.77.0", "", { "os": "android", "cpu": "arm" }, "sha512-E06sKWS6PiI6HRxS1wyQg22HvApt01hI7fV+T3wUk3OSbaaP4a3hYGY/MIQDmASqCiRjBdpRQYkgMkqH82cWmQ=="], @@ -1048,7 +1046,7 @@ "@oxlint/binding-win32-x64-msvc": ["@oxlint/binding-win32-x64-msvc@1.77.0", "", { "os": "win32", "cpu": "x64" }, "sha512-+teyvPDZ2RjUvo+SuCqS/UhaJl1QtdW5fWT5NJTV61V5MIuIS90Db9LixmtEGvXixyttiK62P96MSu3UlpviBw=="], - "@oxlint/plugins": ["@oxlint/plugins@1.61.0", "", {}, "sha512-nkOyZEF1vH527CkdQtOp1HMrVFEM4ResURvI2JFeGoup+h+43J/k/FgdOR9b9Isxg+Yae7qVDa7y3nssE8b3TQ=="], + "@oxlint/plugins": ["@oxlint/plugins@1.79.0", "", {}, "sha512-S0uyoxakDINJ4DPgqxGlEEvrdSMeQb7Z2lKVjxoY2gwsbZbfg2Xr8Klfeo5ZeraHmmdBCELFUHkSe6KEmBpMvg=="], "@playwright/test": ["@playwright/test@1.62.1", "", { "dependencies": { "playwright": "1.62.1" }, "bin": { "playwright": "cli.js" } }, "sha512-DTcUc8qii+cpHvtOwggMtBRMjKZHXYWdw8syRYu2vtzuq4Wxphqq4NfCs5Zt44L6mA8rfDfj+PHnxFc/FeK6mQ=="], @@ -1440,25 +1438,23 @@ "@vitest/utils": ["@vitest/utils@4.1.11", "", { "dependencies": { "@vitest/pretty-format": "4.1.11", "convert-source-map": "^2.0.0", "tinyrainbow": "^3.1.0" } }, "sha512-zTCVGpyFsGWBhllOyKlTw/vnr6D9qxsfSDyfbyZmTyjHw5N/VuvzHpHoQjm2ZJzn4RJgx5w4r7V0er69CmLgPQ=="], - "@voidzero-dev/vite-plus-core": ["@voidzero-dev/vite-plus-core@0.1.24", "", { "dependencies": { "@oxc-project/runtime": "=0.133.0", "@oxc-project/types": "=0.133.0", "lightningcss": "^1.30.2", "postcss": "^8.5.6" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@arethetypeswrong/core": "^0.18.1", "@tsdown/css": "0.22.1", "@tsdown/exe": "0.22.1", "@types/node": "^20.19.0 || >=22.12.0", "@vitejs/devtools": "^0.1.18", "esbuild": "^0.27.0 || ^0.28.0", "jiti": ">=1.21.0", "less": "^4.0.0", "publint": "^0.3.8", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "typescript": "^5.0.0 || ^6.0.0", "unplugin-unused": "^0.5.0", "unrun": "*", "yaml": "^2.4.2" }, "optionalPeers": ["@arethetypeswrong/core", "@tsdown/css", "@tsdown/exe", "@types/node", "@vitejs/devtools", "esbuild", "jiti", "less", "publint", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "typescript", "unplugin-unused", "unrun", "yaml"] }, "sha512-iXPGBABnQnrDMx89H6MOCGcTZp+QW+3rY4YMVKdE6ydchSvPk2O3MI2vgaRVfOtWJ2IjnxSnf1n2yjP67ZBRFQ=="], + "@voidzero-dev/vite-plus-core": ["@voidzero-dev/vite-plus-core@0.3.0", "", { "dependencies": { "@oxc-project/runtime": "=0.146.0", "@oxc-project/types": "=0.146.0", "lightningcss": "^1.33.0", "postcss": "^8.5.6", "yuku-codegen": "^0.5.44", "yuku-parser": "^0.5.44" }, "optionalDependencies": { "@voidzero-dev/vite-plus-darwin-arm64": "0.3.0", "@voidzero-dev/vite-plus-darwin-x64": "0.3.0", "@voidzero-dev/vite-plus-linux-arm64-gnu": "0.3.0", "@voidzero-dev/vite-plus-linux-arm64-musl": "0.3.0", "@voidzero-dev/vite-plus-linux-x64-gnu": "0.3.0", "@voidzero-dev/vite-plus-linux-x64-musl": "0.3.0", "@voidzero-dev/vite-plus-win32-arm64-msvc": "0.3.0", "@voidzero-dev/vite-plus-win32-x64-msvc": "0.3.0", "fsevents": "~2.3.3" }, "peerDependencies": { "@arethetypeswrong/core": "^0.18.1", "@types/node": "^20.19.0 || >=22.12.0", "@vitejs/devtools": "^0.4.0 || ^0.5.0", "esbuild": "^0.27.0 || ^0.28.0", "jiti": ">=1.21.0", "less": "^4.0.0", "publint": "^0.3.8", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "typescript": "^5.0.0 || ^6.0.0 || ^7.0.0", "unplugin-unused": "^0.5.0", "unrun": "*", "yaml": "^2.4.2" }, "optionalPeers": ["@arethetypeswrong/core", "@types/node", "@vitejs/devtools", "esbuild", "jiti", "less", "publint", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "typescript", "unplugin-unused", "unrun", "yaml"] }, "sha512-aOqoqIWaF+Q/geDU48pC2rVFEVSvLV1GGj/NdvhUiBhCZntoFNbwI+hjUeG8BMaPG67sOV6ey+/sgkdmGmKqaw=="], - "@voidzero-dev/vite-plus-darwin-arm64": ["@voidzero-dev/vite-plus-darwin-arm64@0.1.24", "", { "os": "darwin", "cpu": "arm64" }, "sha512-Hpo9W9piSFlEsJzGkwzfDXhJGrnYByxHXF7NVQZ7g+SLOprddtlfTeM8t+gq9dxcuq0RzM8ddMAhDQP/K3fZQA=="], + "@voidzero-dev/vite-plus-darwin-arm64": ["@voidzero-dev/vite-plus-darwin-arm64@0.3.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-9ADr1egZ8T4tJOqrpQLhoDl95Y74R95+bsvjmin0gy1C0eQVhpmcNnBfb07KFNhJioJp9MMO7F7Dx4fQL5SKsw=="], - "@voidzero-dev/vite-plus-darwin-x64": ["@voidzero-dev/vite-plus-darwin-x64@0.1.24", "", { "os": "darwin", "cpu": "x64" }, "sha512-SwnnnZrEFBiU5iKlh/CZAVwn0RFt/Udrvt3kFLtdRxMtN5bKaqTFVA2H8Y/FPCWp1QX9bs4V9ZIAeXAk06zLkw=="], + "@voidzero-dev/vite-plus-darwin-x64": ["@voidzero-dev/vite-plus-darwin-x64@0.3.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-GegasVCwNeDOkNyvhLOuwU1+T2JkjY/Tq+SOvwphUpVcqQ6OOAUq9LlpoXviO2QL/Kq2NbMYjiAfPKVSTLUFQw=="], - "@voidzero-dev/vite-plus-linux-arm64-gnu": ["@voidzero-dev/vite-plus-linux-arm64-gnu@0.1.24", "", { "os": "linux", "cpu": "arm64" }, "sha512-ImM3eqDki4DpRuHjW6dEh4St8zvbcfOMR7KQZJX42ArriCLQ/QdaYhDRRbcDi27XsOBqRxm2eqUUEymPrYIHpA=="], + "@voidzero-dev/vite-plus-linux-arm64-gnu": ["@voidzero-dev/vite-plus-linux-arm64-gnu@0.3.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-nYI3KNYXkXjRPsSdR4Lr7J2xMxfR1+TplWlG/dV37qVXWAjbyHpoAlbULjZBAVJMyXRNlcADhBrEwXe4g6s48A=="], - "@voidzero-dev/vite-plus-linux-arm64-musl": ["@voidzero-dev/vite-plus-linux-arm64-musl@0.1.24", "", { "os": "linux", "cpu": "arm64" }, "sha512-gj4mzbob/ls8Zs7iTuF9Gr0EFFF7tdpDiPxDPBkH8tJP5OkHABlzWUwJhU+9xxcUbTaXqpHDw68Mil7jm5dpMg=="], + "@voidzero-dev/vite-plus-linux-arm64-musl": ["@voidzero-dev/vite-plus-linux-arm64-musl@0.3.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-HRlVA3AOcuGXmOdHhQ+Zv5XAaKbYF9si5rRHoOsKl0UyBo4txA3OoJfmP0WjanfLUNmu85JyO2dO1ptL4C6wgg=="], - "@voidzero-dev/vite-plus-linux-x64-gnu": ["@voidzero-dev/vite-plus-linux-x64-gnu@0.1.24", "", { "os": "linux", "cpu": "x64" }, "sha512-x7IYK7lI+WuF1n3jSzEYU6FgJxPX/R0rDmTTsOutooGGCU7uShZvfZqIoiTXK0eFnJU5ij5BfBgenenUfsaT/A=="], + "@voidzero-dev/vite-plus-linux-x64-gnu": ["@voidzero-dev/vite-plus-linux-x64-gnu@0.3.0", "", { "os": "linux", "cpu": "x64" }, "sha512-9A+dFScPfwcrzF/rRR0zH8++2hOf6xtFmN/5LyzyfUywtw9MILXcC72IMcOeL6QRJwKUMsudi1rFeDE59azNvw=="], - "@voidzero-dev/vite-plus-linux-x64-musl": ["@voidzero-dev/vite-plus-linux-x64-musl@0.1.24", "", { "os": "linux", "cpu": "x64" }, "sha512-JCy2w0eSVUlWQlggK5T47MnL+j0o4EY7hLskINVI8gi+aixQF4xnYBDobz0lbxkqz3/IfiLyXUx6TcU3thcsGQ=="], + "@voidzero-dev/vite-plus-linux-x64-musl": ["@voidzero-dev/vite-plus-linux-x64-musl@0.3.0", "", { "os": "linux", "cpu": "x64" }, "sha512-KfIV3qaPdaOOE8JQMRHRE34FtZocl9O86XLTP6JMjDUlcx8FPgf8/fz/HFqJ8g232vM+JsgLI/YTVeXP8LkTKw=="], - "@voidzero-dev/vite-plus-test": ["@voidzero-dev/vite-plus-test@0.1.24", "", { "dependencies": { "@standard-schema/spec": "^1.1.0", "@types/chai": "^5.2.2", "@voidzero-dev/vite-plus-core": "0.1.24", "es-module-lexer": "^1.7.0", "obug": "^2.1.1", "pixelmatch": "^7.1.0", "pngjs": "^7.0.0", "sirv": "^3.0.2", "std-env": "^4.0.0", "tinybench": "^2.9.0", "tinyexec": "^1.0.2", "tinyglobby": "^0.2.15", "ws": "^8.18.3" }, "peerDependencies": { "@edge-runtime/vm": "*", "@opentelemetry/api": "^1.9.0", "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", "@vitest/coverage-istanbul": "4.1.8", "@vitest/coverage-v8": "4.1.8", "@vitest/ui": "4.1.8", "happy-dom": "*", "jsdom": "*", "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" }, "optionalPeers": ["@edge-runtime/vm", "@opentelemetry/api", "@types/node", "@vitest/coverage-istanbul", "@vitest/coverage-v8", "@vitest/ui", "happy-dom", "jsdom"] }, "sha512-9NiG6UadG0iOaPL1AMsO5sDKkx6MADHw4/mMOmHWZUhhUwqzfVtnnptMK37vD71e6KyR7yAscx19FrtOWWtjvA=="], + "@voidzero-dev/vite-plus-win32-arm64-msvc": ["@voidzero-dev/vite-plus-win32-arm64-msvc@0.3.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-KRhdy5K13AYx9KBfCVHRrK7zSZU+bMW9CL6gTai+UkJgAmDJi1kjdSNboZOjO8mrzUnTCrELgMI2tnstcxSTuA=="], - "@voidzero-dev/vite-plus-win32-arm64-msvc": ["@voidzero-dev/vite-plus-win32-arm64-msvc@0.1.24", "", { "os": "win32", "cpu": "arm64" }, "sha512-G+/lhLKVjyn3FmgXX8jeWgq7RcE5O1kdR7QyFayQOdlMX/ZRkvUwQD7bFaqhKzgJM6Oj3a1FH3HQPYk5QOYuCQ=="], - - "@voidzero-dev/vite-plus-win32-x64-msvc": ["@voidzero-dev/vite-plus-win32-x64-msvc@0.1.24", "", { "os": "win32", "cpu": "x64" }, "sha512-b0e5XohEV1w/RdzAtv8/Hm6tvHPXouPtBNsljjW/lDJZq3NCLND5s6lqe8H4IenrgmKSoqakHWtlqJqM36cFbw=="], + "@voidzero-dev/vite-plus-win32-x64-msvc": ["@voidzero-dev/vite-plus-win32-x64-msvc@0.3.0", "", { "os": "win32", "cpu": "x64" }, "sha512-7+G+GxGmxdpQO0zjiGnkZFXKGqm0CrVduebRsJd6ccuOuxCQYPxLcoHq4WOaGrh56SrAGS7XjhnQCrXRkzKUVQ=="], "@whatwg-node/disposablestack": ["@whatwg-node/disposablestack@0.0.6", "", { "dependencies": { "@whatwg-node/promise-helpers": "^1.0.0", "tslib": "^2.6.3" } }, "sha512-LOtTn+JgJvX8WfBVJtF08TGrdjuFzGJc4mkP8EdDI8ADbvO7kiexYep1o8dwnt0okb0jYclCDXF13xU7Ge4zSw=="], @@ -1582,8 +1578,6 @@ "body-parser": ["body-parser@2.3.0", "", { "dependencies": { "bytes": "^3.1.2", "content-type": "^2.0.0", "debug": "^4.4.3", "http-errors": "^2.0.1", "iconv-lite": "^0.7.2", "on-finished": "^2.4.1", "qs": "^6.15.2", "raw-body": "^3.0.2", "type-is": "^2.1.0" } }, "sha512-2cGmJupaNgg+QUwVLAucDuWuoMZ6EX9iHDRswZ5lsNYEmwPaRknMPCLZz07yTzVq/83p4o/wzbDZbBrTvGGTIw=="], - "boring-avatars": ["boring-avatars@2.0.4", "", { "peerDependencies": { "react": ">=18.0.0", "react-dom": ">=18.0.0" } }, "sha512-xhZO/w/6aFmRfkaWohcl2NfyIy87gK5SBbys8kctZeTGF1Apjpv/10pfUuv+YEfVPkESU/h2Y6tt/Dwp+bIZPw=="], - "brace-expansion": ["brace-expansion@5.0.7", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-7oFy703dxfY3/NLxC1fh2SUCQ0H9rmAY+5EpDVfXjUTTs+HEwR2nYaqLv+GWcTsumwxPfiz6CzCNkwXwBUwqCA=="], "braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="], @@ -1638,7 +1632,7 @@ "cliui": ["cliui@9.0.1", "", { "dependencies": { "string-width": "^7.2.0", "strip-ansi": "^7.1.0", "wrap-ansi": "^9.0.0" } }, "sha512-k7ndgKhwoQveBL+/1tqGJYNz097I7WOvwbmmU2AR5+magtbjPWQTS1C5vzGkBC8Ym8UWRzfKUzUUqFLypY4Q+w=="], - "cloudflare": ["cloudflare@7.0.0", "", { "bin": { "cloudflare": "bin/cli" } }, "sha512-ZU8Yp1biaRRf+i1Hlap/VUwSBeKnOa4kkygIU9naE07JILnHdgLr9Wq/iPEeEf2in0N5m1giTp1ohY+cNQZEGQ=="], + "cloudflare": ["cloudflare@7.1.0", "", { "bin": { "cloudflare": "bin/cli" } }, "sha512-eb6WtiAbhuqdWmBdVo6fuXQgEBkYjhOeujRDYLEUWEZIECuABvDLz2DUYTwpN/4KyFecM4JtMHgZn5prlFUjLg=="], "clsx": ["clsx@2.1.1", "", {}, "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA=="], @@ -1826,7 +1820,7 @@ "es-errors": ["es-errors@1.3.0", "", {}, "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="], - "es-module-lexer": ["es-module-lexer@1.7.0", "", {}, "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA=="], + "es-module-lexer": ["es-module-lexer@2.3.1", "", {}, "sha512-shc1dbU90Yl/xq1QrC7QRtfcwURZuVRfPhZbDoldJ1cn1gzDvBaBWlv0eFolj5+0znnPJz5TXLxsN77X/12KTA=="], "es-object-atoms": ["es-object-atoms@1.1.2", "", { "dependencies": { "es-errors": "^1.3.0" } }, "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw=="], @@ -1918,14 +1912,12 @@ "flatted": ["flatted@3.4.2", "", {}, "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA=="], - "formatly": ["formatly@0.3.0", "", { "dependencies": { "fd-package-json": "^2.0.0" }, "bin": { "formatly": "bin/index.mjs" } }, "sha512-9XNj/o4wrRFyhSMJOvsuyMwy8aUfBaZ1VrqHVfohyXf0Sw0e+yfKG+xZaY3arGCOMdwFsqObtzVOc1gU9KiT9w=="], + "formatly": ["formatly@0.7.0", "", { "dependencies": { "fd-package-json": "^2.0.0", "package-manager-detector": "^1.8.0" }, "bin": { "formatly": "bin/index.mjs" } }, "sha512-7CXJtIIA0zy/u12StsYk25qVKxvdLA2ep2sTNxK3ov0mGNIIDqIvAXDSgTnAfDJFsPfWjuz0WjfYSdpvnLA5Tg=="], "formdata-polyfill": ["formdata-polyfill@4.0.10", "", { "dependencies": { "fetch-blob": "^3.1.2" } }, "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g=="], "forwarded": ["forwarded@0.2.0", "", {}, "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow=="], - "framer-motion": ["framer-motion@12.42.2", "", { "dependencies": { "motion-dom": "^12.42.2", "motion-utils": "^12.39.0", "tslib": "^2.4.0" }, "peerDependencies": { "@emotion/is-prop-valid": "*", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" }, "optionalPeers": ["@emotion/is-prop-valid", "react", "react-dom"] }, "sha512-5XY9luDiu0oHfHBjpDthFMh0ES+122w6p/papSJBweMkO8Sn+PW2QaEgRblQBpWFnuvZS5qvarpt/hO2pjGmnw=="], - "fresh": ["fresh@2.0.0", "", {}, "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A=="], "fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="], @@ -1944,7 +1936,7 @@ "get-proto": ["get-proto@1.0.1", "", { "dependencies": { "dunder-proto": "^1.0.1", "es-object-atoms": "^1.0.0" } }, "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g=="], - "get-tsconfig": ["get-tsconfig@4.14.1", "", { "dependencies": { "resolve-pkg-maps": "^1.0.0" } }, "sha512-Dz/6HxkrxgNehhxLVeyv8sad9UzF2xBVeaKBQNDfJ5XiSXmp2gTR0eO0RWiT2NCKS5aGP9jjkOMggTN90qU50A=="], + "get-tsconfig": ["get-tsconfig@4.14.3", "", { "dependencies": { "resolve-pkg-maps": "^1.0.0" } }, "sha512-++QEw4DIY7WGoukz+/+A/8dGYPT9l9yIadnmSgZ8Rjr3YVSVDipQSO9CdnJo9ePqFqUUqh+wk9uIaoiAwsiPkA=="], "glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="], @@ -2002,7 +1994,7 @@ "iconv-lite": ["iconv-lite@0.7.3", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-IKXpvIzjnC9XTAUbVBcMfGS0EPaIXtW6v+zr+RRp+hqULEpo0owZax6wyRwPOJbWbzjYspQwusTsfVr0ifh4uQ=="], - "ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="], + "ignore": ["ignore@7.0.7", "", {}, "sha512-dML0wP6oak21rsNYCJpJB6O1BJIEwNpGrTw0URPfAk4hm0e3pRfCtzkfB6olBcXcVlU2rouCyz7lCyRB0OMVCA=="], "immutable": ["immutable@5.1.9", "", {}, "sha512-m8nVez3rwrgmWxtLMt1ZYXB2Lv7OKYn/disyxAlSDYAlKSlFoPPfIAmAM/M5xqL4m4C/wAPw7S2/CNaUii1Hxg=="], @@ -2106,7 +2098,7 @@ "kleur": ["kleur@3.0.3", "", {}, "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w=="], - "knip": ["knip@6.32.2", "", { "dependencies": { "fdir": "^6.5.0", "formatly": "^0.3.0", "get-tsconfig": "4.14.1", "jiti": "^2.7.0", "oxc-parser": "^0.143.0", "oxc-resolver": "11.24.2", "picomatch": "^4.0.5", "smol-toml": "^1.7.1", "strip-json-comments": "5.0.3", "tinyglobby": "^0.2.17", "unbash": "^4.0.9", "yaml": "^2.9.0", "zod": "^4.4.3" }, "bin": { "knip": "bin/knip.js", "knip-bun": "bin/knip-bun.js" } }, "sha512-WXTXbmocrw7gqm1A1TQvFN0OgJ7hUSU6E1g6SPRIzzHFogUBhXByc7cYeOFVtJ2uODg7DP4VbESYBYnfbtBYsg=="], + "knip": ["knip@6.33.0", "", { "dependencies": { "fdir": "^6.5.0", "formatly": "^0.7.0", "get-tsconfig": "4.14.3", "jiti": "^2.7.0", "oxc-parser": "^0.147.0", "oxc-resolver": "11.24.2", "picomatch": "^4.0.7", "smol-toml": "^1.8.0", "strip-json-comments": "5.0.3", "tinyglobby": "^0.2.17", "unbash": "^4.0.10", "yaml": "^2.9.0", "zod": "^4.4.3" }, "bin": { "knip": "bin/knip.js", "knip-bun": "bin/knip-bun.js" } }, "sha512-gMXWV2bqgJcdZKsaRgl1oH5V2J0VumhJ2ujfP4M6b9ftpca2BNpvlX3Dq++vVtEey7sU67EXCvZGNkQfG2w1RQ=="], "kysely": ["kysely@0.29.4", "", {}, "sha512-y5mVgQNkMbs1eK9Xyc0pmNdabN2wHhRYY/5r4W5HrUT1rYCEPeVNSj1RUJeSDKT3U0p+mXCvLgkrFuIafYI6BA=="], @@ -2282,18 +2274,12 @@ "mimic-function": ["mimic-function@5.0.1", "", {}, "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA=="], - "miniflare": ["miniflare@5.20260811.1-alpha", "", { "dependencies": { "@cspotcode/source-map-support": "0.8.1", "sharp": "0.35.2", "undici": "7.29.0", "workerd": "1.20260811.1", "ws": "8.21.0", "youch": "4.1.0-beta.10" } }, "sha512-DtOG0BeanIxs2sH0smFvExZD89cBQwGckbHiFkRJrrNAUu3NGClZkUxqu+zy7HYfKBAgq935EMY49vIPm3JVdA=="], + "miniflare": ["miniflare@5.20260828.0-alpha", "", { "dependencies": { "@cspotcode/source-map-support": "0.8.1", "sharp": "0.35.2", "undici": "7.29.0", "workerd": "1.20260828.1", "ws": "8.21.0", "youch": "4.1.0-beta.10" } }, "sha512-6nbxhZEcz/UET3Y1OnYPsrAUjUmuFoib3ynUqteRdn1YnDxsLg8cwgZJZCk9QmtOmGzXwzXzgE/d/C0dJAPtVw=="], "minimatch": ["minimatch@10.2.5", "", { "dependencies": { "brace-expansion": "^5.0.5" } }, "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg=="], "module-details-from-path": ["module-details-from-path@1.0.4", "", {}, "sha512-EGWKgxALGMgzvxYF1UyGTy0HXX/2vHLkw6+NvDKW2jypWbHpjQuj4UMcqQWXHERJhVGKikolT06G3bcKe4fi7w=="], - "motion": ["motion@12.42.2", "", { "dependencies": { "framer-motion": "^12.42.2", "tslib": "^2.4.0" }, "peerDependencies": { "@emotion/is-prop-valid": "*", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" }, "optionalPeers": ["@emotion/is-prop-valid", "react", "react-dom"] }, "sha512-Atvv11yUKIid41cVrRBDVX5m8tF8kNpExRSlbpt6APClhDjtwQssgFHhQzejxw7/7YYbjHSPKBVbHo05BuJT5Q=="], - - "motion-dom": ["motion-dom@12.42.2", "", { "dependencies": { "motion-utils": "^12.39.0" } }, "sha512-5gIMWLp/PycBtJRJWRgjxke5n8dlvkSn2DrYW+tr3XcqAZY1xZh6BJyooJXCM8wdfM7wfMjkBJNLge1CKPUIRA=="], - - "motion-utils": ["motion-utils@12.39.0", "", {}, "sha512-8nadJAJjTtqRkmRF36FoJTrywK9nnFmnPwnSMyxaOCU7GDjN9RTMJIxx9De8ErM+vpPhMccr/6fo5WciyQLnMQ=="], - "mrmime": ["mrmime@2.0.1", "", {}, "sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ=="], "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], @@ -2330,51 +2316,51 @@ "openapi-types": ["openapi-types@12.1.3", "", {}, "sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw=="], - "opencode-ai": ["opencode-ai@1.18.23", "", { "optionalDependencies": { "opencode-darwin-arm64": "1.18.23", "opencode-darwin-x64": "1.18.23", "opencode-darwin-x64-baseline": "1.18.23", "opencode-linux-arm64": "1.18.23", "opencode-linux-arm64-musl": "1.18.23", "opencode-linux-x64": "1.18.23", "opencode-linux-x64-baseline": "1.18.23", "opencode-linux-x64-baseline-musl": "1.18.23", "opencode-linux-x64-musl": "1.18.23", "opencode-windows-arm64": "1.18.23", "opencode-windows-x64": "1.18.23", "opencode-windows-x64-baseline": "1.18.23" }, "os": [ "linux", "win32", "darwin", ], "cpu": [ "x64", "arm64", ], "bin": { "opencode": "bin/opencode.exe" } }, "sha512-3NkT0XINL7d0HYkTyGV1SPChHXhvRgKqNaTgKRTGb0TXUWszXA7MW/y3zMZw29y1AQuUDAzRvVYmQ9KGRQhroA=="], + "opencode-ai": ["opencode-ai@1.18.25", "", { "optionalDependencies": { "opencode-darwin-arm64": "1.18.25", "opencode-darwin-x64": "1.18.25", "opencode-darwin-x64-baseline": "1.18.25", "opencode-linux-arm64": "1.18.25", "opencode-linux-arm64-musl": "1.18.25", "opencode-linux-x64": "1.18.25", "opencode-linux-x64-baseline": "1.18.25", "opencode-linux-x64-baseline-musl": "1.18.25", "opencode-linux-x64-musl": "1.18.25", "opencode-windows-arm64": "1.18.25", "opencode-windows-x64": "1.18.25", "opencode-windows-x64-baseline": "1.18.25" }, "os": [ "linux", "win32", "darwin", ], "cpu": [ "x64", "arm64", ], "bin": { "opencode": "bin/opencode.exe" } }, "sha512-pS4RKJ9eKwU7Dp5G5pdj1rhMnpG5APixXzfTKNoFqv9aFVI36Rnza2jESvKifxyPZlsA65MQB03WCArY0EK6mg=="], - "opencode-darwin-arm64": ["opencode-darwin-arm64@1.18.23", "", { "os": "darwin", "cpu": "arm64" }, "sha512-QP9PjwpHtZoLVXw2WvUmPZecz7mWbQkT4t3K36B//fCaDG+zWa+SsztIeaW5azujNwwtUemLA5icE/zINng48Q=="], + "opencode-darwin-arm64": ["opencode-darwin-arm64@1.18.25", "", { "os": "darwin", "cpu": "arm64" }, "sha512-W4dyMFtHBglWZ1SEooh3Ke9v1M9lv945Y58atb8e1yKII8YykJ8LknOFyKipYC028oPDO4IZc3GYGKbg9PCg2w=="], - "opencode-darwin-x64": ["opencode-darwin-x64@1.18.23", "", { "os": "darwin", "cpu": "x64" }, "sha512-R9nWP3edz/0FnEfwmuxtiWBB7bS4NtZCyCffJyiMlrbwdDC+bIXYrWxWXVrzaP1mJujs6g2MAwTCUSx/qpBhDw=="], + "opencode-darwin-x64": ["opencode-darwin-x64@1.18.25", "", { "os": "darwin", "cpu": "x64" }, "sha512-YYKrfeUSJhD7hZl+yNmayS51sDwxiE9o5XwrfgYSSie6sOyHFc9Ei13VBkVU6T+IJHhFhTahOFAwSDxggrAnGA=="], - "opencode-darwin-x64-baseline": ["opencode-darwin-x64-baseline@1.18.23", "", { "os": "darwin", "cpu": "x64" }, "sha512-QGx6I/nFYur7qJ/Nx2L3fC4XYQt44cyDsm7p8twNA+cdjGX3ndnPbMdAl5ikdZyAfSMUGYK8VWY2JMxv0rmfjw=="], + "opencode-darwin-x64-baseline": ["opencode-darwin-x64-baseline@1.18.25", "", { "os": "darwin", "cpu": "x64" }, "sha512-rRgTaoTeIN2diL1e1HGZ48Zh4ynMDEB1jYjD76LaFUVzMwakEY1i7NvG8e/rbjRMDkgXIr6TwzCtIMcMOpLQMA=="], - "opencode-linux-arm64": ["opencode-linux-arm64@1.18.23", "", { "os": "linux", "cpu": "arm64" }, "sha512-g1zDFhuE9FOYwjSGderlu69wfd4GQzS0xsDiIY11QUciuBmM6DrHqLvmhlLFsUVHYVnCPY1YeN1pq5ewE3x72Q=="], + "opencode-linux-arm64": ["opencode-linux-arm64@1.18.25", "", { "os": "linux", "cpu": "arm64" }, "sha512-PMvcpFpha3yAhaVCC0QbegHPxsEZ0FuQf+52PXvqQut1r3w1l1Pilor9tUA7TyCRa4UokACI90nTmKmtMnQBag=="], - "opencode-linux-arm64-musl": ["opencode-linux-arm64-musl@1.18.23", "", { "os": "linux", "cpu": "arm64" }, "sha512-VyDkzUJfJgkx9h9RhazTW9xeTgSXBmVFPblsbPGBW9tR612f6gjQxfOfu4cpHnQHK1qjuW9ClzLKHWqv3EcJTA=="], + "opencode-linux-arm64-musl": ["opencode-linux-arm64-musl@1.18.25", "", { "os": "linux", "cpu": "arm64" }, "sha512-IwIPKmNwIjLshlSgjoRLKFwxxiLpZ5Y0zjv6r456RQtJKK62IbYGXkCm3AiSZ/lqGsu3XF+xn/Xza29ivgpgcg=="], - "opencode-linux-x64": ["opencode-linux-x64@1.18.23", "", { "os": "linux", "cpu": "x64" }, "sha512-5x9d1Cm/YtqzR6lAlNbgVprTQ3R3hx7qGTWCzm5l5u6lBNkhYTrhy2s8k25dxKKuxqZ9Kngqz9JYWvSVHy2Lmw=="], + "opencode-linux-x64": ["opencode-linux-x64@1.18.25", "", { "os": "linux", "cpu": "x64" }, "sha512-bdRSJ6gbK/EnLNWxROOQYXFXiUeqeFxGz8DIO8LCqnii99A2OWFAyZ3Da5gpvfT1Yrp9/lYL55n/tM3ale5smg=="], - "opencode-linux-x64-baseline": ["opencode-linux-x64-baseline@1.18.23", "", { "os": "linux", "cpu": "x64" }, "sha512-yUhBOXfTQour2JCdAkwD3DDqSnyxB0grefwdPqEhYmJHIkYxfJIIzyy6V//pyouvkE0XMouFtiuZXw8S6Wo0iQ=="], + "opencode-linux-x64-baseline": ["opencode-linux-x64-baseline@1.18.25", "", { "os": "linux", "cpu": "x64" }, "sha512-+b0w7XyHx0XPQWHBk2JymXbXnyZQ2PjIPuu4a4QJgSUqGuGz1L2flA3wgpZVAWFUhrEIr9DFhBk3AkKKNgMuRw=="], - "opencode-linux-x64-baseline-musl": ["opencode-linux-x64-baseline-musl@1.18.23", "", { "os": "linux", "cpu": "x64" }, "sha512-c1DPxauhzAurlIBhJBr/rokDpc65l084T4qTl36gDDT9Xzc/Nk5Q5yMDaPm1DDI3WeHKDt11MDlxT5AjQW5gtw=="], + "opencode-linux-x64-baseline-musl": ["opencode-linux-x64-baseline-musl@1.18.25", "", { "os": "linux", "cpu": "x64" }, "sha512-E2JUeOOSXPbG1cNOzxnqjqkd0a3+oFmwkbJe6bZ308CFgLWBFfVh0fF42HTCEqfK+yYbidpEkQuEkUgxq/11IA=="], - "opencode-linux-x64-musl": ["opencode-linux-x64-musl@1.18.23", "", { "os": "linux", "cpu": "x64" }, "sha512-t/5mlnTBZKdZpqKHwdwxlWqGakntauvMSmXtyJc17M7XJRmZaaGHtNSaSefbbYFIL4agoQCXTvIkhhyxOvr7zQ=="], + "opencode-linux-x64-musl": ["opencode-linux-x64-musl@1.18.25", "", { "os": "linux", "cpu": "x64" }, "sha512-W15qTNDz1fsTzs1SkE6bB/gpIDBF3rwDbewUKdbyXD3dVs6umyugOql1T4u9n/gqWa/Z/VDURbn39VsejeSdbQ=="], - "opencode-windows-arm64": ["opencode-windows-arm64@1.18.23", "", { "os": "win32", "cpu": "arm64" }, "sha512-QtJQcLU0yPz6on3jjks3f/EHgZuIDFw7FvAKu3wsHhL09NYDh7GczfRXDPRHa3NgqnU8dkB8p9mhqgaPRPogoQ=="], + "opencode-windows-arm64": ["opencode-windows-arm64@1.18.25", "", { "os": "win32", "cpu": "arm64" }, "sha512-GFp74pProoPwqktHMf+9wQ8fza1RvFt0RG0iRtTQnJ4VWVY62qEeVuJkH6ki9QXS270HXyqtxvF8AuHQzuVZlA=="], - "opencode-windows-x64": ["opencode-windows-x64@1.18.23", "", { "os": "win32", "cpu": "x64" }, "sha512-mMaIITuXzkNfjdcYL8uZaZuMDjulFyH/UCq9bxblam2mUZf9uWisoi5J6CXFsS/mkN7CZfTAt6PttSp4n3PH4g=="], + "opencode-windows-x64": ["opencode-windows-x64@1.18.25", "", { "os": "win32", "cpu": "x64" }, "sha512-xW5wtSxWYbI7DcmQWMlNWIiDBdMJON1vDiEmVWo88R9tT/PaahOhWKgp7FoWDqJKf89jS3ZIzkqnkU3F2dio7A=="], - "opencode-windows-x64-baseline": ["opencode-windows-x64-baseline@1.18.23", "", { "os": "win32", "cpu": "x64" }, "sha512-AqXsTKaPcDx3rrid5bLUwJbQ/3vr9rJ6fvOStIznTzwrbOgP8wy5G4jCoIzu6KB/WxGx/d1MrV4cGaJ73qnjBA=="], + "opencode-windows-x64-baseline": ["opencode-windows-x64-baseline@1.18.25", "", { "os": "win32", "cpu": "x64" }, "sha512-/28bGRQwT+2JdGbtGaNr95tstgysiULEXtcvgNg7yLDxitqmSVgd8V8XRGS0UWDdfiWWMND9A9T5EAsbF1/xDQ=="], "optionator": ["optionator@0.9.4", "", { "dependencies": { "deep-is": "^0.1.3", "fast-levenshtein": "^2.0.6", "levn": "^0.4.1", "prelude-ls": "^1.2.1", "type-check": "^0.4.0", "word-wrap": "^1.2.5" } }, "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g=="], - "oxc-parser": ["oxc-parser@0.143.0", "", { "dependencies": { "@oxc-project/types": "^0.143.0" }, "optionalDependencies": { "@oxc-parser/binding-android-arm-eabi": "0.143.0", "@oxc-parser/binding-android-arm64": "0.143.0", "@oxc-parser/binding-darwin-arm64": "0.143.0", "@oxc-parser/binding-darwin-x64": "0.143.0", "@oxc-parser/binding-freebsd-x64": "0.143.0", "@oxc-parser/binding-linux-arm-gnueabihf": "0.143.0", "@oxc-parser/binding-linux-arm-musleabihf": "0.143.0", "@oxc-parser/binding-linux-arm64-gnu": "0.143.0", "@oxc-parser/binding-linux-arm64-musl": "0.143.0", "@oxc-parser/binding-linux-ppc64-gnu": "0.143.0", "@oxc-parser/binding-linux-riscv64-gnu": "0.143.0", "@oxc-parser/binding-linux-riscv64-musl": "0.143.0", "@oxc-parser/binding-linux-s390x-gnu": "0.143.0", "@oxc-parser/binding-linux-x64-gnu": "0.143.0", "@oxc-parser/binding-linux-x64-musl": "0.143.0", "@oxc-parser/binding-openharmony-arm64": "0.143.0", "@oxc-parser/binding-win32-arm64-msvc": "0.143.0", "@oxc-parser/binding-win32-ia32-msvc": "0.143.0", "@oxc-parser/binding-win32-x64-msvc": "0.143.0" } }, "sha512-ov0NzaDCOInknS7mP1cwKdJERt3utPW8ldjtdUXQ8Ty0GEFD08wk422vCUN0d7pST6kqtV7dxoI9w1Zi0l/9TA=="], + "oxc-parser": ["oxc-parser@0.147.0", "", { "dependencies": { "@oxc-project/types": "^0.147.0" }, "optionalDependencies": { "@oxc-parser/binding-android-arm-eabi": "0.147.0", "@oxc-parser/binding-android-arm64": "0.147.0", "@oxc-parser/binding-darwin-arm64": "0.147.0", "@oxc-parser/binding-darwin-x64": "0.147.0", "@oxc-parser/binding-freebsd-x64": "0.147.0", "@oxc-parser/binding-linux-arm-gnueabihf": "0.147.0", "@oxc-parser/binding-linux-arm-musleabihf": "0.147.0", "@oxc-parser/binding-linux-arm64-gnu": "0.147.0", "@oxc-parser/binding-linux-arm64-musl": "0.147.0", "@oxc-parser/binding-linux-ppc64-gnu": "0.147.0", "@oxc-parser/binding-linux-riscv64-gnu": "0.147.0", "@oxc-parser/binding-linux-riscv64-musl": "0.147.0", "@oxc-parser/binding-linux-s390x-gnu": "0.147.0", "@oxc-parser/binding-linux-x64-gnu": "0.147.0", "@oxc-parser/binding-linux-x64-musl": "0.147.0", "@oxc-parser/binding-openharmony-arm64": "0.147.0", "@oxc-parser/binding-win32-arm64-msvc": "0.147.0", "@oxc-parser/binding-win32-ia32-msvc": "0.147.0", "@oxc-parser/binding-win32-x64-msvc": "0.147.0" } }, "sha512-5xaug6t7GfV3BO5Iv+xHW1rmQkDEQ3BEu3L8g3InsvWO5i8CYGc4tCZ2X985QcwWNycFJam+aOns6Nr2XAThTA=="], "oxc-resolver": ["oxc-resolver@11.24.2", "", { "optionalDependencies": { "@oxc-resolver/binding-android-arm-eabi": "11.24.2", "@oxc-resolver/binding-android-arm64": "11.24.2", "@oxc-resolver/binding-darwin-arm64": "11.24.2", "@oxc-resolver/binding-darwin-x64": "11.24.2", "@oxc-resolver/binding-freebsd-x64": "11.24.2", "@oxc-resolver/binding-linux-arm-gnueabihf": "11.24.2", "@oxc-resolver/binding-linux-arm-musleabihf": "11.24.2", "@oxc-resolver/binding-linux-arm64-gnu": "11.24.2", "@oxc-resolver/binding-linux-arm64-musl": "11.24.2", "@oxc-resolver/binding-linux-ppc64-gnu": "11.24.2", "@oxc-resolver/binding-linux-riscv64-gnu": "11.24.2", "@oxc-resolver/binding-linux-riscv64-musl": "11.24.2", "@oxc-resolver/binding-linux-s390x-gnu": "11.24.2", "@oxc-resolver/binding-linux-x64-gnu": "11.24.2", "@oxc-resolver/binding-linux-x64-musl": "11.24.2", "@oxc-resolver/binding-openharmony-arm64": "11.24.2", "@oxc-resolver/binding-wasm32-wasi": "11.24.2", "@oxc-resolver/binding-win32-arm64-msvc": "11.24.2", "@oxc-resolver/binding-win32-x64-msvc": "11.24.2" } }, "sha512-FY91FiDBj7ls5MsFS9jN3tjz2o0/zsdSsymlakySaBwVJZorHhkWyICLZMKxlu1R9vYo+sd3z1jwb4J8x7bNDw=="], - "oxfmt": ["oxfmt@0.52.0", "", { "dependencies": { "tinypool": "2.1.0" }, "optionalDependencies": { "@oxfmt/binding-android-arm-eabi": "0.52.0", "@oxfmt/binding-android-arm64": "0.52.0", "@oxfmt/binding-darwin-arm64": "0.52.0", "@oxfmt/binding-darwin-x64": "0.52.0", "@oxfmt/binding-freebsd-x64": "0.52.0", "@oxfmt/binding-linux-arm-gnueabihf": "0.52.0", "@oxfmt/binding-linux-arm-musleabihf": "0.52.0", "@oxfmt/binding-linux-arm64-gnu": "0.52.0", "@oxfmt/binding-linux-arm64-musl": "0.52.0", "@oxfmt/binding-linux-ppc64-gnu": "0.52.0", "@oxfmt/binding-linux-riscv64-gnu": "0.52.0", "@oxfmt/binding-linux-riscv64-musl": "0.52.0", "@oxfmt/binding-linux-s390x-gnu": "0.52.0", "@oxfmt/binding-linux-x64-gnu": "0.52.0", "@oxfmt/binding-linux-x64-musl": "0.52.0", "@oxfmt/binding-openharmony-arm64": "0.52.0", "@oxfmt/binding-win32-arm64-msvc": "0.52.0", "@oxfmt/binding-win32-ia32-msvc": "0.52.0", "@oxfmt/binding-win32-x64-msvc": "0.52.0" }, "peerDependencies": { "svelte": "^5.0.0", "vite-plus": "*" }, "optionalPeers": ["svelte", "vite-plus"], "bin": { "oxfmt": "bin/oxfmt" } }, "sha512-nJlYM35F64zTDMecCNhoHNkf+D/eHv7xcjj9XDSj+bFAVtN93m7v8DQMdHd6nDG6Akf/kEYYHmDUBs2Dz27Sug=="], + "oxfmt": ["oxfmt@0.64.0", "", { "dependencies": { "tinypool": "2.1.0" }, "optionalDependencies": { "@oxfmt/binding-android-arm-eabi": "0.64.0", "@oxfmt/binding-android-arm64": "0.64.0", "@oxfmt/binding-darwin-arm64": "0.64.0", "@oxfmt/binding-darwin-x64": "0.64.0", "@oxfmt/binding-freebsd-x64": "0.64.0", "@oxfmt/binding-linux-arm-gnueabihf": "0.64.0", "@oxfmt/binding-linux-arm-musleabihf": "0.64.0", "@oxfmt/binding-linux-arm64-gnu": "0.64.0", "@oxfmt/binding-linux-arm64-musl": "0.64.0", "@oxfmt/binding-linux-ppc64-gnu": "0.64.0", "@oxfmt/binding-linux-riscv64-gnu": "0.64.0", "@oxfmt/binding-linux-riscv64-musl": "0.64.0", "@oxfmt/binding-linux-s390x-gnu": "0.64.0", "@oxfmt/binding-linux-x64-gnu": "0.64.0", "@oxfmt/binding-linux-x64-musl": "0.64.0", "@oxfmt/binding-openharmony-arm64": "0.64.0", "@oxfmt/binding-win32-arm64-msvc": "0.64.0", "@oxfmt/binding-win32-ia32-msvc": "0.64.0", "@oxfmt/binding-win32-x64-msvc": "0.64.0" }, "peerDependencies": { "svelte": "^5.0.0", "vite-plus": "*" }, "optionalPeers": ["svelte", "vite-plus"], "bin": { "oxfmt": "bin/oxfmt" } }, "sha512-XZ4GFBN/PLbXKq+0zrgpQfPKYuJlUuj+nzZJY7UpIbFMNyefNLCdN9EwViycNqnYcv0wrn0jXcQLlqJp8RCKBg=="], "oxlint": ["oxlint@1.77.0", "", { "optionalDependencies": { "@oxlint/binding-android-arm-eabi": "1.77.0", "@oxlint/binding-android-arm64": "1.77.0", "@oxlint/binding-darwin-arm64": "1.77.0", "@oxlint/binding-darwin-x64": "1.77.0", "@oxlint/binding-freebsd-x64": "1.77.0", "@oxlint/binding-linux-arm-gnueabihf": "1.77.0", "@oxlint/binding-linux-arm-musleabihf": "1.77.0", "@oxlint/binding-linux-arm64-gnu": "1.77.0", "@oxlint/binding-linux-arm64-musl": "1.77.0", "@oxlint/binding-linux-ppc64-gnu": "1.77.0", "@oxlint/binding-linux-riscv64-gnu": "1.77.0", "@oxlint/binding-linux-riscv64-musl": "1.77.0", "@oxlint/binding-linux-s390x-gnu": "1.77.0", "@oxlint/binding-linux-x64-gnu": "1.77.0", "@oxlint/binding-linux-x64-musl": "1.77.0", "@oxlint/binding-openharmony-arm64": "1.77.0", "@oxlint/binding-win32-arm64-msvc": "1.77.0", "@oxlint/binding-win32-ia32-msvc": "1.77.0", "@oxlint/binding-win32-x64-msvc": "1.77.0" }, "peerDependencies": { "oxlint-tsgolint": ">=7.0.2001", "vite-plus": "*" }, "optionalPeers": ["oxlint-tsgolint", "vite-plus"], "bin": { "oxlint": "bin/oxlint" } }, "sha512-qnGh8XJHaQ0dprrDXNQZgS0FgjI6v+V3+X8DwmaV++5Aamy6jGKfDdQ1TUvhUxtmKFAbEf4/WeO5QZX+5WSngg=="], "oxlint-plugin-react-doctor": ["oxlint-plugin-react-doctor@0.9.12", "", { "dependencies": { "@shaderfrog/glsl-parser": "^7.0.1", "@typescript-eslint/types": "^8.59.3", "eslint-scope": "^9.1.2", "eslint-visitor-keys": "^5.0.1", "lightningcss": "^1.33.0", "oxc-parser": "^0.143.0" } }, "sha512-BplcCUU/tGByFGgY1YIax6evUmjk0K8zOGGrdPs3A3dqamrzjUvVuJdYpM1Ftyt/B5fFx6+VwRrWi3YZbIz4VQ=="], - "oxlint-tsgolint": ["oxlint-tsgolint@0.23.0", "", { "optionalDependencies": { "@oxlint-tsgolint/darwin-arm64": "0.23.0", "@oxlint-tsgolint/darwin-x64": "0.23.0", "@oxlint-tsgolint/linux-arm64": "0.23.0", "@oxlint-tsgolint/linux-x64": "0.23.0", "@oxlint-tsgolint/win32-arm64": "0.23.0", "@oxlint-tsgolint/win32-x64": "0.23.0" }, "bin": { "tsgolint": "bin/tsgolint.js" } }, "sha512-3mBv3CoPbh8dFbzfDGIWa2ytZjn2v+3EX4aKRXjIhsoGFzG8GCjfRirz3rwZf1wYbZzsNLTSgpw8VjQuWdp/jA=="], + "oxlint-tsgolint": ["oxlint-tsgolint@7.0.2001", "", { "optionalDependencies": { "@oxlint-tsgolint/darwin-arm64": "7.0.2001", "@oxlint-tsgolint/darwin-x64": "7.0.2001", "@oxlint-tsgolint/linux-arm64": "7.0.2001", "@oxlint-tsgolint/linux-x64": "7.0.2001", "@oxlint-tsgolint/win32-arm64": "7.0.2001", "@oxlint-tsgolint/win32-x64": "7.0.2001" }, "bin": { "tsgolint": "./bin/tsgolint.js" } }, "sha512-KjK/XLcXr1DSyonKhsuFqJRiuKqcyG9j3LJ8nkOsrLzGvodBPqzHOKauy10asLMDI0sUpvb+1sxlzff3udZvfg=="], "p-limit": ["p-limit@3.1.0", "", { "dependencies": { "yocto-queue": "^0.1.0" } }, "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ=="], "p-locate": ["p-locate@5.0.0", "", { "dependencies": { "p-limit": "^3.0.2" } }, "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw=="], - "package-manager-detector": ["package-manager-detector@1.7.0", "", {}, "sha512-xg1eHpwYL/D/HEdWw2goFZP6vV0FH7W+PZ5rFkGjdIDLtxq7EkzBUeT3m+lndYCt8wKbmofUu1MUdMCXkCk9ZQ=="], + "package-manager-detector": ["package-manager-detector@1.8.0", "", {}, "sha512-yQA4H19AmPEoMUeavPMDIe1higySl/gH/yaQrkT/s07Qp+7pp2hYz30N3z2l5BkjVkF9Ow6o0wjJamm2y7Sn0A=="], "parent-module": ["parent-module@1.0.1", "", { "dependencies": { "callsites": "^3.0.0" } }, "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g=="], @@ -2406,9 +2392,7 @@ "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="], - "picomatch": ["picomatch@4.0.5", "", {}, "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A=="], - - "pixelmatch": ["pixelmatch@7.2.0", "", { "dependencies": { "pngjs": "^7.0.0" }, "bin": { "pixelmatch": "bin/pixelmatch" } }, "sha512-xhcb4yHu9sM/G7foGzoLtXYcC0zHEaOXXjRKhGup0fw78Nf2Tkiapv4EQyMzrbcmQPsllAI7DbFY2UT7PlI9Pg=="], + "picomatch": ["picomatch@4.0.7", "", {}, "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA=="], "pkce-challenge": ["pkce-challenge@5.0.1", "", {}, "sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ=="], @@ -2670,7 +2654,7 @@ "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], - "tsx": ["tsx@4.23.12", "", { "dependencies": { "esbuild": "~0.28.0" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "bin": { "tsx": "dist/cli.mjs" } }, "sha512-FDf4L4sYzKtzWYhU/Xm0AQFdTjdIxNo9ElTf2mxXM6k8YMHXzYUe4yODVaXP4V9uMFbVg8c0qyBccK2OOxb45Q=="], + "tsx": ["tsx@4.23.13", "", { "dependencies": { "esbuild": "~0.28.0" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "bin": { "tsx": "dist/cli.mjs" } }, "sha512-BL5MGkRln6aDYhb0xbQlEAGw743BaZYWdbWtdJOBriYJboKgUUYCadFp2/FpBBZquBC/ezNBn7wMMPx7FDZUDw=="], "tw-animate-css": ["tw-animate-css@1.4.0", "", {}, "sha512-7bziOlRqH0hJx80h/3mbicLW7o8qLsH5+RaLR2t+OHM3D0JlWGODQKQ4cxbK7WlvmUxpcj6Kgu6EKqjrGFe3QQ=="], @@ -2744,7 +2728,7 @@ "vite": ["vite@8.2.1", "", { "dependencies": { "lightningcss": "^1.33.0", "picomatch": "^4.0.5", "postcss": "^8.5.25", "rolldown": "~1.2.1", "tinyglobby": "^0.2.17" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "@vitejs/devtools": "^0.4.0", "esbuild": "^0.27.0 || ^0.28.0", "jiti": ">=1.21.0", "less": "^4.0.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "@vitejs/devtools", "esbuild", "jiti", "less", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-EU/eS7BH3XROHh2YnBefjM6DBKA6ZeMZEYQbj7NLWg5wHYlhB8B/Mayd5XsgWq+NFYccDOTemRpdETWR6Ka/lw=="], - "vite-plus": ["vite-plus@0.1.24", "", { "dependencies": { "@oxc-project/types": "=0.133.0", "@oxlint/plugins": "=1.61.0", "@voidzero-dev/vite-plus-core": "0.1.24", "@voidzero-dev/vite-plus-test": "0.1.24", "oxfmt": "=0.52.0", "oxlint": "=1.67.0", "oxlint-tsgolint": "=0.23.0" }, "optionalDependencies": { "@voidzero-dev/vite-plus-darwin-arm64": "0.1.24", "@voidzero-dev/vite-plus-darwin-x64": "0.1.24", "@voidzero-dev/vite-plus-linux-arm64-gnu": "0.1.24", "@voidzero-dev/vite-plus-linux-arm64-musl": "0.1.24", "@voidzero-dev/vite-plus-linux-x64-gnu": "0.1.24", "@voidzero-dev/vite-plus-linux-x64-musl": "0.1.24", "@voidzero-dev/vite-plus-win32-arm64-msvc": "0.1.24", "@voidzero-dev/vite-plus-win32-x64-msvc": "0.1.24" }, "bin": { "vp": "bin/vp", "oxfmt": "bin/oxfmt", "oxlint": "bin/oxlint" } }, "sha512-b3fr6WtCiEhetjuzW/4KcEMOAMuZxoxZATWaXKmPzOLf1upG+pzKJOFZTb94D6wiPBlwcjxoaUtF7C3uAN+VjQ=="], + "vite-plus": ["vite-plus@0.3.0", "", { "dependencies": { "@oxc-project/types": "=0.146.0", "@oxlint/plugins": "=1.79.0", "@vitest/browser": "4.1.11", "@vitest/browser-preview": "4.1.11", "@vitest/expect": "4.1.11", "@vitest/mocker": "4.1.11", "@vitest/pretty-format": "4.1.11", "@vitest/runner": "4.1.11", "@vitest/snapshot": "4.1.11", "@vitest/spy": "4.1.11", "@vitest/utils": "4.1.11", "@voidzero-dev/vite-plus-core": "0.3.0", "oxfmt": "=0.64.0", "oxlint": "=1.79.0", "oxlint-tsgolint": "=7.0.2001", "vitest": "4.1.11" }, "optionalDependencies": { "@voidzero-dev/vite-plus-darwin-arm64": "0.3.0", "@voidzero-dev/vite-plus-darwin-x64": "0.3.0", "@voidzero-dev/vite-plus-linux-arm64-gnu": "0.3.0", "@voidzero-dev/vite-plus-linux-arm64-musl": "0.3.0", "@voidzero-dev/vite-plus-linux-x64-gnu": "0.3.0", "@voidzero-dev/vite-plus-linux-x64-musl": "0.3.0", "@voidzero-dev/vite-plus-win32-arm64-msvc": "0.3.0", "@voidzero-dev/vite-plus-win32-x64-msvc": "0.3.0" }, "peerDependencies": { "@vitest/browser-playwright": "4.1.11", "@vitest/browser-webdriverio": "4.1.11" }, "optionalPeers": ["@vitest/browser-playwright", "@vitest/browser-webdriverio"], "bin": { "oxfmt": "./bin/oxfmt", "oxlint": "./bin/oxlint", "vp": "./bin/vp", "vpr": "./bin/vpr" } }, "sha512-GNWbWuWD37frCSFrz6MLzUo62bTv5IOJozHEgZYOkxsLkuQtTwm4TowzpfoGrSsfwhAAtfPd/sK1Y0+v1SwhZA=="], "vitest": ["vitest@4.1.11", "", { "dependencies": { "@vitest/expect": "4.1.11", "@vitest/mocker": "4.1.11", "@vitest/pretty-format": "4.1.11", "@vitest/runner": "4.1.11", "@vitest/snapshot": "4.1.11", "@vitest/spy": "4.1.11", "@vitest/utils": "4.1.11", "es-module-lexer": "^2.0.0", "expect-type": "^1.3.0", "magic-string": "^0.30.21", "obug": "^2.1.1", "pathe": "^2.0.3", "picomatch": "^4.0.3", "std-env": "^4.0.0-rc.1", "tinybench": "^2.9.0", "tinyexec": "^1.0.2", "tinyglobby": "^0.2.15", "tinyrainbow": "^3.1.0", "vite": "^6.0.0 || ^7.0.0 || ^8.0.0", "why-is-node-running": "^2.3.0" }, "peerDependencies": { "@edge-runtime/vm": "*", "@opentelemetry/api": "^1.9.0", "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", "@vitest/browser-playwright": "4.1.11", "@vitest/browser-preview": "4.1.11", "@vitest/browser-webdriverio": "4.1.11", "@vitest/coverage-istanbul": "4.1.11", "@vitest/coverage-v8": "4.1.11", "@vitest/ui": "4.1.11", "happy-dom": "*", "jsdom": "*" }, "optionalPeers": ["@edge-runtime/vm", "@opentelemetry/api", "@types/node", "@vitest/browser-playwright", "@vitest/browser-preview", "@vitest/browser-webdriverio", "@vitest/coverage-istanbul", "@vitest/coverage-v8", "@vitest/ui", "happy-dom", "jsdom"], "bin": { "vitest": "./vitest.mjs" } }, "sha512-fhACrNXUidIbGSBr5FlbuBkO7VWC1ZyLl0DO4CU2DrQoAPxX84Ysxs+HeGQpii5lZWV1Q4gBZTTu49mF+A6Edw=="], @@ -2782,9 +2766,9 @@ "word-wrap": ["word-wrap@1.2.5", "", {}, "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA=="], - "workerd": ["workerd@1.20260811.1", "", { "optionalDependencies": { "@cloudflare/workerd-darwin-64": "1.20260811.1", "@cloudflare/workerd-darwin-arm64": "1.20260811.1", "@cloudflare/workerd-linux-64": "1.20260811.1", "@cloudflare/workerd-linux-arm64": "1.20260811.1", "@cloudflare/workerd-windows-64": "1.20260811.1" }, "bin": { "workerd": "bin/workerd" } }, "sha512-kh+FFm55JQ4ssxhHZV9VPdMQq3D1nHxNJgwxMtWGD4dGppJvLySdguTRDKgeNTvgq6heSz+6TTXyPSDGj8Yllw=="], + "workerd": ["workerd@1.20260828.1", "", { "optionalDependencies": { "@cloudflare/workerd-darwin-64": "1.20260828.1", "@cloudflare/workerd-darwin-arm64": "1.20260828.1", "@cloudflare/workerd-linux-64": "1.20260828.1", "@cloudflare/workerd-linux-arm64": "1.20260828.1", "@cloudflare/workerd-windows-64": "1.20260828.1" }, "bin": { "workerd": "bin/workerd" } }, "sha512-pB9yvt0kkwZDAGZHmpY59r0o3hM0DzdW6BJERqwZOhunZ3ssOyDSgQxOQer2cSZW4YCFeOTIQYN1qwhK5wv/Cw=="], - "wrangler": ["wrangler@4.123.0", "", { "dependencies": { "@cloudflare/kv-asset-handler": "0.5.0", "@cloudflare/unenv-preset": "2.16.1", "blake3-wasm": "2.1.5", "esbuild": "0.28.1", "miniflare": "5.20260811.1-alpha", "path-to-regexp": "6.3.0", "unenv": "2.0.0-rc.24", "workerd": "1.20260811.1" }, "optionalDependencies": { "fsevents": "2.3.3" }, "peerDependencies": { "@cloudflare/workers-types": "^5.20260811.1" }, "optionalPeers": ["@cloudflare/workers-types"], "bin": { "wrangler": "bin/wrangler.js", "wrangler2": "bin/wrangler.js", "cf-wrangler": "bin/cf-wrangler.js" } }, "sha512-VXo2I1oa0x9aGAKIFPRSQPqTh0RBY5Ktl44YOhNmsJQFUdJKDA2vVTU6Xj+FC2koll6orJqWZN8jbXVIk9O67Q=="], + "wrangler": ["wrangler@4.127.1", "", { "dependencies": { "@cloudflare/kv-asset-handler": "0.5.0", "@cloudflare/unenv-preset": "2.16.1", "blake3-wasm": "2.1.5", "esbuild": "0.28.1", "miniflare": "5.20260828.0-alpha", "path-to-regexp": "6.3.0", "unenv": "2.0.0-rc.24", "workerd": "1.20260828.1" }, "optionalDependencies": { "fsevents": "2.3.3" }, "peerDependencies": { "@cloudflare/workers-types": "^5.20260828.1" }, "optionalPeers": ["@cloudflare/workers-types"], "bin": { "wrangler": "bin/wrangler.js", "wrangler2": "bin/wrangler.js", "cf-wrangler": "bin/cf-wrangler.js" } }, "sha512-OzsiNgaI8i681L/+KnAKc+uEZ5D57xK5JuNvCOpRKICF4/5Q3Cu1oTGuUiT/f3GDUqQb3gzXNT0tfOHGMEtknw=="], "wrap-ansi": ["wrap-ansi@10.0.0", "", { "dependencies": { "ansi-styles": "^6.2.3", "string-width": "^8.2.0", "strip-ansi": "^7.1.2" } }, "sha512-SGcvg80f0wUy2/fXES19feHMz8E0JoXv2uNgHOu4Dgi2OrCy1lqwFYEJz1BLbDI0exjPMe/ZdzZ/YpGECBG/aQ=="], @@ -2796,8 +2780,6 @@ "xmlchars": ["xmlchars@2.2.0", "", {}, "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw=="], - "xstate": ["xstate@5.32.5", "", {}, "sha512-ULazi1oe6wGrXl0Frb6otSlkm5HLifbbVTkMk5kkSKqz4TkxJaVpnl6jOJwKeid3ORPxYyZQgNLUSYX9q65SIA=="], - "y18n": ["y18n@5.0.8", "", {}, "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA=="], "yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="], @@ -2822,7 +2804,7 @@ "yuku-parser": ["yuku-parser@0.5.48", "", { "dependencies": { "@yuku-toolchain/types": "0.5.43" }, "optionalDependencies": { "@yuku-parser/binding-darwin-arm64": "0.5.48", "@yuku-parser/binding-darwin-x64": "0.5.48", "@yuku-parser/binding-freebsd-x64": "0.5.48", "@yuku-parser/binding-linux-arm-gnu": "0.5.48", "@yuku-parser/binding-linux-arm-musl": "0.5.48", "@yuku-parser/binding-linux-arm64-gnu": "0.5.48", "@yuku-parser/binding-linux-arm64-musl": "0.5.48", "@yuku-parser/binding-linux-x64-gnu": "0.5.48", "@yuku-parser/binding-linux-x64-musl": "0.5.48", "@yuku-parser/binding-win32-arm64": "0.5.48", "@yuku-parser/binding-win32-x64": "0.5.48" } }, "sha512-OWBfhrpgK9+/4+IXG9oT8Bao4AhViQA7vdyNNH7EUg8dQYgwa70XtIBWTpCEme1P1ECyoDNYkn0wT63f8XRcVA=="], - "zod": ["zod@4.4.3", "", {}, "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ=="], + "zod": ["zod@4.5.4", "", {}, "sha512-sC95tT5iHHH9gtpj6A81kh+NEaRAUFN+qlUPDUbRfOMvNf5QCBqsb3WgvnpVtK5Y+4UfA6KqufotuTvMGiTlsA=="], "zod-to-json-schema": ["zod-to-json-schema@3.25.2", "", { "peerDependencies": { "zod": "^3.25.28 || ^4" } }, "sha512-O/PgfnpT1xKSDeQYSCfRI5Gy3hPf91mKVDuYLUHZJMiDFptvP41MSnWofm8dnCm0256ZNfZIM7DSzuSMAFnjHA=="], @@ -2834,7 +2816,7 @@ "@ag-ui/core/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="], - "@apm-js-collab/code-transformer-bundler-plugins/es-module-lexer": ["es-module-lexer@2.3.1", "", {}, "sha512-shc1dbU90Yl/xq1QrC7QRtfcwURZuVRfPhZbDoldJ1cn1gzDvBaBWlv0eFolj5+0znnPJz5TXLxsN77X/12KTA=="], + "@antfu/install-pkg/package-manager-detector": ["package-manager-detector@1.7.0", "", {}, "sha512-xg1eHpwYL/D/HEdWw2goFZP6vV0FH7W+PZ5rFkGjdIDLtxq7EkzBUeT3m+lndYCt8wKbmofUu1MUdMCXkCk9ZQ=="], "@ardatan/relay-compiler/@babel/runtime": ["@babel/runtime@8.0.0", "", {}, "sha512-sL6cvO2IfkSu/iU+zs2S/w01B7A8V7suXSIKEN4hPFFdZoiPGxrj5pAG0lCaqLWiEIrjKzdznIWuaLcxPR53qw=="], @@ -2854,20 +2836,34 @@ "@eslint-community/eslint-utils/eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="], - "@graphql-codegen/cli/@graphql-codegen/client-preset": ["@graphql-codegen/client-preset@6.1.0", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.20.2", "@babel/template": "^7.20.7", "@graphql-codegen/add": "^7.1.0", "@graphql-codegen/gql-tag-operations": "^6.1.0", "@graphql-codegen/plugin-helpers": "^7.1.0", "@graphql-codegen/typed-document-node": "^7.1.0", "@graphql-codegen/typescript": "^6.1.0", "@graphql-codegen/typescript-operations": "^6.1.0", "@graphql-codegen/visitor-plugin-common": "^7.2.0", "@graphql-tools/documents": "^1.0.0", "@graphql-tools/utils": "^11.2.0", "@graphql-typed-document-node/core": "3.2.0", "tslib": "^2.8.0" }, "peerDependencies": { "graphql": "^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0", "graphql-sock": "^1.0.0" }, "optionalPeers": ["graphql-sock"] }, "sha512-mGmBuwrOU5oRoaWFodx8g9xu1jecYIiydqvk88QsAIsyMcZwuoybs1lyne85TovpBHjH5CC2wnZGsbDQfcgOCQ=="], + "@graphql-codegen/add/@graphql-codegen/plugin-helpers": ["@graphql-codegen/plugin-helpers@7.1.0", "", { "dependencies": { "@graphql-tools/utils": "^11.2.0", "change-case-all": "^2.1.0", "common-tags": "1.8.2", "import-from": "4.0.0", "tslib": "^2.8.0" }, "peerDependencies": { "graphql": "^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" } }, "sha512-ieJH7kZ5oSZKBPJs7CvHMrFY/CLYLklqv74ir93qMwRna6geZsbIMoJzTDBXohxcQTITiProiYSGrEtZjIpYGg=="], + + "@graphql-codegen/client-preset/@graphql-codegen/plugin-helpers": ["@graphql-codegen/plugin-helpers@7.1.0", "", { "dependencies": { "@graphql-tools/utils": "^11.2.0", "change-case-all": "^2.1.0", "common-tags": "1.8.2", "import-from": "4.0.0", "tslib": "^2.8.0" }, "peerDependencies": { "graphql": "^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" } }, "sha512-ieJH7kZ5oSZKBPJs7CvHMrFY/CLYLklqv74ir93qMwRna6geZsbIMoJzTDBXohxcQTITiProiYSGrEtZjIpYGg=="], + + "@graphql-codegen/core/@graphql-codegen/plugin-helpers": ["@graphql-codegen/plugin-helpers@7.1.0", "", { "dependencies": { "@graphql-tools/utils": "^11.2.0", "change-case-all": "^2.1.0", "common-tags": "1.8.2", "import-from": "4.0.0", "tslib": "^2.8.0" }, "peerDependencies": { "graphql": "^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" } }, "sha512-ieJH7kZ5oSZKBPJs7CvHMrFY/CLYLklqv74ir93qMwRna6geZsbIMoJzTDBXohxcQTITiProiYSGrEtZjIpYGg=="], + + "@graphql-codegen/gql-tag-operations/@graphql-codegen/plugin-helpers": ["@graphql-codegen/plugin-helpers@7.1.0", "", { "dependencies": { "@graphql-tools/utils": "^11.2.0", "change-case-all": "^2.1.0", "common-tags": "1.8.2", "import-from": "4.0.0", "tslib": "^2.8.0" }, "peerDependencies": { "graphql": "^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" } }, "sha512-ieJH7kZ5oSZKBPJs7CvHMrFY/CLYLklqv74ir93qMwRna6geZsbIMoJzTDBXohxcQTITiProiYSGrEtZjIpYGg=="], "@graphql-codegen/gql-tag-operations/@graphql-codegen/visitor-plugin-common": ["@graphql-codegen/visitor-plugin-common@7.2.0", "", { "dependencies": { "@graphql-codegen/plugin-helpers": "^7.1.0", "@graphql-tools/optimize": "^2.0.0", "@graphql-tools/relay-operation-optimizer": "^7.1.1", "@graphql-tools/utils": "^11.2.0", "auto-bind": "^5.0.0", "change-case-all": "^2.1.0", "dependency-graph": "^1.0.0", "graphql-tag": "^2.11.0", "parse-filepath": "^1.0.2", "tslib": "^2.8.0" }, "peerDependencies": { "graphql": "^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" } }, "sha512-qqtTY8taONuxlR0HvD8z+zgWC3CfJ47M2rATx+geWNIN8v8Itc5TflHYxjkWAtNL6E3q7WZRY5P7se+2S3EjBg=="], + "@graphql-codegen/schema-ast/@graphql-codegen/plugin-helpers": ["@graphql-codegen/plugin-helpers@7.1.0", "", { "dependencies": { "@graphql-tools/utils": "^11.2.0", "change-case-all": "^2.1.0", "common-tags": "1.8.2", "import-from": "4.0.0", "tslib": "^2.8.0" }, "peerDependencies": { "graphql": "^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" } }, "sha512-ieJH7kZ5oSZKBPJs7CvHMrFY/CLYLklqv74ir93qMwRna6geZsbIMoJzTDBXohxcQTITiProiYSGrEtZjIpYGg=="], + + "@graphql-codegen/typed-document-node/@graphql-codegen/plugin-helpers": ["@graphql-codegen/plugin-helpers@7.1.0", "", { "dependencies": { "@graphql-tools/utils": "^11.2.0", "change-case-all": "^2.1.0", "common-tags": "1.8.2", "import-from": "4.0.0", "tslib": "^2.8.0" }, "peerDependencies": { "graphql": "^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" } }, "sha512-ieJH7kZ5oSZKBPJs7CvHMrFY/CLYLklqv74ir93qMwRna6geZsbIMoJzTDBXohxcQTITiProiYSGrEtZjIpYGg=="], + "@graphql-codegen/typed-document-node/@graphql-codegen/visitor-plugin-common": ["@graphql-codegen/visitor-plugin-common@7.2.0", "", { "dependencies": { "@graphql-codegen/plugin-helpers": "^7.1.0", "@graphql-tools/optimize": "^2.0.0", "@graphql-tools/relay-operation-optimizer": "^7.1.1", "@graphql-tools/utils": "^11.2.0", "auto-bind": "^5.0.0", "change-case-all": "^2.1.0", "dependency-graph": "^1.0.0", "graphql-tag": "^2.11.0", "parse-filepath": "^1.0.2", "tslib": "^2.8.0" }, "peerDependencies": { "graphql": "^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" } }, "sha512-qqtTY8taONuxlR0HvD8z+zgWC3CfJ47M2rATx+geWNIN8v8Itc5TflHYxjkWAtNL6E3q7WZRY5P7se+2S3EjBg=="], + "@graphql-codegen/typescript/@graphql-codegen/plugin-helpers": ["@graphql-codegen/plugin-helpers@7.1.0", "", { "dependencies": { "@graphql-tools/utils": "^11.2.0", "change-case-all": "^2.1.0", "common-tags": "1.8.2", "import-from": "4.0.0", "tslib": "^2.8.0" }, "peerDependencies": { "graphql": "^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" } }, "sha512-ieJH7kZ5oSZKBPJs7CvHMrFY/CLYLklqv74ir93qMwRna6geZsbIMoJzTDBXohxcQTITiProiYSGrEtZjIpYGg=="], + "@graphql-codegen/typescript/@graphql-codegen/visitor-plugin-common": ["@graphql-codegen/visitor-plugin-common@7.2.0", "", { "dependencies": { "@graphql-codegen/plugin-helpers": "^7.1.0", "@graphql-tools/optimize": "^2.0.0", "@graphql-tools/relay-operation-optimizer": "^7.1.1", "@graphql-tools/utils": "^11.2.0", "auto-bind": "^5.0.0", "change-case-all": "^2.1.0", "dependency-graph": "^1.0.0", "graphql-tag": "^2.11.0", "parse-filepath": "^1.0.2", "tslib": "^2.8.0" }, "peerDependencies": { "graphql": "^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" } }, "sha512-qqtTY8taONuxlR0HvD8z+zgWC3CfJ47M2rATx+geWNIN8v8Itc5TflHYxjkWAtNL6E3q7WZRY5P7se+2S3EjBg=="], + "@graphql-codegen/typescript-operations/@graphql-codegen/plugin-helpers": ["@graphql-codegen/plugin-helpers@7.1.0", "", { "dependencies": { "@graphql-tools/utils": "^11.2.0", "change-case-all": "^2.1.0", "common-tags": "1.8.2", "import-from": "4.0.0", "tslib": "^2.8.0" }, "peerDependencies": { "graphql": "^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" } }, "sha512-ieJH7kZ5oSZKBPJs7CvHMrFY/CLYLklqv74ir93qMwRna6geZsbIMoJzTDBXohxcQTITiProiYSGrEtZjIpYGg=="], + + "@graphql-codegen/visitor-plugin-common/@graphql-codegen/plugin-helpers": ["@graphql-codegen/plugin-helpers@7.1.0", "", { "dependencies": { "@graphql-tools/utils": "^11.2.0", "change-case-all": "^2.1.0", "common-tags": "1.8.2", "import-from": "4.0.0", "tslib": "^2.8.0" }, "peerDependencies": { "graphql": "^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" } }, "sha512-ieJH7kZ5oSZKBPJs7CvHMrFY/CLYLklqv74ir93qMwRna6geZsbIMoJzTDBXohxcQTITiProiYSGrEtZjIpYGg=="], + "@modelcontextprotocol/sdk/hono": ["hono@4.12.30", "", {}, "sha512-emn+JoJjrN9YTpRDS5it/UI2SO9BAE37T6I3d963RxcZ81G9A4pr2SZTEiiaiKbzx+NKRg5BZ89fCL7gCJCUog=="], "@mosoo/agent-driver/typescript": ["typescript@7.0.2", "", { "optionalDependencies": { "@typescript/typescript-aix-ppc64": "7.0.2", "@typescript/typescript-darwin-arm64": "7.0.2", "@typescript/typescript-darwin-x64": "7.0.2", "@typescript/typescript-freebsd-arm64": "7.0.2", "@typescript/typescript-freebsd-x64": "7.0.2", "@typescript/typescript-linux-arm": "7.0.2", "@typescript/typescript-linux-arm64": "7.0.2", "@typescript/typescript-linux-loong64": "7.0.2", "@typescript/typescript-linux-mips64el": "7.0.2", "@typescript/typescript-linux-ppc64": "7.0.2", "@typescript/typescript-linux-riscv64": "7.0.2", "@typescript/typescript-linux-s390x": "7.0.2", "@typescript/typescript-linux-x64": "7.0.2", "@typescript/typescript-netbsd-arm64": "7.0.2", "@typescript/typescript-netbsd-x64": "7.0.2", "@typescript/typescript-openbsd-arm64": "7.0.2", "@typescript/typescript-openbsd-x64": "7.0.2", "@typescript/typescript-sunos-x64": "7.0.2", "@typescript/typescript-win32-arm64": "7.0.2", "@typescript/typescript-win32-x64": "7.0.2" }, "bin": { "tsc": "bin/tsc" } }, "sha512-8FYau96o3NKOhbjKi/qNvG/W5jhzxkbdm5sj9AbZ/5T5sWqn3hJgLfGx27sRKZWTvyzCP8dLRBTf5tBTSRVUNA=="], - "@mosoo/agent-driver/vite-plus": ["vite-plus@0.3.0", "", { "dependencies": { "@oxc-project/types": "=0.146.0", "@oxlint/plugins": "=1.79.0", "@vitest/browser": "4.1.11", "@vitest/browser-preview": "4.1.11", "@vitest/expect": "4.1.11", "@vitest/mocker": "4.1.11", "@vitest/pretty-format": "4.1.11", "@vitest/runner": "4.1.11", "@vitest/snapshot": "4.1.11", "@vitest/spy": "4.1.11", "@vitest/utils": "4.1.11", "@voidzero-dev/vite-plus-core": "0.3.0", "oxfmt": "=0.64.0", "oxlint": "=1.79.0", "oxlint-tsgolint": "=7.0.2001", "vitest": "4.1.11" }, "optionalDependencies": { "@voidzero-dev/vite-plus-darwin-arm64": "0.3.0", "@voidzero-dev/vite-plus-darwin-x64": "0.3.0", "@voidzero-dev/vite-plus-linux-arm64-gnu": "0.3.0", "@voidzero-dev/vite-plus-linux-arm64-musl": "0.3.0", "@voidzero-dev/vite-plus-linux-x64-gnu": "0.3.0", "@voidzero-dev/vite-plus-linux-x64-musl": "0.3.0", "@voidzero-dev/vite-plus-win32-arm64-msvc": "0.3.0", "@voidzero-dev/vite-plus-win32-x64-msvc": "0.3.0" }, "peerDependencies": { "@vitest/browser-playwright": "4.1.11", "@vitest/browser-webdriverio": "4.1.11" }, "optionalPeers": ["@vitest/browser-playwright", "@vitest/browser-webdriverio"], "bin": { "oxfmt": "./bin/oxfmt", "oxlint": "./bin/oxlint", "vp": "./bin/vp", "vpr": "./bin/vpr" } }, "sha512-GNWbWuWD37frCSFrz6MLzUo62bTv5IOJozHEgZYOkxsLkuQtTwm4TowzpfoGrSsfwhAAtfPd/sK1Y0+v1SwhZA=="], - "@mosoo/api/@types/node": ["@types/node@25.9.5", "", { "dependencies": { "undici-types": ">=7.24.0 <7.24.7" } }, "sha512-OScDchr2fwuUmWdf4kZ9h7PcJiYDVInhJizG/biAq3cAvqwYktuy/TYGGdZNMtNTFUP7rnb0NU4TUdm82kt4Rg=="], "@mosoo/db/@types/node": ["@types/node@25.9.5", "", { "dependencies": { "undici-types": ">=7.24.0 <7.24.7" } }, "sha512-OScDchr2fwuUmWdf4kZ9h7PcJiYDVInhJizG/biAq3cAvqwYktuy/TYGGdZNMtNTFUP7rnb0NU4TUdm82kt4Rg=="], @@ -2894,10 +2890,6 @@ "@tailwindcss/oxide-wasm32-wasi/tslib": ["tslib@2.8.1", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], - "@voidzero-dev/vite-plus-core/lightningcss": ["lightningcss@1.32.0", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-android-arm64": "1.32.0", "lightningcss-darwin-arm64": "1.32.0", "lightningcss-darwin-x64": "1.32.0", "lightningcss-freebsd-x64": "1.32.0", "lightningcss-linux-arm-gnueabihf": "1.32.0", "lightningcss-linux-arm64-gnu": "1.32.0", "lightningcss-linux-arm64-musl": "1.32.0", "lightningcss-linux-x64-gnu": "1.32.0", "lightningcss-linux-x64-musl": "1.32.0", "lightningcss-win32-arm64-msvc": "1.32.0", "lightningcss-win32-x64-msvc": "1.32.0" } }, "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ=="], - - "@voidzero-dev/vite-plus-core/postcss": ["postcss@8.5.19", "", { "dependencies": { "nanoid": "^3.3.12", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-Mz8SaolMd8nB+G13WkORcxQKHZ/NE4xXevtkJHVuG+guo9/wYKlIMTKAqGdEmYOXR2ijPjTYNHssizdaVSUNdQ=="], - "assistant-stream/nanoid": ["nanoid@6.0.1", "", { "bin": { "nanoid": "bin/nanoid.js" } }, "sha512-3wVS3i51pE2pi1k5FFL/95BGfVS0kSsvDVuGXHOtxox/TywUmtgq+3qiTOTbs9J7KfHaXPiN171k/A6dBnaXFw=="], "better-call/@better-auth/utils": ["@better-auth/utils@0.5.0", "", { "dependencies": { "@noble/hashes": "^2.0.1" } }, "sha512-BL8W4EfIZFwlu0r54m3v1ztjDhu6dDe/amLTm0xybmbZaNgYUqhD3SjpAsnq0q8YD6/ki4iwIgxJNLP/N3TxiA=="], @@ -2922,6 +2914,8 @@ "deslop-js/@oxc-project/types": ["@oxc-project/types@0.143.0", "", {}, "sha512-u6JZdLBTLotrNC9Vd6vPssINdzcCzleKAH6EJKImQb7GtYvX5keN2dxkoK44stCc4tffE6QQRtZTXVSzsLUlWA=="], + "deslop-js/oxc-parser": ["oxc-parser@0.143.0", "", { "dependencies": { "@oxc-project/types": "^0.143.0" }, "optionalDependencies": { "@oxc-parser/binding-android-arm-eabi": "0.143.0", "@oxc-parser/binding-android-arm64": "0.143.0", "@oxc-parser/binding-darwin-arm64": "0.143.0", "@oxc-parser/binding-darwin-x64": "0.143.0", "@oxc-parser/binding-freebsd-x64": "0.143.0", "@oxc-parser/binding-linux-arm-gnueabihf": "0.143.0", "@oxc-parser/binding-linux-arm-musleabihf": "0.143.0", "@oxc-parser/binding-linux-arm64-gnu": "0.143.0", "@oxc-parser/binding-linux-arm64-musl": "0.143.0", "@oxc-parser/binding-linux-ppc64-gnu": "0.143.0", "@oxc-parser/binding-linux-riscv64-gnu": "0.143.0", "@oxc-parser/binding-linux-riscv64-musl": "0.143.0", "@oxc-parser/binding-linux-s390x-gnu": "0.143.0", "@oxc-parser/binding-linux-x64-gnu": "0.143.0", "@oxc-parser/binding-linux-x64-musl": "0.143.0", "@oxc-parser/binding-openharmony-arm64": "0.143.0", "@oxc-parser/binding-win32-arm64-msvc": "0.143.0", "@oxc-parser/binding-win32-ia32-msvc": "0.143.0", "@oxc-parser/binding-win32-x64-msvc": "0.143.0" } }, "sha512-ov0NzaDCOInknS7mP1cwKdJERt3utPW8ldjtdUXQ8Ty0GEFD08wk422vCUN0d7pST6kqtV7dxoI9w1Zi0l/9TA=="], + "deslop-js/typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], "drizzle-kit/esbuild": ["esbuild@0.25.12", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.12", "@esbuild/android-arm": "0.25.12", "@esbuild/android-arm64": "0.25.12", "@esbuild/android-x64": "0.25.12", "@esbuild/darwin-arm64": "0.25.12", "@esbuild/darwin-x64": "0.25.12", "@esbuild/freebsd-arm64": "0.25.12", "@esbuild/freebsd-x64": "0.25.12", "@esbuild/linux-arm": "0.25.12", "@esbuild/linux-arm64": "0.25.12", "@esbuild/linux-ia32": "0.25.12", "@esbuild/linux-loong64": "0.25.12", "@esbuild/linux-mips64el": "0.25.12", "@esbuild/linux-ppc64": "0.25.12", "@esbuild/linux-riscv64": "0.25.12", "@esbuild/linux-s390x": "0.25.12", "@esbuild/linux-x64": "0.25.12", "@esbuild/netbsd-arm64": "0.25.12", "@esbuild/netbsd-x64": "0.25.12", "@esbuild/openbsd-arm64": "0.25.12", "@esbuild/openbsd-x64": "0.25.12", "@esbuild/openharmony-arm64": "0.25.12", "@esbuild/sunos-x64": "0.25.12", "@esbuild/win32-arm64": "0.25.12", "@esbuild/win32-ia32": "0.25.12", "@esbuild/win32-x64": "0.25.12" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg=="], @@ -2932,8 +2926,12 @@ "eslint/glob-parent": ["glob-parent@6.0.2", "", { "dependencies": { "is-glob": "^4.0.3" } }, "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A=="], + "eslint/ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="], + "express/cookie": ["cookie@0.7.2", "", {}, "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w=="], + "globby/ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="], + "graphql-config/cosmiconfig": ["cosmiconfig@8.3.6", "", { "dependencies": { "import-fresh": "^3.3.0", "js-yaml": "^4.1.0", "parse-json": "^5.2.0", "path-type": "^4.0.0" }, "peerDependencies": { "typescript": ">=4.9.5" }, "optionalPeers": ["typescript"] }, "sha512-kcZ6+W5QzcJ3P1Mt+83OUv/oHFqZHIx8DuxG6eZ5RGMERoLqp4BuGjhHLYGK+Kf5XVkQvqBSmAy/nGWN3qDgEA=="], "graphql-yoga/@graphql-tools/utils": ["@graphql-tools/utils@10.11.0", "", { "dependencies": { "@graphql-typed-document-node/core": "^3.1.1", "@whatwg-node/promise-helpers": "^1.0.0", "cross-inspect": "1.0.1", "tslib": "^2.4.0" }, "peerDependencies": { "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" } }, "sha512-iBFR9GXIs0gCD+yc3hoNswViL1O5josI33dUqiNStFI/MHLCEPduasceAcazRH77YONKNiviHBV8f7OgcT4o2Q=="], @@ -2942,8 +2940,6 @@ "import-fresh/resolve-from": ["resolve-from@4.0.0", "", {}, "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g=="], - "import-in-the-middle/es-module-lexer": ["es-module-lexer@2.3.1", "", {}, "sha512-shc1dbU90Yl/xq1QrC7QRtfcwURZuVRfPhZbDoldJ1cn1gzDvBaBWlv0eFolj5+0znnPJz5TXLxsN77X/12KTA=="], - "jsdom/lru-cache": ["lru-cache@11.5.2", "", {}, "sha512-4pfM1Ff0x50o0tQwb5ucw/RzNyD0/YJME6IVcStalZuMWxdt3sR3huStTtxz4PUmvZfRguvDejasvQ2kifR11g=="], "katex/commander": ["commander@8.3.0", "", {}, "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww=="], @@ -2962,7 +2958,9 @@ "miniflare/ws": ["ws@8.21.0", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g=="], - "oxc-parser/@oxc-project/types": ["@oxc-project/types@0.143.0", "", {}, "sha512-u6JZdLBTLotrNC9Vd6vPssINdzcCzleKAH6EJKImQb7GtYvX5keN2dxkoK44stCc4tffE6QQRtZTXVSzsLUlWA=="], + "oxc-parser/@oxc-project/types": ["@oxc-project/types@0.147.0", "", {}, "sha512-IJ3s6ltHLp45S0bh7phkX+gJO7A1Wuz2EaqpAhb8WjqDwbzMiWKHhyyT42tskaWjEYXtHtVCPpnBJVT9+dcRLg=="], + + "oxlint-plugin-react-doctor/oxc-parser": ["oxc-parser@0.143.0", "", { "dependencies": { "@oxc-project/types": "^0.143.0" }, "optionalDependencies": { "@oxc-parser/binding-android-arm-eabi": "0.143.0", "@oxc-parser/binding-android-arm64": "0.143.0", "@oxc-parser/binding-darwin-arm64": "0.143.0", "@oxc-parser/binding-darwin-x64": "0.143.0", "@oxc-parser/binding-freebsd-x64": "0.143.0", "@oxc-parser/binding-linux-arm-gnueabihf": "0.143.0", "@oxc-parser/binding-linux-arm-musleabihf": "0.143.0", "@oxc-parser/binding-linux-arm64-gnu": "0.143.0", "@oxc-parser/binding-linux-arm64-musl": "0.143.0", "@oxc-parser/binding-linux-ppc64-gnu": "0.143.0", "@oxc-parser/binding-linux-riscv64-gnu": "0.143.0", "@oxc-parser/binding-linux-riscv64-musl": "0.143.0", "@oxc-parser/binding-linux-s390x-gnu": "0.143.0", "@oxc-parser/binding-linux-x64-gnu": "0.143.0", "@oxc-parser/binding-linux-x64-musl": "0.143.0", "@oxc-parser/binding-openharmony-arm64": "0.143.0", "@oxc-parser/binding-win32-arm64-msvc": "0.143.0", "@oxc-parser/binding-win32-ia32-msvc": "0.143.0", "@oxc-parser/binding-win32-x64-msvc": "0.143.0" } }, "sha512-ov0NzaDCOInknS7mP1cwKdJERt3utPW8ldjtdUXQ8Ty0GEFD08wk422vCUN0d7pST6kqtV7dxoI9w1Zi0l/9TA=="], "parse-entities/@types/unist": ["@types/unist@2.0.11", "", {}, "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA=="], @@ -2982,11 +2980,15 @@ "sync-fetch/whatwg-mimetype": ["whatwg-mimetype@4.0.0", "", {}, "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg=="], + "tinyglobby/picomatch": ["picomatch@4.0.5", "", {}, "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A=="], + "type-is/content-type": ["content-type@2.0.0", "", {}, "sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ=="], - "vite-plus/oxlint": ["oxlint@1.67.0", "", { "optionalDependencies": { "@oxlint/binding-android-arm-eabi": "1.67.0", "@oxlint/binding-android-arm64": "1.67.0", "@oxlint/binding-darwin-arm64": "1.67.0", "@oxlint/binding-darwin-x64": "1.67.0", "@oxlint/binding-freebsd-x64": "1.67.0", "@oxlint/binding-linux-arm-gnueabihf": "1.67.0", "@oxlint/binding-linux-arm-musleabihf": "1.67.0", "@oxlint/binding-linux-arm64-gnu": "1.67.0", "@oxlint/binding-linux-arm64-musl": "1.67.0", "@oxlint/binding-linux-ppc64-gnu": "1.67.0", "@oxlint/binding-linux-riscv64-gnu": "1.67.0", "@oxlint/binding-linux-riscv64-musl": "1.67.0", "@oxlint/binding-linux-s390x-gnu": "1.67.0", "@oxlint/binding-linux-x64-gnu": "1.67.0", "@oxlint/binding-linux-x64-musl": "1.67.0", "@oxlint/binding-openharmony-arm64": "1.67.0", "@oxlint/binding-win32-arm64-msvc": "1.67.0", "@oxlint/binding-win32-ia32-msvc": "1.67.0", "@oxlint/binding-win32-x64-msvc": "1.67.0" }, "peerDependencies": { "oxlint-tsgolint": ">=0.22.1", "vite-plus": "*" }, "optionalPeers": ["oxlint-tsgolint", "vite-plus"], "bin": { "oxlint": "bin/oxlint" } }, "sha512-blwwaHPdoH8piQ5/z0KHeoHFR7FZgl12WluKJfu4qFLPkZl6mK04PkLE45Fw1NxfBRSlh40Gu7MkxHUw++ociQ=="], + "vite/picomatch": ["picomatch@4.0.5", "", {}, "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A=="], - "vitest/es-module-lexer": ["es-module-lexer@2.3.1", "", {}, "sha512-shc1dbU90Yl/xq1QrC7QRtfcwURZuVRfPhZbDoldJ1cn1gzDvBaBWlv0eFolj5+0znnPJz5TXLxsN77X/12KTA=="], + "vite-plus/oxlint": ["oxlint@1.79.0", "", { "optionalDependencies": { "@oxlint/binding-android-arm-eabi": "1.79.0", "@oxlint/binding-android-arm64": "1.79.0", "@oxlint/binding-darwin-arm64": "1.79.0", "@oxlint/binding-darwin-x64": "1.79.0", "@oxlint/binding-freebsd-x64": "1.79.0", "@oxlint/binding-linux-arm-gnueabihf": "1.79.0", "@oxlint/binding-linux-arm-musleabihf": "1.79.0", "@oxlint/binding-linux-arm64-gnu": "1.79.0", "@oxlint/binding-linux-arm64-musl": "1.79.0", "@oxlint/binding-linux-ppc64-gnu": "1.79.0", "@oxlint/binding-linux-riscv64-gnu": "1.79.0", "@oxlint/binding-linux-riscv64-musl": "1.79.0", "@oxlint/binding-linux-s390x-gnu": "1.79.0", "@oxlint/binding-linux-x64-gnu": "1.79.0", "@oxlint/binding-linux-x64-musl": "1.79.0", "@oxlint/binding-openharmony-arm64": "1.79.0", "@oxlint/binding-win32-arm64-msvc": "1.79.0", "@oxlint/binding-win32-ia32-msvc": "1.79.0", "@oxlint/binding-win32-x64-msvc": "1.79.0" }, "peerDependencies": { "oxlint-tsgolint": ">=7.0.2001", "vite-plus": "*" }, "optionalPeers": ["oxlint-tsgolint", "vite-plus"], "bin": { "oxlint": "bin/oxlint" } }, "sha512-hVJ9hq9m2unPS+Of4eJJgCPdIeCC+3DHEUX3tkmrPJr3OK2hz7PhXwgC+ZP71ZcYu8cCDEtQrqLxWNvxBppBVg=="], + + "vitest/picomatch": ["picomatch@4.0.5", "", {}, "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A=="], "wrap-ansi/string-width": ["string-width@8.2.2", "", { "dependencies": { "get-east-asian-width": "^1.5.0", "strip-ansi": "^7.1.2" } }, "sha512-GaPUh5gfdrYzqeVNZvUfT23vYYxXzKYidUcnMtJg/3rxRV63EFZy3k6xfKlmfeJD0176lnUV/Usr3XcwSvFzpg=="], @@ -3034,38 +3036,6 @@ "@esbuild-kit/core-utils/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.18.20", "", { "os": "win32", "cpu": "x64" }, "sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ=="], - "@graphql-codegen/cli/@graphql-codegen/client-preset/@graphql-codegen/typescript-operations": ["@graphql-codegen/typescript-operations@6.1.0", "", { "dependencies": { "@graphql-codegen/plugin-helpers": "^7.1.0", "@graphql-codegen/schema-ast": "^6.1.0", "@graphql-codegen/visitor-plugin-common": "^7.2.0", "auto-bind": "^5.0.0", "tslib": "^2.8.0" }, "peerDependencies": { "graphql": "^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0", "graphql-sock": "^1.0.0" }, "optionalPeers": ["graphql-sock"] }, "sha512-zv0ohBJqLP1G/Kgiq1MhLTdNmakChtJivpaMaBmuPz9gasKWJkc9MhxuVytF6xS27PkJOh81TNM9srAkqpdejA=="], - - "@graphql-codegen/cli/@graphql-codegen/client-preset/@graphql-codegen/visitor-plugin-common": ["@graphql-codegen/visitor-plugin-common@7.2.0", "", { "dependencies": { "@graphql-codegen/plugin-helpers": "^7.1.0", "@graphql-tools/optimize": "^2.0.0", "@graphql-tools/relay-operation-optimizer": "^7.1.1", "@graphql-tools/utils": "^11.2.0", "auto-bind": "^5.0.0", "change-case-all": "^2.1.0", "dependency-graph": "^1.0.0", "graphql-tag": "^2.11.0", "parse-filepath": "^1.0.2", "tslib": "^2.8.0" }, "peerDependencies": { "graphql": "^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" } }, "sha512-qqtTY8taONuxlR0HvD8z+zgWC3CfJ47M2rATx+geWNIN8v8Itc5TflHYxjkWAtNL6E3q7WZRY5P7se+2S3EjBg=="], - - "@mosoo/agent-driver/vite-plus/@oxc-project/types": ["@oxc-project/types@0.146.0", "", {}, "sha512-XC0QsnnhVe7sLIWmYmdPw7x5P0h4W8vUU3Nv1ySgWXtvCz8NizoAEpGXA0sOYoJQV2Rl13LgURAHQ5cI5ILCSA=="], - - "@mosoo/agent-driver/vite-plus/@oxlint/plugins": ["@oxlint/plugins@1.79.0", "", {}, "sha512-S0uyoxakDINJ4DPgqxGlEEvrdSMeQb7Z2lKVjxoY2gwsbZbfg2Xr8Klfeo5ZeraHmmdBCELFUHkSe6KEmBpMvg=="], - - "@mosoo/agent-driver/vite-plus/@voidzero-dev/vite-plus-core": ["@voidzero-dev/vite-plus-core@0.3.0", "", { "dependencies": { "@oxc-project/runtime": "=0.146.0", "@oxc-project/types": "=0.146.0", "lightningcss": "^1.33.0", "postcss": "^8.5.6", "yuku-codegen": "^0.5.44", "yuku-parser": "^0.5.44" }, "optionalDependencies": { "@voidzero-dev/vite-plus-darwin-arm64": "0.3.0", "@voidzero-dev/vite-plus-darwin-x64": "0.3.0", "@voidzero-dev/vite-plus-linux-arm64-gnu": "0.3.0", "@voidzero-dev/vite-plus-linux-arm64-musl": "0.3.0", "@voidzero-dev/vite-plus-linux-x64-gnu": "0.3.0", "@voidzero-dev/vite-plus-linux-x64-musl": "0.3.0", "@voidzero-dev/vite-plus-win32-arm64-msvc": "0.3.0", "@voidzero-dev/vite-plus-win32-x64-msvc": "0.3.0", "fsevents": "~2.3.3" }, "peerDependencies": { "@arethetypeswrong/core": "^0.18.1", "@types/node": "^20.19.0 || >=22.12.0", "@vitejs/devtools": "^0.4.0 || ^0.5.0", "esbuild": "^0.27.0 || ^0.28.0", "jiti": ">=1.21.0", "less": "^4.0.0", "publint": "^0.3.8", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "typescript": "^5.0.0 || ^6.0.0 || ^7.0.0", "unplugin-unused": "^0.5.0", "unrun": "*", "yaml": "^2.4.2" }, "optionalPeers": ["@arethetypeswrong/core", "@types/node", "@vitejs/devtools", "esbuild", "jiti", "less", "publint", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "typescript", "unplugin-unused", "unrun", "yaml"] }, "sha512-aOqoqIWaF+Q/geDU48pC2rVFEVSvLV1GGj/NdvhUiBhCZntoFNbwI+hjUeG8BMaPG67sOV6ey+/sgkdmGmKqaw=="], - - "@mosoo/agent-driver/vite-plus/@voidzero-dev/vite-plus-darwin-arm64": ["@voidzero-dev/vite-plus-darwin-arm64@0.3.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-9ADr1egZ8T4tJOqrpQLhoDl95Y74R95+bsvjmin0gy1C0eQVhpmcNnBfb07KFNhJioJp9MMO7F7Dx4fQL5SKsw=="], - - "@mosoo/agent-driver/vite-plus/@voidzero-dev/vite-plus-darwin-x64": ["@voidzero-dev/vite-plus-darwin-x64@0.3.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-GegasVCwNeDOkNyvhLOuwU1+T2JkjY/Tq+SOvwphUpVcqQ6OOAUq9LlpoXviO2QL/Kq2NbMYjiAfPKVSTLUFQw=="], - - "@mosoo/agent-driver/vite-plus/@voidzero-dev/vite-plus-linux-arm64-gnu": ["@voidzero-dev/vite-plus-linux-arm64-gnu@0.3.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-nYI3KNYXkXjRPsSdR4Lr7J2xMxfR1+TplWlG/dV37qVXWAjbyHpoAlbULjZBAVJMyXRNlcADhBrEwXe4g6s48A=="], - - "@mosoo/agent-driver/vite-plus/@voidzero-dev/vite-plus-linux-arm64-musl": ["@voidzero-dev/vite-plus-linux-arm64-musl@0.3.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-HRlVA3AOcuGXmOdHhQ+Zv5XAaKbYF9si5rRHoOsKl0UyBo4txA3OoJfmP0WjanfLUNmu85JyO2dO1ptL4C6wgg=="], - - "@mosoo/agent-driver/vite-plus/@voidzero-dev/vite-plus-linux-x64-gnu": ["@voidzero-dev/vite-plus-linux-x64-gnu@0.3.0", "", { "os": "linux", "cpu": "x64" }, "sha512-9A+dFScPfwcrzF/rRR0zH8++2hOf6xtFmN/5LyzyfUywtw9MILXcC72IMcOeL6QRJwKUMsudi1rFeDE59azNvw=="], - - "@mosoo/agent-driver/vite-plus/@voidzero-dev/vite-plus-linux-x64-musl": ["@voidzero-dev/vite-plus-linux-x64-musl@0.3.0", "", { "os": "linux", "cpu": "x64" }, "sha512-KfIV3qaPdaOOE8JQMRHRE34FtZocl9O86XLTP6JMjDUlcx8FPgf8/fz/HFqJ8g232vM+JsgLI/YTVeXP8LkTKw=="], - - "@mosoo/agent-driver/vite-plus/@voidzero-dev/vite-plus-win32-arm64-msvc": ["@voidzero-dev/vite-plus-win32-arm64-msvc@0.3.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-KRhdy5K13AYx9KBfCVHRrK7zSZU+bMW9CL6gTai+UkJgAmDJi1kjdSNboZOjO8mrzUnTCrELgMI2tnstcxSTuA=="], - - "@mosoo/agent-driver/vite-plus/@voidzero-dev/vite-plus-win32-x64-msvc": ["@voidzero-dev/vite-plus-win32-x64-msvc@0.3.0", "", { "os": "win32", "cpu": "x64" }, "sha512-7+G+GxGmxdpQO0zjiGnkZFXKGqm0CrVduebRsJd6ccuOuxCQYPxLcoHq4WOaGrh56SrAGS7XjhnQCrXRkzKUVQ=="], - - "@mosoo/agent-driver/vite-plus/oxfmt": ["oxfmt@0.64.0", "", { "dependencies": { "tinypool": "2.1.0" }, "optionalDependencies": { "@oxfmt/binding-android-arm-eabi": "0.64.0", "@oxfmt/binding-android-arm64": "0.64.0", "@oxfmt/binding-darwin-arm64": "0.64.0", "@oxfmt/binding-darwin-x64": "0.64.0", "@oxfmt/binding-freebsd-x64": "0.64.0", "@oxfmt/binding-linux-arm-gnueabihf": "0.64.0", "@oxfmt/binding-linux-arm-musleabihf": "0.64.0", "@oxfmt/binding-linux-arm64-gnu": "0.64.0", "@oxfmt/binding-linux-arm64-musl": "0.64.0", "@oxfmt/binding-linux-ppc64-gnu": "0.64.0", "@oxfmt/binding-linux-riscv64-gnu": "0.64.0", "@oxfmt/binding-linux-riscv64-musl": "0.64.0", "@oxfmt/binding-linux-s390x-gnu": "0.64.0", "@oxfmt/binding-linux-x64-gnu": "0.64.0", "@oxfmt/binding-linux-x64-musl": "0.64.0", "@oxfmt/binding-openharmony-arm64": "0.64.0", "@oxfmt/binding-win32-arm64-msvc": "0.64.0", "@oxfmt/binding-win32-ia32-msvc": "0.64.0", "@oxfmt/binding-win32-x64-msvc": "0.64.0" }, "peerDependencies": { "svelte": "^5.0.0", "vite-plus": "*" }, "optionalPeers": ["svelte", "vite-plus"], "bin": { "oxfmt": "bin/oxfmt" } }, "sha512-XZ4GFBN/PLbXKq+0zrgpQfPKYuJlUuj+nzZJY7UpIbFMNyefNLCdN9EwViycNqnYcv0wrn0jXcQLlqJp8RCKBg=="], - - "@mosoo/agent-driver/vite-plus/oxlint": ["oxlint@1.79.0", "", { "optionalDependencies": { "@oxlint/binding-android-arm-eabi": "1.79.0", "@oxlint/binding-android-arm64": "1.79.0", "@oxlint/binding-darwin-arm64": "1.79.0", "@oxlint/binding-darwin-x64": "1.79.0", "@oxlint/binding-freebsd-x64": "1.79.0", "@oxlint/binding-linux-arm-gnueabihf": "1.79.0", "@oxlint/binding-linux-arm-musleabihf": "1.79.0", "@oxlint/binding-linux-arm64-gnu": "1.79.0", "@oxlint/binding-linux-arm64-musl": "1.79.0", "@oxlint/binding-linux-ppc64-gnu": "1.79.0", "@oxlint/binding-linux-riscv64-gnu": "1.79.0", "@oxlint/binding-linux-riscv64-musl": "1.79.0", "@oxlint/binding-linux-s390x-gnu": "1.79.0", "@oxlint/binding-linux-x64-gnu": "1.79.0", "@oxlint/binding-linux-x64-musl": "1.79.0", "@oxlint/binding-openharmony-arm64": "1.79.0", "@oxlint/binding-win32-arm64-msvc": "1.79.0", "@oxlint/binding-win32-ia32-msvc": "1.79.0", "@oxlint/binding-win32-x64-msvc": "1.79.0" }, "peerDependencies": { "oxlint-tsgolint": ">=7.0.2001", "vite-plus": "*" }, "optionalPeers": ["oxlint-tsgolint", "vite-plus"], "bin": { "oxlint": "bin/oxlint" } }, "sha512-hVJ9hq9m2unPS+Of4eJJgCPdIeCC+3DHEUX3tkmrPJr3OK2hz7PhXwgC+ZP71ZcYu8cCDEtQrqLxWNvxBppBVg=="], - - "@mosoo/agent-driver/vite-plus/oxlint-tsgolint": ["oxlint-tsgolint@7.0.2001", "", { "optionalDependencies": { "@oxlint-tsgolint/darwin-arm64": "7.0.2001", "@oxlint-tsgolint/darwin-x64": "7.0.2001", "@oxlint-tsgolint/linux-arm64": "7.0.2001", "@oxlint-tsgolint/linux-x64": "7.0.2001", "@oxlint-tsgolint/win32-arm64": "7.0.2001", "@oxlint-tsgolint/win32-x64": "7.0.2001" }, "bin": { "tsgolint": "./bin/tsgolint.js" } }, "sha512-KjK/XLcXr1DSyonKhsuFqJRiuKqcyG9j3LJ8nkOsrLzGvodBPqzHOKauy10asLMDI0sUpvb+1sxlzff3udZvfg=="], - "@mosoo/api/@types/node/undici-types": ["undici-types@7.24.6", "", {}, "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg=="], "@mosoo/db/@types/node/undici-types": ["undici-types@7.24.6", "", {}, "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg=="], @@ -3096,33 +3066,47 @@ "@tailwindcss/node/lightningcss/lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.32.0", "", { "os": "win32", "cpu": "x64" }, "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q=="], - "@voidzero-dev/vite-plus-core/lightningcss/lightningcss-android-arm64": ["lightningcss-android-arm64@1.32.0", "", { "os": "android", "cpu": "arm64" }, "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg=="], + "cytoscape-fcose/cose-base/layout-base": ["layout-base@2.0.1", "", {}, "sha512-dp3s92+uNI1hWIpPGH3jK2kxE2lMjdXdr+DH8ynZHpd6PUlH6x6cbuXnoMmiNumznqaNO31xu9e79F0uuZ0JFg=="], + + "d3-sankey/d3-shape/d3-path": ["d3-path@1.0.9", "", {}, "sha512-VLaYcn81dtHVTjEHd8B+pbe9yHWpXKZUC87PzoFmsFrJqgFwDe/qxfp5MlfsfM1V5E/iVt0MmEbWQ7FVIXh/bg=="], - "@voidzero-dev/vite-plus-core/lightningcss/lightningcss-darwin-arm64": ["lightningcss-darwin-arm64@1.32.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ=="], + "deslop-js/oxc-parser/@oxc-parser/binding-android-arm-eabi": ["@oxc-parser/binding-android-arm-eabi@0.143.0", "", { "os": "android", "cpu": "arm" }, "sha512-n9uozULWflPqBtdmI8lAabLqGKNgLVNN0ZH8HfgCwpKGNtzRzauB76jTiW/3YLkcA7N1zskpi9GdVnZuu1SAvg=="], - "@voidzero-dev/vite-plus-core/lightningcss/lightningcss-darwin-x64": ["lightningcss-darwin-x64@1.32.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w=="], + "deslop-js/oxc-parser/@oxc-parser/binding-android-arm64": ["@oxc-parser/binding-android-arm64@0.143.0", "", { "os": "android", "cpu": "arm64" }, "sha512-9BbdjHETk6O3zH/DDid9IgBtF0GlpLabNKN231uraXpRDSfY+iiZxTP5bk1Z63GBownVdhdINFIeddmMz4MzpQ=="], - "@voidzero-dev/vite-plus-core/lightningcss/lightningcss-freebsd-x64": ["lightningcss-freebsd-x64@1.32.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig=="], + "deslop-js/oxc-parser/@oxc-parser/binding-darwin-arm64": ["@oxc-parser/binding-darwin-arm64@0.143.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-gh+6ecoHUy4/sUcolBl/1qPXKBbYNxFY0Pk0ujgQvINTMSftJY7o4yb8gOkDJPeZeB8+a+u7xTe6umoP8N5HFA=="], - "@voidzero-dev/vite-plus-core/lightningcss/lightningcss-linux-arm-gnueabihf": ["lightningcss-linux-arm-gnueabihf@1.32.0", "", { "os": "linux", "cpu": "arm" }, "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw=="], + "deslop-js/oxc-parser/@oxc-parser/binding-darwin-x64": ["@oxc-parser/binding-darwin-x64@0.143.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-qd1hl2d+lXgHv/VQ/M9qm8TrMC5T4RqDBwtOnl+1D0QMjwcz+8AaB4JSg8STgeag0GP6a6L74XEGAsrTSJWNzQ=="], - "@voidzero-dev/vite-plus-core/lightningcss/lightningcss-linux-arm64-gnu": ["lightningcss-linux-arm64-gnu@1.32.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ=="], + "deslop-js/oxc-parser/@oxc-parser/binding-freebsd-x64": ["@oxc-parser/binding-freebsd-x64@0.143.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-M5XXcNa7aOqLPKTR41msfghKu2yQ4xWvCm11/gwU0JzOzHNk5sgW//rVEjJ+LO48+VDAMzXTSzurUVxIDKwozw=="], - "@voidzero-dev/vite-plus-core/lightningcss/lightningcss-linux-arm64-musl": ["lightningcss-linux-arm64-musl@1.32.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg=="], + "deslop-js/oxc-parser/@oxc-parser/binding-linux-arm-gnueabihf": ["@oxc-parser/binding-linux-arm-gnueabihf@0.143.0", "", { "os": "linux", "cpu": "arm" }, "sha512-T/GXusuOkPNQhCQCSBbcU/N8j0rAypuDBl1IyFK+lyYT594XsVz80clPC/OtbSSpBGyJxj8uYEfctxVuxVYoww=="], - "@voidzero-dev/vite-plus-core/lightningcss/lightningcss-linux-x64-gnu": ["lightningcss-linux-x64-gnu@1.32.0", "", { "os": "linux", "cpu": "x64" }, "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA=="], + "deslop-js/oxc-parser/@oxc-parser/binding-linux-arm-musleabihf": ["@oxc-parser/binding-linux-arm-musleabihf@0.143.0", "", { "os": "linux", "cpu": "arm" }, "sha512-oKu4RcBlXSqo3OC62dp6YTnQaZIurNDpCX3BnAM3+bJxt7s8J2TJKMnC0UYer1qhlRaDCg6wkTaTw+2IlsZ12w=="], - "@voidzero-dev/vite-plus-core/lightningcss/lightningcss-linux-x64-musl": ["lightningcss-linux-x64-musl@1.32.0", "", { "os": "linux", "cpu": "x64" }, "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg=="], + "deslop-js/oxc-parser/@oxc-parser/binding-linux-arm64-gnu": ["@oxc-parser/binding-linux-arm64-gnu@0.143.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-WJBbD186AZmMGaSIhlktC+rPl8L3peCTXAh88Ih9uEvK0en2mPojGyCGYiL6mHtV1RPV3JyfJW5t6n5hh0lXhA=="], - "@voidzero-dev/vite-plus-core/lightningcss/lightningcss-win32-arm64-msvc": ["lightningcss-win32-arm64-msvc@1.32.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw=="], + "deslop-js/oxc-parser/@oxc-parser/binding-linux-arm64-musl": ["@oxc-parser/binding-linux-arm64-musl@0.143.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-t1AcYOwEzgceadT4v5e+vaCCb0AncCA3v5AyzfBAz/tMq11qzVccXKzNHtkWdjBsgvTKwRkaUF3QvT4kot8vcQ=="], - "@voidzero-dev/vite-plus-core/lightningcss/lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.32.0", "", { "os": "win32", "cpu": "x64" }, "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q=="], + "deslop-js/oxc-parser/@oxc-parser/binding-linux-ppc64-gnu": ["@oxc-parser/binding-linux-ppc64-gnu@0.143.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-RsnO/NoD8376LMJq8JS8TwI0ieNaFRTuNe2GVJntQg6gwZNMENZsEbknHdVwjpOmxdGLGodcwaGSbAeRr5Bgjw=="], - "@voidzero-dev/vite-plus-core/postcss/nanoid": ["nanoid@3.3.16", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q=="], + "deslop-js/oxc-parser/@oxc-parser/binding-linux-riscv64-gnu": ["@oxc-parser/binding-linux-riscv64-gnu@0.143.0", "", { "os": "linux", "cpu": "none" }, "sha512-48fSVfR9TZi5CASZFyv0VC6z6BCoeihFsX031mAD/oSH7d9PYsPgIqza7d9mjP7Z2KTEpTFyH6SIu0Ui6R1vdg=="], - "cytoscape-fcose/cose-base/layout-base": ["layout-base@2.0.1", "", {}, "sha512-dp3s92+uNI1hWIpPGH3jK2kxE2lMjdXdr+DH8ynZHpd6PUlH6x6cbuXnoMmiNumznqaNO31xu9e79F0uuZ0JFg=="], + "deslop-js/oxc-parser/@oxc-parser/binding-linux-riscv64-musl": ["@oxc-parser/binding-linux-riscv64-musl@0.143.0", "", { "os": "linux", "cpu": "none" }, "sha512-T8CpdD+SfE01DnIOD4HpVxu0ZJOfMJ/VhCvikKfaXAxkZ+9veyLM/D2hpi7Y2hFUyPmVQO3FNZHmYzV/WlVR4g=="], - "d3-sankey/d3-shape/d3-path": ["d3-path@1.0.9", "", {}, "sha512-VLaYcn81dtHVTjEHd8B+pbe9yHWpXKZUC87PzoFmsFrJqgFwDe/qxfp5MlfsfM1V5E/iVt0MmEbWQ7FVIXh/bg=="], + "deslop-js/oxc-parser/@oxc-parser/binding-linux-s390x-gnu": ["@oxc-parser/binding-linux-s390x-gnu@0.143.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-QLdeMsCcacenPEFsfxnBUDF1y6opyz5+fmOz9bfD5Y7fiGCMupUCuB3KTPQhNwshIG1P9fPqar9MHxuBDd4bwQ=="], + + "deslop-js/oxc-parser/@oxc-parser/binding-linux-x64-gnu": ["@oxc-parser/binding-linux-x64-gnu@0.143.0", "", { "os": "linux", "cpu": "x64" }, "sha512-659ujfqLy6k7cuH3sbzhd8b+ztSq+i6E2E9pG78Q0BmHjAExfGIdgc8cGgMdwAozDXeZFHkJ+LXYJdWsaGdgyw=="], + + "deslop-js/oxc-parser/@oxc-parser/binding-linux-x64-musl": ["@oxc-parser/binding-linux-x64-musl@0.143.0", "", { "os": "linux", "cpu": "x64" }, "sha512-/Mw/9j4TfZcnKphPrzOE6t4MMknXadcAAuVUlDRTF/ETWB5xOgQvOJV2Mh9We/bWxZdoxaGAdc+hy4GuYwQ2yQ=="], + + "deslop-js/oxc-parser/@oxc-parser/binding-openharmony-arm64": ["@oxc-parser/binding-openharmony-arm64@0.143.0", "", { "os": "none", "cpu": "arm64" }, "sha512-8rIKWR2BFuifbIK/1XB9wTaSdtuJ25dlE7ZQYDnEwj/2xH2vHsxnvIjHT3ZjSVuLLwGGlSslIG/fbOJ8TV8rTw=="], + + "deslop-js/oxc-parser/@oxc-parser/binding-win32-arm64-msvc": ["@oxc-parser/binding-win32-arm64-msvc@0.143.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-5U9kQYMfRRI6Zq7KDxgbIP0RMnKrfn3gLepRMgJuRkPSUALTiRCk9d/uyhb4lGDjUdzwK7mBkKqhLgzBPCmLpQ=="], + + "deslop-js/oxc-parser/@oxc-parser/binding-win32-ia32-msvc": ["@oxc-parser/binding-win32-ia32-msvc@0.143.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-25P7AaHk4R88Yv2XH4gToDVmh0cOu+bEURQU10CRrmvgabfRArSGAP5osmwUKeSUHj0VS50upbpbRWWW/m7mHA=="], + + "deslop-js/oxc-parser/@oxc-parser/binding-win32-x64-msvc": ["@oxc-parser/binding-win32-x64-msvc@0.143.0", "", { "os": "win32", "cpu": "x64" }, "sha512-ORMh3JE1s6V7ySicdRK7vgaDQnn5o+UHg9ct989PlWHbel8O9ARrmWXM6kZjrBMtNucxNayQ8g69G0VfWzhANw=="], "drizzle-kit/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.12", "", { "os": "aix", "cpu": "ppc64" }, "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA=="], @@ -3182,132 +3166,82 @@ "hast-util-raw/parse5/entities": ["entities@6.0.1", "", {}, "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g=="], - "vite-plus/oxlint/@oxlint/binding-android-arm-eabi": ["@oxlint/binding-android-arm-eabi@1.67.0", "", { "os": "android", "cpu": "arm" }, "sha512-VrSi571rDv1N8HaEDM+DEX8nmT0y9jJo8tzzW13vsOWTx59xQczCIJx68n2zWOXRT5YKZsOZXp4qkHN/10x4mw=="], - - "vite-plus/oxlint/@oxlint/binding-android-arm64": ["@oxlint/binding-android-arm64@1.67.0", "", { "os": "android", "cpu": "arm64" }, "sha512-l6+NdYxMoRohix5r5bbigW16LPicceCwGcQ6LKKuE1kUdjgFfQolJjrJsQYPFetIs78Gxj/G/f5TEGoTCwj9nQ=="], - - "vite-plus/oxlint/@oxlint/binding-darwin-arm64": ["@oxlint/binding-darwin-arm64@1.67.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-jOzXxS1AxFxhImLIRbtGIMrEwaXcgMw3gR57WB1cRk8ai+vpr6726kxXqVvlNsrXtJ/FrmOm8RxlC0m8SW24Qg=="], - - "vite-plus/oxlint/@oxlint/binding-darwin-x64": ["@oxlint/binding-darwin-x64@1.67.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-3DFAVY94OqjIZHXIPz37yGRSWwOFTAqChQ64/M69GYLawzP0KiwdhDNfqdKKYT0bTR/DNxmMnQsj3ns+8+X/Lg=="], - - "vite-plus/oxlint/@oxlint/binding-freebsd-x64": ["@oxlint/binding-freebsd-x64@1.67.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-e4dDKZuLu8TR9DEBssWSDahlPgZBwojTTHZUvnjBRJfJJbpxYCjfjKfi0Z1+CSLMiJBwI2yCDtRM1XJQaARjmg=="], - - "vite-plus/oxlint/@oxlint/binding-linux-arm-gnueabihf": ["@oxlint/binding-linux-arm-gnueabihf@1.67.0", "", { "os": "linux", "cpu": "arm" }, "sha512-BKytFdcQzbITV3xlnzDUDTEDtbUMCCiC4EaNTDZ4FyT8gdNvBC4gfiLucXp/sQl0XU3p7syTlorUWVVVBZab2g=="], - - "vite-plus/oxlint/@oxlint/binding-linux-arm-musleabihf": ["@oxlint/binding-linux-arm-musleabihf@1.67.0", "", { "os": "linux", "cpu": "arm" }, "sha512-XYAv0esBDX7BpTzRDjVX2Vdj+zndd8ll2dFQiaeQ6zTZr7A8GRDTN7fH3FP3jU+O0vCDx85oH/EtG7BzPgAXuw=="], - - "vite-plus/oxlint/@oxlint/binding-linux-arm64-gnu": ["@oxlint/binding-linux-arm64-gnu@1.67.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-zizRMjA0i6u/2B0evgda04iycu+MoNuf1pBy6Eh+1CjC5wMEG7qN5zdDKTCvFc0KSYSDM9QTG3gjZHirgtQuKg=="], - - "vite-plus/oxlint/@oxlint/binding-linux-arm64-musl": ["@oxlint/binding-linux-arm64-musl@1.67.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-zB/Tf6sUjmmvvbva9Gj3JTJ8rJ9t4I8/U0o6vSRtd0DRIsIuyegBwJAzhSUFQHdMijIRJkW0exs/yBhpw2S20w=="], - - "vite-plus/oxlint/@oxlint/binding-linux-ppc64-gnu": ["@oxlint/binding-linux-ppc64-gnu@1.67.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-kgU40Gt74CK0TCsF51KZymkIwN9U0BajKsMijB52zPqOeZU9NAHkA/NSQkZDHEaCakx42DxhXkODiAqf2b4Gug=="], - - "vite-plus/oxlint/@oxlint/binding-linux-riscv64-gnu": ["@oxlint/binding-linux-riscv64-gnu@1.67.0", "", { "os": "linux", "cpu": "none" }, "sha512-tOYhkk/iaG9aD3FvGpBFd1Lrw0x0RaVoJBxjUkfNzS50rC5NS5BteNCwgr8A2zCdADrIIoze6D7u6U5Ic++/iQ=="], - - "vite-plus/oxlint/@oxlint/binding-linux-riscv64-musl": ["@oxlint/binding-linux-riscv64-musl@1.67.0", "", { "os": "linux", "cpu": "none" }, "sha512-sEtywrPb+0b+tHYl1SDCrw903fiC4eyKoNqzP3v+f2JT3Xcv4NEYG+P8rj+eEnX7IWhqV/xj8/JmcmVj21CXaA=="], - - "vite-plus/oxlint/@oxlint/binding-linux-s390x-gnu": ["@oxlint/binding-linux-s390x-gnu@1.67.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-BvR8Moa0zCLxroOx4vZaZN9nUfwAUpSTwjZdxZyKy4bv3PrzrXrxKR/ZQ0L9wNSvlPhnMJeZfa3q5w6ZCTuN6Q=="], - - "vite-plus/oxlint/@oxlint/binding-linux-x64-gnu": ["@oxlint/binding-linux-x64-gnu@1.67.0", "", { "os": "linux", "cpu": "x64" }, "sha512-mm2cxM6fksOpq6l0uFws8BUGKAR4dNa/cZCn37Npq7PFbhD5HDJqWfnoIvTaeRKMy5XdS2tO0MA0qbHDrnXAAA=="], - - "vite-plus/oxlint/@oxlint/binding-linux-x64-musl": ["@oxlint/binding-linux-x64-musl@1.67.0", "", { "os": "linux", "cpu": "x64" }, "sha512-WmbMuLapKyDlobMkXAaAL0Y+Uczh4LETfIfQsUpbId4Ip8Ai82/jqeYTOoUCkuuhBFapgqP253+d83tLKOksJg=="], - - "vite-plus/oxlint/@oxlint/binding-openharmony-arm64": ["@oxlint/binding-openharmony-arm64@1.67.0", "", { "os": "none", "cpu": "arm64" }, "sha512-9g/PqxYJelzzTAOR5Y+RiRqdeydhEuXv2KxNeFcAKQ7UsvnWSY1OP4MsuPMbTO2Pf70tz7mFhl1j13H3fyh+8g=="], - - "vite-plus/oxlint/@oxlint/binding-win32-arm64-msvc": ["@oxlint/binding-win32-arm64-msvc@1.67.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-2VhwE6Gatb0vJGnN0TBuQMbKCOiZlSQ/zJvVWYLK4a9d4iDiJOen/yVQkGpmsJ90MuH66fzi0kEKI0jRQMDxGA=="], - - "vite-plus/oxlint/@oxlint/binding-win32-ia32-msvc": ["@oxlint/binding-win32-ia32-msvc@1.67.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-EQ3VExXfeM1InbE5+JjufhZZTWy+kHUwgt3yZR7gQ47Je/mE0WspQPan0OJznh493L5anM210YNJtH1PXjTSFg=="], - - "vite-plus/oxlint/@oxlint/binding-win32-x64-msvc": ["@oxlint/binding-win32-x64-msvc@1.67.0", "", { "os": "win32", "cpu": "x64" }, "sha512-bw24y+/1MHS4QDkons3YyHkPT9uCMoLHHgQhb+mb8NOjTYwub1CZ+K9Ngr8aO5DMrDrkqHwTzlTwFP2vS8Y/ZQ=="], - - "@mosoo/agent-driver/vite-plus/@voidzero-dev/vite-plus-core/@oxc-project/runtime": ["@oxc-project/runtime@0.146.0", "", {}, "sha512-lbXHIpZ1MmK6zuw5txlMdIZ2waLVUIU5Gnm3sEuwJOiqDfQfbtjeHscatmeBoxbv8+If9LFM6PGh/3DcDWYIYw=="], - - "@mosoo/agent-driver/vite-plus/oxfmt/@oxfmt/binding-android-arm-eabi": ["@oxfmt/binding-android-arm-eabi@0.64.0", "", { "os": "android", "cpu": "arm" }, "sha512-o6uzh/jTOQeAY5TdkAeXdqv7MBRcPxiRA08zrcBtkKj5cSu/FMu0Hl7Q6Fi1KCKyCWZ6lJVjBzdsJvsKltUsGQ=="], - - "@mosoo/agent-driver/vite-plus/oxfmt/@oxfmt/binding-android-arm64": ["@oxfmt/binding-android-arm64@0.64.0", "", { "os": "android", "cpu": "arm64" }, "sha512-jRGSUeeP7p3Gynw2YaCVtjBIA6ZxY6bEB/ES5i54OhqmRTyuVg7ZgstEtzgq6GOAJd+2QZ5pvf+bFfmW5Mp9cw=="], - - "@mosoo/agent-driver/vite-plus/oxfmt/@oxfmt/binding-darwin-arm64": ["@oxfmt/binding-darwin-arm64@0.64.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-JINwtU2lW7nOFSqi+H2qplipNUqah9Gc1jgGmB82kTD4UnZrZIVxCJ9qEmFiKfjNq27gYLFhrUb0to86aCwMjw=="], - - "@mosoo/agent-driver/vite-plus/oxfmt/@oxfmt/binding-darwin-x64": ["@oxfmt/binding-darwin-x64@0.64.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-gCmuswrgrOSajV4HCRFkVCGIruPq8bjYuPYgSE2WQB3mD6XrdyZ3JMSRZCkQ8zCxOyGWriBo6QoZ5nmMHQ1BfA=="], - - "@mosoo/agent-driver/vite-plus/oxfmt/@oxfmt/binding-freebsd-x64": ["@oxfmt/binding-freebsd-x64@0.64.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-Ab8g7a38pT0MMImjh7anRSTve6buWBIlcXIFBYa5xl4s6UxEgKSc2xOOhbGtLwvXnEi2PsEDGoJh3oUU7xkehQ=="], - - "@mosoo/agent-driver/vite-plus/oxfmt/@oxfmt/binding-linux-arm-gnueabihf": ["@oxfmt/binding-linux-arm-gnueabihf@0.64.0", "", { "os": "linux", "cpu": "arm" }, "sha512-BgvS3CoQ+Xy2deoZqEN8JVKabcCZi2RxA3yant8G9OAv9KuPJ9TCjHkqigzdHUVwErZxEP5d2bzLIEyKYyBDLg=="], + "oxlint-plugin-react-doctor/oxc-parser/@oxc-parser/binding-android-arm-eabi": ["@oxc-parser/binding-android-arm-eabi@0.143.0", "", { "os": "android", "cpu": "arm" }, "sha512-n9uozULWflPqBtdmI8lAabLqGKNgLVNN0ZH8HfgCwpKGNtzRzauB76jTiW/3YLkcA7N1zskpi9GdVnZuu1SAvg=="], - "@mosoo/agent-driver/vite-plus/oxfmt/@oxfmt/binding-linux-arm-musleabihf": ["@oxfmt/binding-linux-arm-musleabihf@0.64.0", "", { "os": "linux", "cpu": "arm" }, "sha512-QXpNxwoMj0YvnceCNZadNSden3bIcnvjn/sDp/rwZhRoZoZYGpHvtPyhGsdJz9uvT9GkaMW7SsLddurU56dt8w=="], + "oxlint-plugin-react-doctor/oxc-parser/@oxc-parser/binding-android-arm64": ["@oxc-parser/binding-android-arm64@0.143.0", "", { "os": "android", "cpu": "arm64" }, "sha512-9BbdjHETk6O3zH/DDid9IgBtF0GlpLabNKN231uraXpRDSfY+iiZxTP5bk1Z63GBownVdhdINFIeddmMz4MzpQ=="], - "@mosoo/agent-driver/vite-plus/oxfmt/@oxfmt/binding-linux-arm64-gnu": ["@oxfmt/binding-linux-arm64-gnu@0.64.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-BBgH3I1ppDsI5pZ4Pdhw0ceYxwVCfbU/bZEBCeZ6caRS9x0ZabErxubP7riGUn11PXZBhe8DYdjkDKP1FlVQ5w=="], + "oxlint-plugin-react-doctor/oxc-parser/@oxc-parser/binding-darwin-arm64": ["@oxc-parser/binding-darwin-arm64@0.143.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-gh+6ecoHUy4/sUcolBl/1qPXKBbYNxFY0Pk0ujgQvINTMSftJY7o4yb8gOkDJPeZeB8+a+u7xTe6umoP8N5HFA=="], - "@mosoo/agent-driver/vite-plus/oxfmt/@oxfmt/binding-linux-arm64-musl": ["@oxfmt/binding-linux-arm64-musl@0.64.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-v19HSjC/BGXdt26qEvKZtwAHgGmQ2Agcap2kQP+KIqoRZqivVzYth3ui2dJA1i+6/fjpjga85lIOaJJjQ/bOOw=="], + "oxlint-plugin-react-doctor/oxc-parser/@oxc-parser/binding-darwin-x64": ["@oxc-parser/binding-darwin-x64@0.143.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-qd1hl2d+lXgHv/VQ/M9qm8TrMC5T4RqDBwtOnl+1D0QMjwcz+8AaB4JSg8STgeag0GP6a6L74XEGAsrTSJWNzQ=="], - "@mosoo/agent-driver/vite-plus/oxfmt/@oxfmt/binding-linux-ppc64-gnu": ["@oxfmt/binding-linux-ppc64-gnu@0.64.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-PElLnOo4xFTBZrxPhgTIj0eHqZXwEBQoNWtb7facUV170T0B0FRET0iNbb3LUeLWTybkUW+vsdyv4ihOdyXGyw=="], + "oxlint-plugin-react-doctor/oxc-parser/@oxc-parser/binding-freebsd-x64": ["@oxc-parser/binding-freebsd-x64@0.143.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-M5XXcNa7aOqLPKTR41msfghKu2yQ4xWvCm11/gwU0JzOzHNk5sgW//rVEjJ+LO48+VDAMzXTSzurUVxIDKwozw=="], - "@mosoo/agent-driver/vite-plus/oxfmt/@oxfmt/binding-linux-riscv64-gnu": ["@oxfmt/binding-linux-riscv64-gnu@0.64.0", "", { "os": "linux", "cpu": "none" }, "sha512-Qzsg15n4F5CH+MorcRW4MkAEMiLzXmeG+DiDSbP/bBTqCmWOH3K9DHryNrve+JHlV0txS+B6Z9P5Xz+cmWeL+g=="], + "oxlint-plugin-react-doctor/oxc-parser/@oxc-parser/binding-linux-arm-gnueabihf": ["@oxc-parser/binding-linux-arm-gnueabihf@0.143.0", "", { "os": "linux", "cpu": "arm" }, "sha512-T/GXusuOkPNQhCQCSBbcU/N8j0rAypuDBl1IyFK+lyYT594XsVz80clPC/OtbSSpBGyJxj8uYEfctxVuxVYoww=="], - "@mosoo/agent-driver/vite-plus/oxfmt/@oxfmt/binding-linux-riscv64-musl": ["@oxfmt/binding-linux-riscv64-musl@0.64.0", "", { "os": "linux", "cpu": "none" }, "sha512-/GZ358wnQ/Ez4UVnCcZIi56JkY0sOdZ+B108pqXKqZz3jLS59F4KEAB1Qv3fRlObrFEk+3L2vUQ/xoPx+3vjXw=="], + "oxlint-plugin-react-doctor/oxc-parser/@oxc-parser/binding-linux-arm-musleabihf": ["@oxc-parser/binding-linux-arm-musleabihf@0.143.0", "", { "os": "linux", "cpu": "arm" }, "sha512-oKu4RcBlXSqo3OC62dp6YTnQaZIurNDpCX3BnAM3+bJxt7s8J2TJKMnC0UYer1qhlRaDCg6wkTaTw+2IlsZ12w=="], - "@mosoo/agent-driver/vite-plus/oxfmt/@oxfmt/binding-linux-s390x-gnu": ["@oxfmt/binding-linux-s390x-gnu@0.64.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-/C9We3DXegowfLXtVCYHeNiU9azwCDr5cQkEtCVlc74vyn+lLQSPApJ1CZmxAduqeq/Oi3gQ+IVptyhCaTMtkQ=="], + "oxlint-plugin-react-doctor/oxc-parser/@oxc-parser/binding-linux-arm64-gnu": ["@oxc-parser/binding-linux-arm64-gnu@0.143.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-WJBbD186AZmMGaSIhlktC+rPl8L3peCTXAh88Ih9uEvK0en2mPojGyCGYiL6mHtV1RPV3JyfJW5t6n5hh0lXhA=="], - "@mosoo/agent-driver/vite-plus/oxfmt/@oxfmt/binding-linux-x64-gnu": ["@oxfmt/binding-linux-x64-gnu@0.64.0", "", { "os": "linux", "cpu": "x64" }, "sha512-91KM2CeRWscIEHlj1NsW2WSnzGeq1Ehq+39bfDowTdkn+fcvK/x4Y1RcyqT7glyBjZio0ldkeCG6Usj3v7ASog=="], + "oxlint-plugin-react-doctor/oxc-parser/@oxc-parser/binding-linux-arm64-musl": ["@oxc-parser/binding-linux-arm64-musl@0.143.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-t1AcYOwEzgceadT4v5e+vaCCb0AncCA3v5AyzfBAz/tMq11qzVccXKzNHtkWdjBsgvTKwRkaUF3QvT4kot8vcQ=="], - "@mosoo/agent-driver/vite-plus/oxfmt/@oxfmt/binding-linux-x64-musl": ["@oxfmt/binding-linux-x64-musl@0.64.0", "", { "os": "linux", "cpu": "x64" }, "sha512-gw7uEk9I+7zoT1EYLra1eWArIzNcz8e3jkv+Noo2+o2T7wPvsNSQbfoa4DSfZlvn1i6mJ05RiZ4/omaXPDNhQg=="], + "oxlint-plugin-react-doctor/oxc-parser/@oxc-parser/binding-linux-ppc64-gnu": ["@oxc-parser/binding-linux-ppc64-gnu@0.143.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-RsnO/NoD8376LMJq8JS8TwI0ieNaFRTuNe2GVJntQg6gwZNMENZsEbknHdVwjpOmxdGLGodcwaGSbAeRr5Bgjw=="], - "@mosoo/agent-driver/vite-plus/oxfmt/@oxfmt/binding-openharmony-arm64": ["@oxfmt/binding-openharmony-arm64@0.64.0", "", { "os": "none", "cpu": "arm64" }, "sha512-HYHFf616FHSPSO07c09mjmXBfQ73wIVM3m0txOiooa5XZkGoxFd6B14PVj0LB0DXIqJ6wAO/dDR/NX/5UUaqnw=="], + "oxlint-plugin-react-doctor/oxc-parser/@oxc-parser/binding-linux-riscv64-gnu": ["@oxc-parser/binding-linux-riscv64-gnu@0.143.0", "", { "os": "linux", "cpu": "none" }, "sha512-48fSVfR9TZi5CASZFyv0VC6z6BCoeihFsX031mAD/oSH7d9PYsPgIqza7d9mjP7Z2KTEpTFyH6SIu0Ui6R1vdg=="], - "@mosoo/agent-driver/vite-plus/oxfmt/@oxfmt/binding-win32-arm64-msvc": ["@oxfmt/binding-win32-arm64-msvc@0.64.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-uQjFp081IZSWD6VAofX2iO2z01awAdHmfC+NrieWIPKrT2hZKQDyq/U18M7ifC0sm0Wz8aHY/p6+FDYIzs/CrQ=="], + "oxlint-plugin-react-doctor/oxc-parser/@oxc-parser/binding-linux-riscv64-musl": ["@oxc-parser/binding-linux-riscv64-musl@0.143.0", "", { "os": "linux", "cpu": "none" }, "sha512-T8CpdD+SfE01DnIOD4HpVxu0ZJOfMJ/VhCvikKfaXAxkZ+9veyLM/D2hpi7Y2hFUyPmVQO3FNZHmYzV/WlVR4g=="], - "@mosoo/agent-driver/vite-plus/oxfmt/@oxfmt/binding-win32-ia32-msvc": ["@oxfmt/binding-win32-ia32-msvc@0.64.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-lNM6byTAQ881jugzFu8juJTbNRgsUTlswMA6pJmwi1XDvmIqnnb49lcUAs5gz94fCJLrVN+/X3s3jOKqx23WIQ=="], + "oxlint-plugin-react-doctor/oxc-parser/@oxc-parser/binding-linux-s390x-gnu": ["@oxc-parser/binding-linux-s390x-gnu@0.143.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-QLdeMsCcacenPEFsfxnBUDF1y6opyz5+fmOz9bfD5Y7fiGCMupUCuB3KTPQhNwshIG1P9fPqar9MHxuBDd4bwQ=="], - "@mosoo/agent-driver/vite-plus/oxfmt/@oxfmt/binding-win32-x64-msvc": ["@oxfmt/binding-win32-x64-msvc@0.64.0", "", { "os": "win32", "cpu": "x64" }, "sha512-BtmbtL/QjMtF1a6C3CqoDluH2IfB6fJt62E+B9RFfUPtFk4Iz9PFS6+y/SzzOvSxc7aUk2Kphwg7Dh8lMbwu6g=="], + "oxlint-plugin-react-doctor/oxc-parser/@oxc-parser/binding-linux-x64-gnu": ["@oxc-parser/binding-linux-x64-gnu@0.143.0", "", { "os": "linux", "cpu": "x64" }, "sha512-659ujfqLy6k7cuH3sbzhd8b+ztSq+i6E2E9pG78Q0BmHjAExfGIdgc8cGgMdwAozDXeZFHkJ+LXYJdWsaGdgyw=="], - "@mosoo/agent-driver/vite-plus/oxlint/@oxlint/binding-android-arm-eabi": ["@oxlint/binding-android-arm-eabi@1.79.0", "", { "os": "android", "cpu": "arm" }, "sha512-TebFaaMklO/RXzTv7PucaCq9l3X6D1gA+C8H6K4njtjFOV+zWE9MKLpulcJZN9bzytbUbQIY0mZuz12nQ5Kv4Q=="], + "oxlint-plugin-react-doctor/oxc-parser/@oxc-parser/binding-linux-x64-musl": ["@oxc-parser/binding-linux-x64-musl@0.143.0", "", { "os": "linux", "cpu": "x64" }, "sha512-/Mw/9j4TfZcnKphPrzOE6t4MMknXadcAAuVUlDRTF/ETWB5xOgQvOJV2Mh9We/bWxZdoxaGAdc+hy4GuYwQ2yQ=="], - "@mosoo/agent-driver/vite-plus/oxlint/@oxlint/binding-android-arm64": ["@oxlint/binding-android-arm64@1.79.0", "", { "os": "android", "cpu": "arm64" }, "sha512-KqqnOtAVgNsPPF0YSodkFZA1O80jcKoCZCTu3bgsszxA+MrMP9TLzfXitKjEj1FmrPprKDMdRDMmY3weESO9sg=="], + "oxlint-plugin-react-doctor/oxc-parser/@oxc-parser/binding-openharmony-arm64": ["@oxc-parser/binding-openharmony-arm64@0.143.0", "", { "os": "none", "cpu": "arm64" }, "sha512-8rIKWR2BFuifbIK/1XB9wTaSdtuJ25dlE7ZQYDnEwj/2xH2vHsxnvIjHT3ZjSVuLLwGGlSslIG/fbOJ8TV8rTw=="], - "@mosoo/agent-driver/vite-plus/oxlint/@oxlint/binding-darwin-arm64": ["@oxlint/binding-darwin-arm64@1.79.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-BVC2nsMzqQzRDPc5RhixkZ+m1p7iH4bxRRvqkbwDXX0PlQKm1BPy8J8cRjnAFafOq2QzI+BfO3vE8w2GZ3CBag=="], + "oxlint-plugin-react-doctor/oxc-parser/@oxc-parser/binding-win32-arm64-msvc": ["@oxc-parser/binding-win32-arm64-msvc@0.143.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-5U9kQYMfRRI6Zq7KDxgbIP0RMnKrfn3gLepRMgJuRkPSUALTiRCk9d/uyhb4lGDjUdzwK7mBkKqhLgzBPCmLpQ=="], - "@mosoo/agent-driver/vite-plus/oxlint/@oxlint/binding-darwin-x64": ["@oxlint/binding-darwin-x64@1.79.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-p6Lm+snmhGuLKL1+CpCV8L6ijkE/qJzK2H2jG9+eKJT0n31RbY4FLsdhexekgP3bLpw4Kgde+9DZuDZQ4yIInA=="], + "oxlint-plugin-react-doctor/oxc-parser/@oxc-parser/binding-win32-ia32-msvc": ["@oxc-parser/binding-win32-ia32-msvc@0.143.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-25P7AaHk4R88Yv2XH4gToDVmh0cOu+bEURQU10CRrmvgabfRArSGAP5osmwUKeSUHj0VS50upbpbRWWW/m7mHA=="], - "@mosoo/agent-driver/vite-plus/oxlint/@oxlint/binding-freebsd-x64": ["@oxlint/binding-freebsd-x64@1.79.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-qDMm0dXZnoHyRqSL4N4xUq82T4sqK5cbKSjvd/dF/YbMUXc2R1wEPf+vmA5S0qUmi0nwXfNbjXBtZaIqzQLIMg=="], + "oxlint-plugin-react-doctor/oxc-parser/@oxc-parser/binding-win32-x64-msvc": ["@oxc-parser/binding-win32-x64-msvc@0.143.0", "", { "os": "win32", "cpu": "x64" }, "sha512-ORMh3JE1s6V7ySicdRK7vgaDQnn5o+UHg9ct989PlWHbel8O9ARrmWXM6kZjrBMtNucxNayQ8g69G0VfWzhANw=="], - "@mosoo/agent-driver/vite-plus/oxlint/@oxlint/binding-linux-arm-gnueabihf": ["@oxlint/binding-linux-arm-gnueabihf@1.79.0", "", { "os": "linux", "cpu": "arm" }, "sha512-2od7s0nuKPzqyUZAWk9KkCyGg7eI9dwFPZg+20lB15fKFkVZ0c9ZFxqPfiBAyDTlTkh9stPI0t+JlPCqMbItVA=="], + "oxlint-plugin-react-doctor/oxc-parser/@oxc-project/types": ["@oxc-project/types@0.143.0", "", {}, "sha512-u6JZdLBTLotrNC9Vd6vPssINdzcCzleKAH6EJKImQb7GtYvX5keN2dxkoK44stCc4tffE6QQRtZTXVSzsLUlWA=="], - "@mosoo/agent-driver/vite-plus/oxlint/@oxlint/binding-linux-arm-musleabihf": ["@oxlint/binding-linux-arm-musleabihf@1.79.0", "", { "os": "linux", "cpu": "arm" }, "sha512-ZOQUjkzDnvlhSE3+tWC3YXx94MMl+sYMlwH+u1+YGApGHOJP/YAc8ZBRFOXZ6eOBmxtXAWuS/fBcdZr8qqNO1A=="], + "vite-plus/oxlint/@oxlint/binding-android-arm-eabi": ["@oxlint/binding-android-arm-eabi@1.79.0", "", { "os": "android", "cpu": "arm" }, "sha512-TebFaaMklO/RXzTv7PucaCq9l3X6D1gA+C8H6K4njtjFOV+zWE9MKLpulcJZN9bzytbUbQIY0mZuz12nQ5Kv4Q=="], - "@mosoo/agent-driver/vite-plus/oxlint/@oxlint/binding-linux-arm64-gnu": ["@oxlint/binding-linux-arm64-gnu@1.79.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-lu158FR4nGqGeRS3BQvtG85wRgU/Fy4MD5Cxp1hzJXizGiLo6u2742wJSCDKh8cFcZntvX7fcxlq4mMmfryH1g=="], + "vite-plus/oxlint/@oxlint/binding-android-arm64": ["@oxlint/binding-android-arm64@1.79.0", "", { "os": "android", "cpu": "arm64" }, "sha512-KqqnOtAVgNsPPF0YSodkFZA1O80jcKoCZCTu3bgsszxA+MrMP9TLzfXitKjEj1FmrPprKDMdRDMmY3weESO9sg=="], - "@mosoo/agent-driver/vite-plus/oxlint/@oxlint/binding-linux-arm64-musl": ["@oxlint/binding-linux-arm64-musl@1.79.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-mbpKQeE2aflTjddaHK7MP8KP/OFbUM++lt5M635ENM8IyIdK0jm2t9pb+2v9mVVIvhF6TqA4l7F79Pll1mi+uw=="], + "vite-plus/oxlint/@oxlint/binding-darwin-arm64": ["@oxlint/binding-darwin-arm64@1.79.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-BVC2nsMzqQzRDPc5RhixkZ+m1p7iH4bxRRvqkbwDXX0PlQKm1BPy8J8cRjnAFafOq2QzI+BfO3vE8w2GZ3CBag=="], - "@mosoo/agent-driver/vite-plus/oxlint/@oxlint/binding-linux-ppc64-gnu": ["@oxlint/binding-linux-ppc64-gnu@1.79.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-WpGNua7gaxaHnpSDeog2ji8IDHn/QLPl9LPzwkR/FvVv58vT5BcXjRXnU+wbu3N75cpeha8CdC7ho/U2OIsB4g=="], + "vite-plus/oxlint/@oxlint/binding-darwin-x64": ["@oxlint/binding-darwin-x64@1.79.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-p6Lm+snmhGuLKL1+CpCV8L6ijkE/qJzK2H2jG9+eKJT0n31RbY4FLsdhexekgP3bLpw4Kgde+9DZuDZQ4yIInA=="], - "@mosoo/agent-driver/vite-plus/oxlint/@oxlint/binding-linux-riscv64-gnu": ["@oxlint/binding-linux-riscv64-gnu@1.79.0", "", { "os": "linux", "cpu": "none" }, "sha512-tK1E93A5LVzISg4ngpKJnfTs7EqtIUceGI7MQ4GyDjJiLi8wPCkEyKlj2xkyKWZ1yzkDJyLHTBJ5/iFWRdnJvg=="], + "vite-plus/oxlint/@oxlint/binding-freebsd-x64": ["@oxlint/binding-freebsd-x64@1.79.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-qDMm0dXZnoHyRqSL4N4xUq82T4sqK5cbKSjvd/dF/YbMUXc2R1wEPf+vmA5S0qUmi0nwXfNbjXBtZaIqzQLIMg=="], - "@mosoo/agent-driver/vite-plus/oxlint/@oxlint/binding-linux-riscv64-musl": ["@oxlint/binding-linux-riscv64-musl@1.79.0", "", { "os": "linux", "cpu": "none" }, "sha512-qhQvUIrngXivA2A9pQ+xPCychztn/5qUv7yS3gDwXv3w7Rag+eTeeXWmRyx+t7XsW5x6LuY/8AsTq36UgFIblg=="], + "vite-plus/oxlint/@oxlint/binding-linux-arm-gnueabihf": ["@oxlint/binding-linux-arm-gnueabihf@1.79.0", "", { "os": "linux", "cpu": "arm" }, "sha512-2od7s0nuKPzqyUZAWk9KkCyGg7eI9dwFPZg+20lB15fKFkVZ0c9ZFxqPfiBAyDTlTkh9stPI0t+JlPCqMbItVA=="], - "@mosoo/agent-driver/vite-plus/oxlint/@oxlint/binding-linux-s390x-gnu": ["@oxlint/binding-linux-s390x-gnu@1.79.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-sv6AaVgU/eE6u+6WFiQVDcPPwTxP6IJMSB9k701W2r/r6Tx465e8vPvVyRxquNH4Vy6KwRNu90mVbxXJN8+5gg=="], + "vite-plus/oxlint/@oxlint/binding-linux-arm-musleabihf": ["@oxlint/binding-linux-arm-musleabihf@1.79.0", "", { "os": "linux", "cpu": "arm" }, "sha512-ZOQUjkzDnvlhSE3+tWC3YXx94MMl+sYMlwH+u1+YGApGHOJP/YAc8ZBRFOXZ6eOBmxtXAWuS/fBcdZr8qqNO1A=="], - "@mosoo/agent-driver/vite-plus/oxlint/@oxlint/binding-linux-x64-gnu": ["@oxlint/binding-linux-x64-gnu@1.79.0", "", { "os": "linux", "cpu": "x64" }, "sha512-iFZL02deziHslb3jEX9KdqlAkYoo4fGyotchKDzdfK1f5mxlIBeiQeHhvK3iFpuEJSB4ma/qeFn9oxPiwnhUPQ=="], + "vite-plus/oxlint/@oxlint/binding-linux-arm64-gnu": ["@oxlint/binding-linux-arm64-gnu@1.79.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-lu158FR4nGqGeRS3BQvtG85wRgU/Fy4MD5Cxp1hzJXizGiLo6u2742wJSCDKh8cFcZntvX7fcxlq4mMmfryH1g=="], - "@mosoo/agent-driver/vite-plus/oxlint/@oxlint/binding-linux-x64-musl": ["@oxlint/binding-linux-x64-musl@1.79.0", "", { "os": "linux", "cpu": "x64" }, "sha512-3DtZR2raqObnh7wXZoFYFd0Fw7skBvcb3f7A+/lkEiDuh8hrE6vv9b/62Qxao1a9/OeHLw/FcXlXzgsW9wTRFg=="], + "vite-plus/oxlint/@oxlint/binding-linux-arm64-musl": ["@oxlint/binding-linux-arm64-musl@1.79.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-mbpKQeE2aflTjddaHK7MP8KP/OFbUM++lt5M635ENM8IyIdK0jm2t9pb+2v9mVVIvhF6TqA4l7F79Pll1mi+uw=="], - "@mosoo/agent-driver/vite-plus/oxlint/@oxlint/binding-openharmony-arm64": ["@oxlint/binding-openharmony-arm64@1.79.0", "", { "os": "none", "cpu": "arm64" }, "sha512-Oatt4GuA1WJkqzk2ozx4HrWROOi7opV3AKDw/U8qDIqeTqzsjn5K2x3REJMNjU3/KU/Bkq96Zi3CknaiDTaC/Q=="], + "vite-plus/oxlint/@oxlint/binding-linux-ppc64-gnu": ["@oxlint/binding-linux-ppc64-gnu@1.79.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-WpGNua7gaxaHnpSDeog2ji8IDHn/QLPl9LPzwkR/FvVv58vT5BcXjRXnU+wbu3N75cpeha8CdC7ho/U2OIsB4g=="], - "@mosoo/agent-driver/vite-plus/oxlint/@oxlint/binding-win32-arm64-msvc": ["@oxlint/binding-win32-arm64-msvc@1.79.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-NAgZr9Qp8nIA9rpo0JEvwiabTF/2UVqBNnupBG9X4kxXcQoScJUTi+qHhvabb9s/thgj5wQ4XcIaJvb+ZMgoKw=="], + "vite-plus/oxlint/@oxlint/binding-linux-riscv64-gnu": ["@oxlint/binding-linux-riscv64-gnu@1.79.0", "", { "os": "linux", "cpu": "none" }, "sha512-tK1E93A5LVzISg4ngpKJnfTs7EqtIUceGI7MQ4GyDjJiLi8wPCkEyKlj2xkyKWZ1yzkDJyLHTBJ5/iFWRdnJvg=="], - "@mosoo/agent-driver/vite-plus/oxlint/@oxlint/binding-win32-ia32-msvc": ["@oxlint/binding-win32-ia32-msvc@1.79.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-+KyXjIvcpaXmWW/j9NNY5yWjrIVxaX18VyIheQy3jwc2GSYgpCr7MGI/HxIGQ/shAL5IWEKbhsqoMpAO5Stiog=="], + "vite-plus/oxlint/@oxlint/binding-linux-riscv64-musl": ["@oxlint/binding-linux-riscv64-musl@1.79.0", "", { "os": "linux", "cpu": "none" }, "sha512-qhQvUIrngXivA2A9pQ+xPCychztn/5qUv7yS3gDwXv3w7Rag+eTeeXWmRyx+t7XsW5x6LuY/8AsTq36UgFIblg=="], - "@mosoo/agent-driver/vite-plus/oxlint/@oxlint/binding-win32-x64-msvc": ["@oxlint/binding-win32-x64-msvc@1.79.0", "", { "os": "win32", "cpu": "x64" }, "sha512-mEelcCMMBS57sIXh2veGMNy+pQwuGtcMxHxGIZWQ5Ba9pJ5jCCUFOZB9E2JhBaxGsURe+WGe0zJp4RVre52gpQ=="], + "vite-plus/oxlint/@oxlint/binding-linux-s390x-gnu": ["@oxlint/binding-linux-s390x-gnu@1.79.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-sv6AaVgU/eE6u+6WFiQVDcPPwTxP6IJMSB9k701W2r/r6Tx465e8vPvVyRxquNH4Vy6KwRNu90mVbxXJN8+5gg=="], - "@mosoo/agent-driver/vite-plus/oxlint-tsgolint/@oxlint-tsgolint/darwin-arm64": ["@oxlint-tsgolint/darwin-arm64@7.0.2001", "", { "os": "darwin", "cpu": "arm64" }, "sha512-CUJEdbSZ54+Xy9OXqOhWLTKZKV0BBiV7C2i/ygyVmXtkUNXx5YCzN8DpSSshTAKktoL7S+tnQ/ftFG/i7X896w=="], + "vite-plus/oxlint/@oxlint/binding-linux-x64-gnu": ["@oxlint/binding-linux-x64-gnu@1.79.0", "", { "os": "linux", "cpu": "x64" }, "sha512-iFZL02deziHslb3jEX9KdqlAkYoo4fGyotchKDzdfK1f5mxlIBeiQeHhvK3iFpuEJSB4ma/qeFn9oxPiwnhUPQ=="], - "@mosoo/agent-driver/vite-plus/oxlint-tsgolint/@oxlint-tsgolint/darwin-x64": ["@oxlint-tsgolint/darwin-x64@7.0.2001", "", { "os": "darwin", "cpu": "x64" }, "sha512-pXfBb5BqONCcgrXQNUZWXgiYmRSWJzd97S8i41VVOh6ut0tyo+cJ5FKFpczDHxiVNfj/3e7c9B4MtztNdpIVCw=="], + "vite-plus/oxlint/@oxlint/binding-linux-x64-musl": ["@oxlint/binding-linux-x64-musl@1.79.0", "", { "os": "linux", "cpu": "x64" }, "sha512-3DtZR2raqObnh7wXZoFYFd0Fw7skBvcb3f7A+/lkEiDuh8hrE6vv9b/62Qxao1a9/OeHLw/FcXlXzgsW9wTRFg=="], - "@mosoo/agent-driver/vite-plus/oxlint-tsgolint/@oxlint-tsgolint/linux-arm64": ["@oxlint-tsgolint/linux-arm64@7.0.2001", "", { "os": "linux", "cpu": "arm64" }, "sha512-roP7zujb/QDPzDwEKsFFpzNHHy91/Y7oX9vQXk78ekyZtcQj1QXDIMH33gjDdHBfRl4K9pZ36xhRgrP4Zr+R8A=="], + "vite-plus/oxlint/@oxlint/binding-openharmony-arm64": ["@oxlint/binding-openharmony-arm64@1.79.0", "", { "os": "none", "cpu": "arm64" }, "sha512-Oatt4GuA1WJkqzk2ozx4HrWROOi7opV3AKDw/U8qDIqeTqzsjn5K2x3REJMNjU3/KU/Bkq96Zi3CknaiDTaC/Q=="], - "@mosoo/agent-driver/vite-plus/oxlint-tsgolint/@oxlint-tsgolint/linux-x64": ["@oxlint-tsgolint/linux-x64@7.0.2001", "", { "os": "linux", "cpu": "x64" }, "sha512-UDezNqdECVmngu2TPnjaS1YoAmcTaBoI5lV9vk3VahBxoi+I5r9k3iJTT7qZoYWOXTD/7T7bNcwRgrocR6BscQ=="], + "vite-plus/oxlint/@oxlint/binding-win32-arm64-msvc": ["@oxlint/binding-win32-arm64-msvc@1.79.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-NAgZr9Qp8nIA9rpo0JEvwiabTF/2UVqBNnupBG9X4kxXcQoScJUTi+qHhvabb9s/thgj5wQ4XcIaJvb+ZMgoKw=="], - "@mosoo/agent-driver/vite-plus/oxlint-tsgolint/@oxlint-tsgolint/win32-arm64": ["@oxlint-tsgolint/win32-arm64@7.0.2001", "", { "os": "win32", "cpu": "arm64" }, "sha512-uJZhqB6pdXLuN+AD1F5082byyQti/NPmJA77GtcFlmT2HzRelqbNls3SaIqxpjdFgvSBF9g0yOKGBkGFg7kX8Q=="], + "vite-plus/oxlint/@oxlint/binding-win32-ia32-msvc": ["@oxlint/binding-win32-ia32-msvc@1.79.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-+KyXjIvcpaXmWW/j9NNY5yWjrIVxaX18VyIheQy3jwc2GSYgpCr7MGI/HxIGQ/shAL5IWEKbhsqoMpAO5Stiog=="], - "@mosoo/agent-driver/vite-plus/oxlint-tsgolint/@oxlint-tsgolint/win32-x64": ["@oxlint-tsgolint/win32-x64@7.0.2001", "", { "os": "win32", "cpu": "x64" }, "sha512-FkDRm8hx9OwzGQqyWG1tO5QrTLRApff9DzSgpz9QZau37BR8d1VYKOxMLGf6shPZntJFoTwIIJYT68VndYDCog=="], + "vite-plus/oxlint/@oxlint/binding-win32-x64-msvc": ["@oxlint/binding-win32-x64-msvc@1.79.0", "", { "os": "win32", "cpu": "x64" }, "sha512-mEelcCMMBS57sIXh2veGMNy+pQwuGtcMxHxGIZWQ5Ba9pJ5jCCUFOZB9E2JhBaxGsURe+WGe0zJp4RVre52gpQ=="], } } diff --git a/config/bun-script-types.d.ts b/config/bun-script-types.d.ts index df9839bc..0f605bf6 100644 --- a/config/bun-script-types.d.ts +++ b/config/bun-script-types.d.ts @@ -42,6 +42,7 @@ interface BunProcessOptions { readonly stderr?: "inherit" | "pipe"; readonly stdin?: "inherit" | "pipe"; readonly stdout?: "inherit" | "pipe"; + readonly timeout?: number; } export interface BunRuntime { diff --git a/docs/architecture.md b/docs/architecture.md index 4fb36feb..6a341ffb 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -23,13 +23,21 @@ The architecture is built on the Cloudflare platform and uses a Serverless shape - **State and connection management: Cloudflare Durable Objects**. Durable Objects hold upgraded WebSocket connections, high-frequency Session state, and distributed coordination points that need single-instance concurrency. - **Primary database: Cloudflare D1**. D1 stores Account records, Organization shell records, App records, core entity configuration, and metadata. - **Message queues: Cloudflare Queues**. Queues decouple the control plane from offline tasks. They provide ACK semantics, dead-letter queues, and at-least-once delivery for API commands, including deployment and scheduled maintenance. Cost usage is written from normalized runtime events, not ingested through a queue. +- **Long-running App deployment: Cloudflare Workflows**. + The API command Queue provides durable admission and starts `APP_DEPLOYMENT_WORKFLOW`, but it does not execute or wait for the long deployment. + `AppDeploymentWorkflow` owns the long-running detect, build, and publish orchestration after that handoff. + The isolated Workflow instances are `mosoo-app-deployment-dev`, `mosoo-app-deployment-stage`, and `mosoo-app-deployment-prod`. - **Object storage: Cloudflare R2**. R2 stores session-level file objects, internal configuration/package uploads, any records using the reserved library scope, and sandbox state backups. Runtime-produced files are recorded as session artifacts. Sandbox private state backups use a separate backup bucket and must not be mixed with user-visible file prefixes. - **Execution sandbox: Cloudflare Sandbox / Containers**. Heterogeneous Agents run in container-image-backed isolated environments, with Sandbox APIs and Durable Object boundaries controlling runtime lifecycle. -- **Secondary App deployment: mosoo-managed Cloudflare Pages / Workers**. App Deployment clones and builds a public GitHub repository in an isolated Sandbox, then publishes the resulting Web artifact with mosoo platform credentials. D1 stores the App-owned Deployment and DeploymentRun records, while the successful artifact receives a mosoo-owned URL. This is an independent Alpha capability, not App runtime or part of the core Agent API promise. +- **Secondary App deployment: mosoo-managed Cloudflare Workers for Platforms**. + App Deployment clones and builds a public GitHub repository in an isolated Sandbox, then publishes the result as a Workers for Platforms user Worker with Workers Static Assets or a Worker module. + D1 stores the App-owned Deployment and DeploymentRun records and is the only active-script routing authority. + The API Worker's wildcard gateway dispatches the original request through `APP_DEPLOYMENT_DISPATCHER` only when D1 names an active script. + This is an independent Alpha capability, not App runtime or part of the core Agent API promise. - **Configuration editing**. Owner-side Agent configuration is currently edited through Preview, which combines the writable configuration form with in-context test chat. There is no dedicated `AgentBuilderSystemAgent` topology in the current codebase. Future configuration assistance must remain a control-plane feature and must not enter the full Sandbox / Driver runtime path. --- @@ -57,7 +65,8 @@ graph TD File[File Service
File records & Session artifacts] Env[Environment Service
Runtime Templates & Revisions] Cost[Cost / Billing Service] - Deployment[App Deployment
Build / Publish] + Deployment[App Deployment
Admission] + DeploymentWorkflow[Cloudflare Workflow
AppDeploymentWorkflow] subgraph Agent_Plane [Agent Plane] Profile[Profile Management] @@ -74,6 +83,7 @@ graph TD File & Agent_Plane --> |Push Events / Session RPC| Session Env --> |Resolve frozen EnvironmentRevision| Runtime Agent_Plane --> |Usage / Runtime Metrics| Cost + Deployment --> |Start APP_DEPLOYMENT_WORKFLOW| DeploymentWorkflow end subgraph Driver_Layer [Driver Layer / Sandbox] @@ -91,7 +101,7 @@ graph TD FileBucket[(R2 FILE_BUCKET
Session & internal file objects)] SandboxStateBucket[(R2 SANDBOX_STATE_BUCKET
Sandbox State Objects)] - DeployedApp[Cloudflare Pages / Workers
mosoo-owned App URL] + DeployedApp[Cloudflare WfP User Worker
Static Assets / Worker] Client <==> |HTTPS: App Shell / Assets| WebWorker Client <==> |HTTPS: Same-Origin /api/*| Ingress @@ -100,8 +110,8 @@ graph TD AgentDriver --> |Outbound ORPC WebSocket
/api/driver/socket| Ingress Runtime --> |Checkpoint / Restore selected Pet paths| SandboxStateBucket File --> |Session Snapshots / File Objects| FileBucket - Deployment --> |Build public repository| CF_Sandbox - Deployment --> |Publish with mosoo credentials| DeployedApp + DeploymentWorkflow --> |Build public repository| CF_Sandbox + DeploymentWorkflow --> |Publish with mosoo credentials| DeployedApp classDef web fill:#e3f2fd,stroke:#1565c0,stroke-width:2px; classDef api fill:#f3e5f5,stroke:#6a1b9a,stroke-width:2px; @@ -139,7 +149,17 @@ Except for runtime boundaries such as Session Durable Objects and Sandbox instan - **Default App provisioning**: Onboarding / Organization provisioning creates a default App. If the Organization has exactly one App, the console routes directly into that App instead of forcing an App picker. - **Agent and resource ownership**: Agents, Threads / Sessions, Environments, Skills, MCP servers, Provider credentials, file records, Agent exposure state, App Deployments and DeploymentRuns, Agent runtime logs/state, and app-scoped cost are App-owned resources. The current UI keeps Agent logs/runtime operations on Agent detail and App Usage under App Settings; it does not expose a generic App health/log console. - **No generic Service entity**: Do not add a unified `services` table, polymorphic `service.kind`, or generic Service CRUD for concrete App resources. If a future Web/API runtime, database service, worker process, or scheduled job is needed, model it with an explicit noun and lifecycle. - - **App Deployment**: One active Deployment per App records the public GitHub source and its DeploymentRuns. Build and publish work is asynchronous. Successful runs expose a mosoo-owned Cloudflare Pages or Workers URL under the configured App deployment domain. Users do not bring their own Cloudflare account in the current flow. A deployed Worker reaches its declared Agents only through injected bound capability URLs — signed, deployment-scoped identities that carry the Public Thread and file workflow and are revoked with the Deployment or binding — never through an owner Access Token or any owner-managed secret. + - **App Deployment**: One active Deployment per App records the public GitHub source and its DeploymentRuns. + Build and publish work is asynchronous. + The API command Queue starts the bound `AppDeploymentWorkflow` and then leaves the long-running deployment to Cloudflare Workflows. + Queue delivery never owns the build or publication lifetime. + The first production Workflow release is created only by the candidate Worker while the production admission gate is closed and all Queues are paused; later releases require the existing Workflow identity before and after publication. + Successful runs publish Workers for Platforms user Workers, using Workers Static Assets for static output and a Worker module when server execution is required, then expose a mosoo-owned URL under the configured App deployment domain. + The API Worker resolves that hostname through D1 and dispatches the unchanged request through `APP_DEPLOYMENT_DISPATCHER`; an unpublished, deleted, or unknown Deployment is never routed to a user Worker. + Deletion clears the D1 active-script pointer first, so traffic is revoked without depending on a Cloudflare delete response. + The `app_deployment_script` ledger permanently disqualifies the old generation from promotion, waits at least 24 hours, and then lets garbage collection delete only that exact script after rechecking D1 authority. + Users do not bring their own Cloudflare account in the current flow. + A deployed Worker reaches its declared Agents only through injected bound capability URLs — signed, deployment-scoped identities that carry the Public Thread and file workflow and are revoked with the Deployment or binding — never through an owner Access Token or any owner-managed secret. - **Access boundary**: App access maps to the single Organization owner for this phase. No secondary principal model is part of the first cut. 3. **Agent Plane** @@ -209,7 +229,7 @@ The **Agent Driver** is a top-level independently built execution component beca must add a real Driver backend and declare capabilities and gaps in Runtime Catalog before admission. - **Boot configuration injection**: Runtime writes the complete boot payload to a private Sandbox file and passes the file path through `MOSOO_DRIVER_BOOT_PAYLOAD_FILE`. The payload carries `controlUrl`, a one-time boot token, `traceparent`, Driver identity, and the frozen execution spec. The Driver removes the file after reading it. mosoo's runtime path does not send this configuration through standard input. - - **Control flow establishment**: The Driver does not listen on a sandbox-local control port. It actively opens an authenticated WebSocket to the payload's `controlUrl`, currently `/api/driver/socket`. The API Worker hands the upgrade to the `DriverConnection` binding, whose `DriverInstance` Durable Object owns the socket and ORPC command/event lifecycle. Runtime talks to that Durable Object for readiness, commands, status, and cleanup. Protocol v1 still carries a required `driverControlPort` field for legacy diagnostics, but neither side binds that port and new integrations must not depend on it; removal needs a versioned API/Driver rollout. Authentication, routing, App access checks, and asset boundaries remain in Worker / Runtime. + - **Control flow establishment**: The Driver does not listen on a sandbox-local control port. It actively opens an authenticated WebSocket to the payload's `controlUrl`, currently `/api/driver/socket`. The API Worker hands the upgrade to the `DriverConnection` binding, whose `DriverInstance` Durable Object owns the socket and ORPC command/event lifecycle. Runtime talks to that Durable Object for readiness, commands, status, and cleanup. Protocol v3 still carries a required `driverControlPort` field for legacy diagnostics, but neither side binds that port and new integrations must not depend on it; removal needs a versioned API/Driver rollout. Authentication, routing, App access checks, and asset boundaries remain in Worker / Runtime. - **Multi-Session isolation**: `AgentSession` is the product-level conversation boundary. Cloudflare Sandbox and container processes are execution resource boundaries. If multiple long-lived Driver / Agent processes in one Sandbox need stronger process, filesystem, or environment isolation, evaluate user-space container tooling such as `bubblewrap` inside the container for narrower per-session namespaces. 2. **Agent Process** diff --git a/docs/production-deploy-verification.md b/docs/production-deploy-verification.md index 6b15c169..f2042426 100644 --- a/docs/production-deploy-verification.md +++ b/docs/production-deploy-verification.md @@ -1,19 +1,24 @@ # Production Deploy Verification -This runbook simulates `just deploy` without publishing Workers or mutating -production D1. It is the required preflight before a production deploy. +This runbook simulates `just deploy` without publishing Workers or mutating production D1. +It also defines the fail-closed Workers for Platforms gates that the real production deploy runs before any production mutation. ## Rules -- Do not put Cloudflare account IDs, API tokens, secret values, or private keys - in tracked files. -- Set `CLOUDFLARE_ACCOUNT_ID` only in the shell that runs the check. -- Remove app-local `.env*` files that Wrangler or Vite would load implicitly, - and unset every `VITE_*` process variable before a production build. -- Export the production account id in the current shell: +- Do not put Cloudflare account IDs, zone IDs, API tokens, secret values, or private keys in tracked files. +- Set `CLOUDFLARE_ACCOUNT_ID`, `CLOUDFLARE_ZONE_ID`, and `CLOUDFLARE_API_TOKEN` only in the shell that runs the check. +- Remove app-local `.env*` files that Wrangler or Vite would load implicitly, and unset every `VITE_*` process variable before a production build. +- Export the production account ID, zone ID, and API token in the current shell. +- The token needs the existing D1, Queues, Containers, and Worker Routes permissions plus Zone Read, DNS Read, SSL and Certificates Read, and Workers Scripts Write for the production dispatch namespace. +- The token must also be able to read the production Workflow metadata and API Worker bindings used by the fail-closed preflight. +- Every Wrangler deploy command, including dry-runs and real API and Web publication, must include `--experimental-provision=false`. +- Normal deployment never creates or purchases a dispatch namespace, DNS record, certificate, route, custom domain, Pages project, or paid Workers for Platforms resource. +- The sole Workflow exception is the explicit first-release path described below, which can create only the configured production Workflow while the admission gate is closed and every Queue is paused. ```bash export CLOUDFLARE_ACCOUNT_ID="" +export CLOUDFLARE_ZONE_ID="" +export CLOUDFLARE_API_TOKEN="" ``` - During simulation, do not run: @@ -39,10 +44,12 @@ Acceptance: - Current branch is the intended release branch or `main`. - The whole repository, including `apps/driver`, has no staged, unstaged, or - untracked changes. The deploy script does not check this itself — it ships - whatever is on disk — so this manual check is the only worktree gate. -- No tracked path uses Git `assume-unchanged` or `skip-worktree`, and no ignored - file exists under the Web `src` or `public` build-input directories. + untracked changes. +- No tracked path uses Git `assume-unchanged` or `skip-worktree`. +- The API deploy enforces both conditions, but this manual check confirms the + intended release before any production command is invoked. +- No ignored file exists under the Web `src` or `public` build-input + directories. ## Step 1 - Run The Full Repository Gate @@ -54,6 +61,10 @@ Acceptance: - Command exits `0`. - Formatting, lint, typecheck, tests, and generated output checks pass. +- The in-memory Drizzle source-to-latest-snapshot check passes without creating + temporary migration files. +- The complete in-memory SQLite migration chain matches that latest snapshot's + managed catalog. ## Step 2 - Confirm Production D1 Migration State @@ -129,7 +140,7 @@ the required environment-artifact queue. ```bash cd apps/api CLOUDFLARE_ACCOUNT_ID="$CLOUDFLARE_ACCOUNT_ID" \ - ../../node_modules/.bin/vp exec wrangler deploy --env prod --minify --dry-run + ../../node_modules/.bin/vp exec wrangler deploy --env prod --minify --dry-run --experimental-provision=false cd ../.. ``` @@ -158,7 +169,7 @@ Acceptance: ```bash cd apps/web CLOUDFLARE_ACCOUNT_ID="$CLOUDFLARE_ACCOUNT_ID" \ - ../../node_modules/.bin/vp exec wrangler deploy --env prod --dry-run + ../../node_modules/.bin/vp exec wrangler deploy --env prod --dry-run --experimental-provision=false cd ../.. ``` @@ -177,15 +188,443 @@ rg -n "wipeProdD1|Wiping prod D1|database delete|database create" apps/api/bin/d Acceptance: - The `rg` command returns no matches. -- `apps/api/bin/deploy-prod.ts` still performs, in order: load and validate the - latest local Drizzle snapshot, apply pending remote D1 migrations (its first - remote mutation), verify every expected table exists in prod (the - DEPLOY-D1-001 missing-table guard), ensure the required environment-artifact - queue, build the Driver, then deploy the API Worker with an immediate Container rollout so the API and Driver protocol cannot split - across a gradual image rollout. -- The guard does not compare columns, indexes, constraints, or extra live - tables. The script performs no clean-worktree check and no dry-run of its own; - Steps 0-7 of this runbook are the only preflight. +- `apps/api/bin/deploy-prod.ts` still performs, in order: load and validate the latest local Drizzle snapshot, verify the Driver protocol pin, build the Driver, and dry-run the API Worker. +- It then verifies the pre-provisioned WfP infrastructure, zero legacy inventory, and either the exact existing Workflow boundary or the exact fully absent first-release Workflow boundary without mutation. +- An existing Workflow requires the live fixed operational gateway probe before mutation, while the first-release path defers that impossible route probe until the candidate Worker exists behind the closed gate. +- Only after those checks pass does it acquire the production D1 deploy lease, prove the WfP write canary lifecycle, and continue to Queue or D1 mutation. +- If any local D1 migration is pending, or the one-shot gate remains from an + interrupted release, the script follows the closed cutover sequence below + instead of migrating beside live Workers or treating `immediate` as an atomic + rollout. +- The complete ordered local filename list must equal + `AUDITED_MIGRATION_NAMES` and end at `LAST_CUTOVER_AUDITED_MIGRATION`. + Adding, inserting, renaming, or substituting a migration filename fails before + any remote mutation until its tables, admissions, rebuilds, gate, and drain + have been reviewed and that explicit list is deliberately advanced. +- The schema guard compares every managed table's columns, primary-key order, + defaults, nullability, named indexes and partial predicates, foreign keys, + CHECK expressions, autoincrement state, and the exact migration-owned + `session_event_tool_identity_consistency` trigger with the latest migration + contract. +- Unknown application triggers fail closed. +- Unknown extra application tables fail closed. + The eight Channels and WeChat tables created by `0000_baseline.sql` are an + explicit exception because the Channels subsystem was removed by #577 + without an approved destructive data migration. + The exact allowlist is `agent_channel_binding`, `channel_event_receipt`, + `channel_final_delivery_job`, `channel_runtime_state`, + `channel_thread_session`, `wechat_channel_account`, + `wechat_channel_pairing`, and `wechat_context_token`. + They have no current runtime reader or writer, but their historical data is + retained until a separate disposal and recovery plan is approved. +- The API script refuses a dirty tracked or untracked worktree and dirty or + moved submodules before local preflight, then repeats that check after the + Driver build and Worker dry run. +- Steps 0-4 and 6-7 remain required to review the intended commit and complete + repository checks before invoking a production command. + +## Workers for Platforms And Workflow Deployment Boundary + +App Deployment uses Cloudflare Workers for Platforms in both shared environments. + +| Environment | App domain | Wildcard route | Dispatch namespace | +| ----------- | --------------------- | ------------------------- | ----------------------------- | +| Stage | `apps-stage.mosoo.ai` | `*.apps-stage.mosoo.ai/*` | `mosoo-app-deployments-stage` | +| Production | `apps.mosoo.ai` | `*.apps.mosoo.ai/*` | `mosoo-app-deployments-prod` | + +Both API Worker environments bind their exact namespace as `APP_DEPLOYMENT_DISPATCHER` and expose the same value through `MOSOO_APP_DISPATCH_NAMESPACE`. +The top-level local environment has neither a dispatch namespace binding nor a public wildcard route. + +| Environment | Workflow name | +| ----------- | ---------------------------- | +| Development | `mosoo-app-deployment-dev` | +| Stage | `mosoo-app-deployment-stage` | +| Production | `mosoo-app-deployment-prod` | + +Every environment binds its Workflow as `APP_DEPLOYMENT_WORKFLOW` with class `AppDeploymentWorkflow`. +The API command Queue starts that Workflow and does not execute or wait for the long detect, build, and publish sequence. +The gateway accepts only the single label `app-` below the environment's exact App domain. +D1 `app_deployment.active_script_name` is the sole routing authority, and the gateway dispatches the original `Request` directly to that exact user Worker. +Unknown, unpublished, or deleted App hosts return `404`. +An active D1 mapping whose dispatch binding or exact script cannot be reached returns `503` and never falls through to another API route. + +The real production deploy follows this fail-closed WfP and Workflow sequence. +The account, zone, namespace, DNS, certificate, and legacy-inventory gates are read-only and must pass before any remote mutation. +An existing Workflow and gateway must also pass before mutation. +The first-release exception is defined explicitly below. + +1. It verifies the exact Cloudflare account, active `mosoo.ai` zone ID, and exact untrusted namespace `mosoo-app-deployments-prod` through read-only APIs. +2. A normal upgrade requires exactly one remote production Workflow named `mosoo-app-deployment-prod` with class `AppDeploymentWorkflow` and script `mosoo-api-prod`. +3. A normal upgrade requires the deployed `mosoo-api-prod` Worker to expose exactly one `APP_DEPLOYMENT_WORKFLOW` binding to that Workflow. +4. It requires a proxied `A`, `AAAA`, or `CNAME` traffic record for `*.apps.mosoo.ai` and rejects an unproxied or differently named record. +5. It requires an active certificate pack containing an active certificate with the exact `*.apps.mosoo.ai` SAN and at least 30 days of remaining validity. +6. It does not accept the shallower `*.mosoo.ai` certificate or a certificate for only one current App hostname. +7. It inventories legacy App Deployment Pages projects, classic Worker scripts, per-deployment Worker routes, and per-deployment custom domains through read-only APIs and requires every legacy set to be empty. +8. If legacy inventory is nonempty, it prints the exact resources and manual cleanup commands, then exits without deleting anything. +9. Every release reads the exact pre-provisioned `mosoo-wfp-probe` script and probes it through `probe.apps.mosoo.ai` and the real production wildcard gateway before acquiring the deploy lease when the existing Workflow is exact. +10. As the first remote mutation after acquiring the durable deploy lease, it first deletes the fixed unrouted `mosoo-release-write-canary`, requires `404`, uploads that same recoverable name to the exact production namespace, reads back its exact script ID and namespace, deletes it, and requires the final exact read to return `404`. +11. Any canary upload, readback, delete, or `404` verification failure stops the deploy and prints the exact script name and a credential-free manual cleanup command. +12. Immediately before actual API Worker publication, it repeats the exact namespace and Workflow boundary check. +13. The API Worker publication uses `--experimental-provision=false`, as do every API and Web dry-run and every Web publication. +14. After API Worker publication and health verification, it repeats the fixed operational gateway probe before persisting rollout metadata or reopening Queue delivery. + +The fixed `mosoo-wfp-probe` script is a pre-provisioned operational resource, not an App, a business-ledger row, or a test fixture created during deploy. +The API gateway routes only `probe.apps.mosoo.ai/.well-known/mosoo-wfp-probe/*` directly to that script without consulting D1. +Every other App hostname still requires its exact `app_deployment.active_script_name` authority. +Its `/.well-known/mosoo-wfp-probe/http` endpoint must preserve the original method, hostname, path, query, nonce header, and request body and return the exact uncached JSON contract with `Cache-Control: no-store`. +Its `/.well-known/mosoo-wfp-probe/stream` endpoint must expose `start:` as the first chunk before later yielding `end:`. +Its `/.well-known/mosoo-wfp-probe/cancel` endpoint must expose the first chunk, accept downstream cancellation, and leave a following HTTP probe healthy. +Its `/.well-known/mosoo-wfp-probe/websocket` endpoint must echo `echo:` and close normally with code `1000` and reason `verified:`. +These checks exercise real HTTP request bodies, response streaming, cancellation propagation, and WebSocket upgrade behavior through the gateway rather than merely checking that a URL returns `200`. + +Dispatch namespaces, wildcard DNS, wildcard certificates, gateway routes, and the fixed probe script remain pre-provisioned infrastructure and are never created by this deploy. +Before the App script ledger exists, the namespace must contain exactly that one probe script. +The first Workflow release is reachable without pretending that the old Worker already exports the new class. +It is accepted only when the remote `mosoo-app-deployment-prod` Workflow is completely absent and the old `mosoo-api-prod` Worker has no Workflow binding. +That exact state forces the ordinary deploy into the same closed cutover used for migrations, which pauses and reads back every Queue, drains existing authority, and closes admission before Worker publication. +The same candidate Worker publication explicitly creates `mosoo-app-deployment-prod` and `APP_DEPLOYMENT_WORKFLOW` from the checked-in Wrangler declaration. +Before any Queue can resume, the deploy requires exact Workflow name/class/script and binding readback plus the full HTTP body, streaming, cancellation, recovery, and WebSocket gateway probe. +A partial Workflow, any old Workflow binding, any wrong identity, or any post-publication verification failure is not bootstrap and fails closed. +The failed first release retains the cutover gate and verified paused Queues for exact roll-forward. +On that exact release retry, the durable gate permits only the narrow repair state where the Workflow identity is already exact but its Worker binding is still absent; the candidate is republished and all post-publication checks repeat before Queue resume. +The same partial state without the exact durable gate is treated as foreign drift and stops before mutation. +There is no skip flag and no automatic creation or adoption of any other infrastructure. + +Deleting an App Deployment commits traffic revocation in D1 before any external cleanup. +The delete transaction clears `active_script_name` and the public URL, tombstones the Deployment, terminates its active Run and command authority, and lets the `app_deployment_script` ledger arm the exact script with `retire_after = now + 24 hours`. +The request does not synchronously delete a WfP script, so a Cloudflare response cannot restore or delay traffic revocation. +Ledger garbage collection re-reads D1 and its exact row lease before each exact-name delete. +If the script is active again or is still the candidate of an exact nonterminal Run, garbage collection postpones retirement instead of deleting it. +A successful delete or provider `404` records a permanent ledger tombstone, and transient failures retry only that exact script. +The ledger is retained so a late upload with a reused name is detected and collected; garbage collection never uses a remote list or tag-based bulk delete. + +## Durable API Deploy Mutex + +Every ordinary API deploy and protocol-v3 cutover acquires one row in `__production_deploy_lease` before queue creation, D1 migration, Queue delivery changes, API Worker publication, or production smoke writes. + +The acquisition inserts one owner UUID only when the table is empty. + +It sends that mutating acquisition batch exactly once. + +The mutex never expires and never transfers ownership automatically. + +Every remote mutation verifies that exact owner immediately before and after the mutation without extending or changing the mutex. + +The mutation result is not treated as ownership evidence. + +An independent final D1 read must still return the exact owner, the canonical lease table SQL, and zero triggers attached to that table. + +Only a completely successful API deployment releases the mutex automatically. + +A failed or unproven acquisition never issues a compensating delete because a timed-out request may still commit later. + +Any failure after acquisition deliberately leaves the owner row in place because a timed-out Cloudflare request may still be completing remotely. + +Before an exact-owner manual release, an operator must prove that the original deploy process has stopped, every remote mutation is quiescent, and the Worker, Container, Queue, D1, and cutover-gate state has one exact recoverable result. + +Elapsed time is never evidence that the mutex is safe to release. + +Release deletes only the exact current owner's row, so a stale process cannot delete another owner's mutex. + +The empty mutex table remains for reuse by later deploys. + +The Web Worker is outside this API/D1/Queue cutover boundary and continues to use workflow concurrency. + +## Audited One-Shot Production Migration Cutover + +Migration `0013_durable-mcp-effect-v3.sql` was the historical reason for introducing the cutover gate, but its filename is not a deployment sentinel. +Any migration that was pending when the audited deploy began uses the same closed admission, complete drain, bookmark, migration, rollout, smoke, and Queue-resume sequence. +The canonical audited journal and its current boundary are `AUDITED_MIGRATION_NAMES` and `LAST_CUTOVER_AUDITED_MIGRATION` in `apps/api/bin/protocol-v3-cutover.ts`. + +Before scheduling a release in which `0013_durable-mcp-effect-v3.sql` is pending, run its read-only loss inventory: + +```bash +bun apps/api/bin/deploy-prod.ts --protocol-v3-lossy-migration-inventory +``` + +The command counts oversized MCP arguments, input text, input results, control reasons, permission payload rebuilds, MCP results, provider receipts, command errors, and Session Run errors. + +It also detects duplicate command-payload keys, orphan effects, missing terminal-attempt timestamps, conflicting MCP result copies, conflicting provider receipts, and succeeded effects that disagree with their command terminal state. + +Successful MCP results keep the authoritative effect JSON text byte for byte. + +Any existing command or succeeded-attempt result copy must match that authority exactly before migration. + +It prints at most 50 stable category and row ID pairs and never prints a payload, result, receipt, or error body. + +Every category must be zero. + +There is no implicit approval flag. + +The deploy repeats this inventory after the complete drain and before any migration is applied. + +Migration `0013` repeats the same predicates at the start of its transaction, so a direct migration attempt with any candidate aborts before changing schema or history. + +Before scheduling a release in which `0014_session-event-stream-identity.sql` is pending, run the read-only production terminal inventory: + +```bash +bun apps/api/bin/deploy-prod.ts --protocol-v3-legacy-inventory +``` + +The command groups `run.completed`, `run.cancelled`, and `run.failed` history and +prints canonical sources, deterministic rewrite candidates, canonical-target +collisions, broken Run links, status/kind mismatches, and Runs with multiple +terminal events. + +The `run.cancelled` group covers both `cancelled` and `expired` Run statuses. + +It performs no build, Queue mutation, migration, Worker deploy, or Container +rollout. + +It exits nonzero for a collision, broken link, status/kind mismatch, or multiple +terminal winners. + +Provider source IDs are allowed as rewrite candidates because migration `0014` +can derive the exact canonical source from an unambiguous Run and event kind. + +The collision guards and source rewrite are one migration, and +[Cloudflare documents](https://developers.cloudflare.com/d1/wrangler-commands/#d1-migrations-apply) +that a failed D1 migration is rolled back. + +Treat this inventory as an explicit human release gate and investigate every +collision or multiple winner before starting the production cutover. + +The deploy repeats the same inventory behind the closed admission gate, so a +change after the manual check still fails closed. + +The deploy identifies its immutable release by the clean `HEAD^{tree}` Git tree +OID before any production mutation. + +That tree identity covers every tracked file, executable mode, and submodule +gitlink, and the deploy rejects tracked changes, untracked files, and dirty or +moved submodules. + +It repeats the clean-tree check after the Driver build and Worker dry run, so +local preflight cannot silently change the release being deployed. + +The Worker version receives the native `protocol-v3-` Wrangler tag. + +The gate binds the clean release tree before drain or migration. + +Only after Container convergence, health, live smoke cleanup, and final +readback does it atomically add the exact Worker version ID, Container +application version, and OCI image digest. + +Every entry with an existing gate verifies its release tree, and a recovery +with bound rollout metadata verifies all three identities before any Queue +mutation. + +A retry from another tree is rejected, while a retry with stored rollout +metadata skips publication and verifies the already-bound rollout. + +A crash after publication but before metadata persistence may publish the same +clean tree again; it cannot adopt metadata already bound to another rollout. + +The deploy script performs this sequence automatically: + +1. It compares the remote migration ledger with the local journal as an exact prefix. + Any initially pending migration within the explicitly audited journal uses this closed path. + A migration beyond `LAST_CUTOVER_AUDITED_MIGRATION` fails before production mutation rather than inheriting safety from an older gate definition. +2. It installs or verifies the canonical `__protocol_v3_cutover` table and temporary triggers. + Object definitions and counts come only from the canonical pre-migration or post-migration arrays in `apps/api/bin/protocol-v3-cutover.ts`, and the deploy consumes their generated SQL and count constants. + The migration-chain test compares the runtime-authority migration tail with the canonical post-migration catalog, so this runbook deliberately does not duplicate object counts. + A fresh database may contain the exact empty post-migration table and inert triggers; the installer atomically seeds its single release-bound row. + Any partial, extra, renamed, or differently defined protected object fails closed. +3. It pauses `api-command`, `api-command-dlq`, and + `environment-artifact-build`, then reads every Queue through the Cloudflare + API and requires `delivery_paused = true`. + A durable `queues_resuming` retry follows the recovery rules below instead of + pausing before its phase is read. +4. The gate blocks new active Session Runs, live Drivers, nonterminal business Driver commands, non-cold Sandboxes, live Sandbox Sessions, in-progress backups, and non-static Session lifecycle state. + It immediately rejects new queued or running `session_run_dispatch`, `app_deployment_run_dispatch`, and `environment_package_artifact_build` API commands while the first drain still permits reconciliation and control commands. + When the runtime-authority schema is present, the gate also blocks runtime-provisioning leases, cleanup operations, Sandbox operation authority, and Sandbox backup staging. + An Environment artifact command created before the gate may create backup staging only while it still owns the exact current generation, attempt, claim, unexpired lease, app, and input digest. + Every other Environment artifact backup staging insert fails closed. +5. Existing work may move only toward a terminal or cold state. + Already-admitted business commands and necessary reconciliation or control continuations remain available during the first drain. + The gate never rewrites an active Sandbox to `cold`; an unsafe Sandbox must + finish through the supported hibernate or checkpoint lifecycle path. +6. It temporarily resumes `api-command`, `api-command-dlq`, and `environment-artifact-build` behind the gate and reads all three delivery states back so every already-admitted lane can settle. + Repeated new business-admission requests remain rejected and cannot starve the drain. + It then waits for the complete canonical drain to reach zero. + The drain covers active Session Runs. + It covers live Driver instances. + It covers executing or claimed external tool effects. + It covers queued, delivered, or accepted Driver commands. + It covers queued or running API commands. + It covers Sandboxes that are not cold or still hold operation or claim authority. + It covers Sandbox Sessions that are not closed or errored. + It covers Sandbox backups that are not ready or pruned. + When the corresponding tables exist, it covers every Sandbox backup staging row and every Environment package artifact backup staging row. + It covers Sessions that are not static, including cleanup and runtime-provisioning authority in the runtime-authority schema. + `PROTOCOL_V3_CUTOVER_DRAIN_SQL` and `PROTOCOL_V3_POST_MIGRATION_CUTOVER_DRAIN_SQL` are the source of truth for this boundary. +7. One atomic D1 update enables the final freeze only if no queued or running API + command exists. + The frozen gate rejects new Driver commands, new API commands, terminal-command + retries, and delivery-generation rotation. + It then re-pauses and reads back all three API command lanes before repeating the complete zero-state read. +8. A 15-minute drain timeout fails before migration and reports the unsafe + Sandbox identities. +9. Before migration `0013`, it runs the read-only loss inventory above and requires every category to be zero. + The migration repeats the same guard before its first schema or data rewrite. +10. Before migration `0014`, it checks every legacy terminal Run, terminal event, + and linked assistant projection. + Duplicate terminal winners, Run-status/event-kind mismatches, missing terminal + events, broken Run/session links, ambiguous assistant rows, and failed Runs + without an authoritative error block the migration. + Every legacy terminal must have a committed timestamp and lifecycle event, no + permission request, and cursors that include its terminal event and assistant + projection. + A provider source is rewritten only when its exact + `session-run-terminal::` target is unused. + Existing terminal events keep `semantic_hash = NULL` as explicit legacy + history. +11. Before migration `0019`, it reports legacy Sandbox identities and backup rows + that the migration's authoritative transaction guard will reject. + Terminal backups require a nonempty directory, a completed Run with the exact + Agent and Pet/Cattle subject authority, and a matching Sandbox Session on the + same Sandbox and directory. + The read-only deploy preflight improves diagnostics; the guard inside the + migration is the authority against races. +12. For every pending migration set, it creates or reuses a D1 Time Travel + bookmark and persists it in the release-bound gate. + The bookmark is emergency recovery evidence, not an automatic rollback plan. +13. Immediately before applying migrations, it pauses and independently reads back all three Queues again, verifies the exact gate, re-verifies the clean Git tree, and persists the irreversible migration intent. +14. When `0014` is pending, it then records the exact rewrite manifest in a fresh 600-second authorization bound to the deploy mutex owner, bookmark, clean release tree, candidate count, and canonical candidate identities. + Migration `0014` repeats the full checks in its transaction and requires the exact canonical pre-migration gate before changing a source identity. +15. It immediately applies all pending migrations and requires the remote ledger to equal the complete local journal afterward. + D1 rolls back a failed migration while the gate and paused Queues preserve a + safe boundary until recovery proves whether any earlier migration committed. + Migration `0019` drops the old temporary triggers before table rebuilds and + recreates the exact post-migration gate in the same transaction. +16. It verifies the latest schema snapshot, publishes the exact tagged Worker and + Container image, and resolves the registry tag to its OCI SHA-256 digest. +17. It follows every Containers API page and waits until every instance is + inactive or runs the target application version and digest. + It then runs deep health and a real API-to-Sandbox Driver boot, protocol-v3 + hello, and ready smoke without starting a model turn. +18. The smoke configuration must identify an existing published `cattle` Agent + and a dedicated PAT. + The gate permits only the exact account and unique request-key pair until the synthetic Session is known, then only that exact Session, Sandbox, Run, and Driver chain. + It never grants an account-wide smoke bypass, so another request or Session from the same account remains blocked. + Cleanup may enqueue only the exact smoke Driver's `session.stop` command. + The script deletes the synthetic Thread in `finally`, closes the smoke + allowance, and rechecks the full post-migration zero-state boundary. +19. Only after rollout convergence, health, smoke cleanup, and final Worker, + application-version, tag, and digest readback does it persist immutable + rollout metadata in the gate. +20. It persists `queues_resuming` with admission still closed, resumes each Queue, + and independently requires `delivery_paused = false` for all three. + Only then does it persist `enabled = 0` as the acceptance commit and remove + the one-shot table and triggers. + A lost response is retried idempotently from the durable phase. + +The cutover protects against concurrent or stale deploy processes and failed or +lost Cloudflare responses. + +Its database trust boundary is the production D1 administrator: an actor with +arbitrary schema-DDL authority can replace tables or triggers and is outside the +deployment-adversary model. + +Keeping that operator boundary explicit lets a fresh migration chain with no +legacy rewrite candidates remain self-contained, while any candidate rewrite +still requires the exact frozen gate, lease, bookmark, release, authorization, +revoker, foreign key, and protected-trigger inventory. + +Set `MOSOO_PROTOCOL_V3_SMOKE_AGENT_ID` and +`MOSOO_PROTOCOL_V3_SMOKE_TOKEN` to an existing published `cattle` production +smoke Agent and its dedicated PAT. + +Do not use that Agent or PAT for normal traffic. + +The script derives the PAT's account ID from the authenticated GraphQL viewer and stores a unique request key before Thread creation. +The temporary allowance is the exact account and request-key pair until the Session is known and the exact resulting Session, Session-scoped Sandbox, Run, and Driver chain afterward. +There is no account-wide allowance, and another request from the same account remains blocked. + +A `pet` Agent is rejected before the deploy mutex is acquired because its +Agent-scoped Sandbox cannot satisfy that exact synthetic Session chain. + +Cloudflare activates the Worker before its Container rollout finishes, and a +successful deploy only means that rollout started. + +The instance-version poll is therefore part of the cutover gate; the +`immediate` flag alone is not acceptance evidence. + +On failure, the script rereads the exact migration ledger and compares it with +the pending set captured at cutover entry. + +If none of those migrations committed, it resumes and reads back every Queue, +then removes the gate and reports the original error. + +If any initially pending migration committed, it pauses and reads back every +Queue, preserves the admission gate or queue-resume marker and bookmark, and +exits failed. + +At that point, keep production closed and rerun the exact same v3 release. + +That roll-forward is the only default or automated recovery path after any +migration from that cutover commits. + +A roll-forward retry never reopens `api-command` merely because a particular +historical migration name is present. + +It first recovers any interrupted smoke request, closes the exact request-scoped smoke allowance, keeps the final command freeze active, and proves every counter in the complete canonical drain above is zero again. + +Only then does it reuse the persisted bookmark and continue the migration or +run a fresh acceptance smoke. + +If migration `0014` fails, its rewrite authorization remains bound to the failed deploy owner and expires independently after 600 seconds. + +The failed deploy retains the non-expiring mutex until an operator proves the remote state is quiescent and performs an exact-owner release. + +A retry must repeat the drain, full integrity preflight, bookmark check, and exact candidate authorization. + +If queue resume or verification fails, the script re-pauses queue delivery and +keeps the `queues_resuming` pre-acceptance marker with admission closed. + +A retry recognizes that phase, idempotently resumes and verifies every Queue, +then removes the marker. + +If the acceptance update or marker removal succeeded but its response was lost, +the running deploy re-verifies every Queue, repeats the idempotent acceptance +update, and repeats marker cleanup. + +The cutover commit point is the durable `enabled = 0` update after successful +readback of all three resumed Queues. + +If marker deletion or its readback fails before that accepted state is proven, +recovery first re-pauses and reads back every Queue to establish a known-safe +boundary. + +It resumes again only when a later probe proves the gate is already absent. + +After every Queue was verified open and acceptance was attempted, cleanup +failure does not re-pause delivery because the acceptance response may have been +lost after committing and deleting its durable marker. + +That path repeats Queue verification and idempotent marker cleanup on the next +run. + +If final confirmation still fails, keep the accepted v3 release and open Queues +in place and rerun the exact same release to repeat verification and cleanup. + +Never roll back only the Worker, only the Driver image, or only D1. + +The stored bookmark is not a routine rollback mechanism. + +A D1 Time Travel restore permanently discards every D1 write made after that +bookmark and does not restore Durable Object storage. + +Do not attempt a manual v2 recovery until a separate global maintenance mode +has stopped every production write, all post-bookmark writes have been +inventoried and reconciled, and a human has explicitly approved the data loss +and recovery plan. + +Only then may an operator coordinate the matching v2 D1 state, Worker, and +Driver image, clear the reviewed stale DriverConnection Durable Object state, +verify the v2 rollout and data reconciliation, and reopen admission. + +The deploy script deliberately provides no copyable Time Travel restore command. ## Step 9 - Final Worktree Check @@ -216,8 +655,10 @@ from a fork. Configure the GitHub `try` Environment before the first release: - Restrict deployment branches to `deploy/try`. -- Add `CLOUDFLARE_ACCOUNT_ID` and `CLOUDFLARE_API_TOKEN` as Environment - secrets. Keep Worker runtime secrets in Cloudflare. +- Add `CLOUDFLARE_ACCOUNT_ID`, `CLOUDFLARE_ZONE_ID`, and `CLOUDFLARE_API_TOKEN` as Environment secrets. +- Grant the deploy token only the production permissions listed in the WfP boundary above, and keep Worker runtime secrets in Cloudflare. +- Pre-provision and verify the WfP namespace, wildcard DNS, wildcard certificate, gateway route, and fixed `mosoo-wfp-probe` operational script. +- Let the first reviewed API candidate create only its configured Workflow through the closed-gate first-release path. - Protect `deploy/try` from force-push and deletion, and restrict who may push it. Advance it only to a reviewed commit from `main`. @@ -258,20 +699,33 @@ Acceptance before running `just deploy`: - `just check` exits `0` in the same shell shape used for deploy. - All simulation steps above passed on this exact commit. -`just deploy` publishes production resources. It runs the full repository check -(`just check`), then `deploy:api`, then `deploy:web`. The API deploy -(`apps/api/bin/deploy-prod.ts`) applies pending remote D1 migrations as its -very first remote action — after loading the local snapshot but before any -build or bundle validation — then runs the latest-snapshot missing-table guard, -ensures the required environment-artifact queue, builds the Driver, and deploys -the API Worker. The Web deploy then builds and publishes the Web Worker. Neither -deploy performs its own worktree check or dry-run; that is -exactly why the simulation steps above are required. +`just deploy` publishes production resources. + +It runs the full repository check (`just check`), then `deploy:api`, then +`deploy:web`. + +The API deploy (`apps/api/bin/deploy-prod.ts`) repeats the Driver build and API dry-run, verifies the pre-provisioned WfP account, zone, namespace, DNS, certificate, zero legacy inventory, and the exact existing or fully absent Workflow boundary, and only then acquires the production deploy mutex. + +Normal upgrades also run the live gateway probe before that lease. + +The first Workflow release runs that probe only after the candidate exists behind the closed gate. + +Behind that lease it runs the exact WfP write canary before any other mutation, then uses the one-shot cutover above when any migration in the explicitly audited local journal is pending or when the gate remains from an interrupted release. + +The Web deploy then builds and publishes the Web Worker. + +The API deploy enforces its clean release tree after local preflight and again +immediately before migration and Worker publication. + +The Web deploy does not independently repeat that identity check, so the manual +worktree checks above remain required for the complete two-Worker release. The preflights prevent deterministic build, bundle, config, and migration-chain -failures from surfacing after a remote mutation. Cloudflare publication across -D1, queues, the API Worker, and the Web Worker is not transactional: a provider, -permission, or network failure can still leave an earlier remote step published. +failures from surfacing after a remote mutation. + +Cloudflare publication across D1, queues, the API Worker, and the Web Worker is +not transactional, so the protocol v3 gate deliberately fails closed after its +breaking migration. If the final Web publish fails, keep the same clean release commit, diagnose the provider failure, repeat Steps 6-7 (build and dry-run) manually, then rerun `just deploy-web`. Do not rewrite or roll back an already-applied D1 migration. @@ -308,8 +762,16 @@ Stop before any real deploy when any item below is true: - `just check` fails. - Production D1 has pending migrations that were not reviewed. +- Migration `0013` is pending and its read-only loss inventory reports any candidate. - The isolated local migration apply in Step 2 fails, or `pkgs/db/drizzle/**` rewrites SQL that production has already recorded. - The API or Web dry-run fails. - Production account identity is unclear. +- The production zone, dispatch namespace, wildcard DNS, or exact wildcard certificate cannot be read and verified. +- The remote Workflow boundary is partial or mismatched rather than either fully absent/unbound for the first release or exactly `mosoo-app-deployment-prod` / `AppDeploymentWorkflow` / `mosoo-api-prod` for a normal upgrade. +- A normal-upgrade `mosoo-api-prod` Worker lacks the exact `APP_DEPLOYMENT_WORKFLOW` binding. +- The exact wildcard certificate has less than 30 days of remaining validity. +- Any legacy App Deployment Pages project, classic Worker script, per-deployment Worker route, or per-deployment custom domain remains. +- The fixed `probe.apps.mosoo.ai` / `mosoo-wfp-probe` gateway probe fails HTTP body, stream, cancellation, recovery, or WebSocket verification. +- The WfP write canary cannot prove the exact PUT, GET, DELETE, and final `404` lifecycle. - Any secret value appears in command output, tracked files, or staged diff. diff --git a/package.json b/package.json index 7d1bf096..777856eb 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,7 @@ "scripts": { "build": "vp run --filter @mosoo/agent-driver build && vp run --filter @mosoo/web build", "cf:types": "vp run --filter @mosoo/api cf:types", - "check": "vp run -w fmt:check && vp run -w docs:check && vp run -w lint && vp run -w tc && vp run -w test && vp run -w public-api:contract:check", + "check": "vp run -w fmt:check && vp run -w docs:check && vp run --filter @mosoo/db schema:check && vp run -w lint && vp run -w tc && vp run -w test && vp run -w public-api:contract:check", "db:migrate:local": "vp run --filter @mosoo/api db:migrate:local", "deploy": "vp run -w deploy:api && vp run -w deploy:web", "deploy:api": "vp run --filter @mosoo/api deploy", @@ -42,18 +42,18 @@ "test": "vp exec bun test config/commit-policy.test.ts config/public-api-compatibility.test.ts scripts/public-api-nonproduction-smoke.test.ts scripts/validate-commit-message.test.ts && vp run --filter @mosoo/api --filter @mosoo/agent-driver --filter @mosoo/web --filter @mosoo/ag-ui-session --filter @mosoo/agent-package --filter @mosoo/contracts --filter @mosoo/db --filter @mosoo/id --filter @mosoo/public-api-client --filter @mosoo/runtime-catalog --filter @mosoo/runtime-events --filter @mosoo/session-policy --filter @mosoo/skill-package test && vp run -w graphql:codegen:check" }, "devDependencies": { - "@graphql-codegen/cli": "^7.2.0", + "@graphql-codegen/cli": "^7.3.1", "@graphql-codegen/client-preset": "^6.1.3", "@graphql-codegen/schema-ast": "^6.1.0", - "@j178/prek": "^0.4.13", + "@j178/prek": "^0.5.0", "graphql": "^17.0.2", - "knip": "^6.32.2", + "knip": "^6.33.0", "react-doctor": "0.9.12", - "tsx": "^4.23.12", - "vite-plus": "^0.1.24" + "tsx": "^4.23.13", + "vite-plus": "^0.3.0" }, "engines": { - "bun": ">=1.4.0-canary.1" + "bun": ">=1.4.0" }, - "packageManager": "bun@1.4.0-canary.1" + "packageManager": "bun@1.4.0" } diff --git a/pkgs/ag-ui-session/package.json b/pkgs/ag-ui-session/package.json index edba2bc8..f657e87e 100644 --- a/pkgs/ag-ui-session/package.json +++ b/pkgs/ag-ui-session/package.json @@ -17,6 +17,6 @@ }, "devDependencies": { "typescript": "^6.0.3", - "vite-plus": "^0.1.23" + "vite-plus": "^0.3.0" } } diff --git a/pkgs/ag-ui-session/src/ag-ui-session-events.ts b/pkgs/ag-ui-session/src/ag-ui-session-events.ts index 412ed66e..1289860c 100644 --- a/pkgs/ag-ui-session/src/ag-ui-session-events.ts +++ b/pkgs/ag-ui-session/src/ag-ui-session-events.ts @@ -26,6 +26,7 @@ import type { MosooSessionRunUpdatedValue, MosooSessionStoppedValue, MosooSessionTasksReplacedValue, + MosooSessionToolUpdatedValue, MosooSessionSyncRequestValue, MosooSessionUsageUpdatedValue, } from "./custom-event-values"; @@ -147,6 +148,7 @@ export interface MosooCustomEventValueByName { [CUSTOM_EVENT_REGISTRY.sessionRunUpdated.name]: MosooSessionRunUpdatedValue; [CUSTOM_EVENT_REGISTRY.sessionStopped.name]: MosooSessionStoppedValue; [CUSTOM_EVENT_REGISTRY.sessionTasksReplaced.name]: MosooSessionTasksReplacedValue; + [CUSTOM_EVENT_REGISTRY.sessionToolUpdated.name]: MosooSessionToolUpdatedValue; [CUSTOM_EVENT_REGISTRY.sessionSyncRequest.name]: MosooSessionSyncRequestValue; [CUSTOM_EVENT_REGISTRY.sessionUsageUpdated.name]: MosooSessionUsageUpdatedValue; } diff --git a/pkgs/ag-ui-session/src/custom-event-registry.ts b/pkgs/ag-ui-session/src/custom-event-registry.ts index 85eabb88..65bbb7cd 100644 --- a/pkgs/ag-ui-session/src/custom-event-registry.ts +++ b/pkgs/ag-ui-session/src/custom-event-registry.ts @@ -66,7 +66,6 @@ export const MOSOO_CUSTOM_EVENT = { name: "mosoo.session.mode.updated", }, sessionPermissionsUpdated: { - coalescing: "replace", direction: "server", name: "mosoo.session.permissions.updated", }, @@ -93,6 +92,10 @@ export const MOSOO_CUSTOM_EVENT = { direction: "server", name: "mosoo.session.tasks.replaced", }, + sessionToolUpdated: { + direction: "server", + name: "mosoo.session.tool.updated", + }, sessionSyncRequest: { direction: "viewer", name: "mosoo.session.sync.request", diff --git a/pkgs/ag-ui-session/src/custom-event-schema.ts b/pkgs/ag-ui-session/src/custom-event-schema.ts index 7780d124..cf38a70b 100644 --- a/pkgs/ag-ui-session/src/custom-event-schema.ts +++ b/pkgs/ag-ui-session/src/custom-event-schema.ts @@ -185,6 +185,9 @@ export const MosooServerCustomEventSchema = type.or( type: '"CUSTOM"', value: { permissionRequests: SessionPermissionRequestViewSchema.array(), + "permissionRequest?": SessionPermissionRequestViewSchema, + "resolvedRequestId?": "string", + "runId?": "string", }, }), type({ @@ -225,6 +228,21 @@ export const MosooServerCustomEventSchema = type.or( type: '"CUSTOM"', value: AgentTaskSnapshot, }), + type({ + name: eventNameLiteral(MOSOO_CUSTOM_EVENT.sessionToolUpdated.name), + type: '"CUSTOM"', + value: { + inputDelta: NullableString, + inputSnapshot: NullableString, + outputDelta: NullableString, + outputSnapshot: NullableString, + parentMessageId: NullableString, + resultMessageId: "string", + runId: NullableString, + toolCallId: "string", + toolName: "string", + }, + }), type({ name: eventNameLiteral(MOSOO_CUSTOM_EVENT.sessionUsageUpdated.name), type: '"CUSTOM"', diff --git a/pkgs/ag-ui-session/src/custom-event-values.ts b/pkgs/ag-ui-session/src/custom-event-values.ts index 4135e442..3a9ddd69 100644 --- a/pkgs/ag-ui-session/src/custom-event-values.ts +++ b/pkgs/ag-ui-session/src/custom-event-values.ts @@ -21,6 +21,18 @@ export interface MosooSessionRunUpdatedValue { export type MosooSessionTasksReplacedValue = AgentTaskSnapshot; +export interface MosooSessionToolUpdatedValue { + inputDelta: string | null; + inputSnapshot: string | null; + outputDelta: string | null; + outputSnapshot: string | null; + parentMessageId: string | null; + resultMessageId: string; + runId: string | null; + toolCallId: string; + toolName: string; +} + export interface MosooSessionSyncRequestValue { reason: "manual" | "reconnect"; } @@ -46,6 +58,9 @@ export interface MosooSessionFilesUpdatedValue { export interface MosooSessionPermissionsUpdatedValue { permissionRequests: SessionPermissionRequestView[]; + permissionRequest?: SessionPermissionRequestView; + resolvedRequestId?: string; + runId?: string; } export interface MosooSessionReadinessValue { diff --git a/pkgs/ag-ui-session/src/index.ts b/pkgs/ag-ui-session/src/index.ts index d5e5c19c..8b7fc4a6 100644 --- a/pkgs/ag-ui-session/src/index.ts +++ b/pkgs/ag-ui-session/src/index.ts @@ -19,6 +19,7 @@ export type { } from "@ag-ui/core"; export * from "./live-state"; export * from "./live-state.reducer"; +export { applyToolCallUpdateToSessionLiveState } from "./live-state-message.reducer"; export * from "./ag-ui-session-codec"; export * from "./ag-ui-session-compaction"; export * from "./ag-ui-session-events"; diff --git a/pkgs/ag-ui-session/src/live-state-custom.reducer.ts b/pkgs/ag-ui-session/src/live-state-custom.reducer.ts index 6441b7ec..1d3552f5 100644 --- a/pkgs/ag-ui-session/src/live-state-custom.reducer.ts +++ b/pkgs/ag-ui-session/src/live-state-custom.reducer.ts @@ -2,7 +2,12 @@ import type { MosooCustomEvent, MosooSessionFileChange } from "./ag-ui-session-e import { MOSOO_CUSTOM_EVENT as CUSTOM_EVENT_REGISTRY } from "./custom-event-registry"; import type { SessionLiveState } from "./live-state"; import { updateSessionMetadataState } from "./live-state-custom-metadata.reducer"; -import { completePendingToolUses, normalizeMessagePlan } from "./live-state-message.reducer"; +import { + agUiEventTimestampToIso, + applyToolCallUpdateToSessionLiveState, + completePendingToolUses, + normalizeMessagePlan, +} from "./live-state-message.reducer"; import { currentIsoTimestamp, isTerminalRunStatus, @@ -83,9 +88,27 @@ function updatePermissionRequests( state: SessionLiveState, event: CustomEventByName, ): SessionLiveState { - const permissionRequests = isTerminalRunStatus(state.run.status) - ? [] - : filterPermissionRequestsForCurrentRun(state, event.value.permissionRequests); + const permissionRequests = (() => { + if (isTerminalRunStatus(state.run.status)) { + return []; + } + if (event.value.permissionRequest !== undefined) { + return filterPermissionRequestsForCurrentRun(state, [ + ...state.permissionRequests.filter( + (request) => request.requestId !== event.value.permissionRequest?.requestId, + ), + event.value.permissionRequest, + ]); + } + if (event.value.resolvedRequestId !== undefined) { + return state.permissionRequests.filter( + (request) => + request.requestId !== event.value.resolvedRequestId || + (event.value.runId !== undefined && request.runId !== event.value.runId), + ); + } + return filterPermissionRequestsForCurrentRun(state, event.value.permissionRequests); + })(); return touchSessionLiveState({ ...state, @@ -361,6 +384,14 @@ function updateRuntimeCustomState( return replaceAgentTasks(state, event); } + case CUSTOM_EVENT_REGISTRY.sessionToolUpdated.name: { + const timestamp = event["timestamp"]; + return applyToolCallUpdateToSessionLiveState(state, { + ...event.value, + ...(typeof timestamp !== "number" ? {} : { createdAt: agUiEventTimestampToIso(timestamp) }), + }); + } + case CUSTOM_EVENT_REGISTRY.sessionInfraRescheduling.name: { return updateInfraForRescheduling(state, event); } diff --git a/pkgs/ag-ui-session/src/live-state-message-core.reducer.ts b/pkgs/ag-ui-session/src/live-state-message-core.reducer.ts index 45bc3719..8b4449c7 100644 --- a/pkgs/ag-ui-session/src/live-state-message-core.reducer.ts +++ b/pkgs/ag-ui-session/src/live-state-message-core.reducer.ts @@ -65,7 +65,14 @@ export function createSessionLiveStateMessage(input: { return createLiveStateMessage(input); } -export function upsertMessage( +export function agUiEventTimestampToIso(timestamp: number): string; +export function agUiEventTimestampToIso(timestamp: undefined): undefined; +export function agUiEventTimestampToIso(timestamp: number | undefined): string | undefined; +export function agUiEventTimestampToIso(timestamp: number | undefined): string | undefined { + return timestamp === undefined ? undefined : new Date(timestamp).toISOString(); +} + +function upsertMessage( state: SessionLiveState, nextMessage: SessionLiveStateMessage, ): SessionLiveState { @@ -83,3 +90,32 @@ export function upsertMessage( messages, }); } + +export function replaceMessageText( + state: SessionLiveState, + input: { + content: string; + createdAt?: string; + id: string; + role: "assistant" | "user"; + }, +): SessionLiveState { + const current = state.messages.find((message) => message.id === input.id); + + return upsertMessage( + state, + current === undefined + ? createSessionLiveStateMessage(input) + : { + ...current, + content: input.content, + createdAt: current.segments.some( + (segment) => segment.kind === "tool_result" && segment.runId === current.id, + ) + ? current.createdAt + : (input.createdAt ?? current.createdAt), + role: input.role, + segments: current.segments.filter((segment) => segment.kind !== "text"), + }, + ); +} diff --git a/pkgs/ag-ui-session/src/live-state-message-text.reducer.ts b/pkgs/ag-ui-session/src/live-state-message-text.reducer.ts index 23db9d60..93da646e 100644 --- a/pkgs/ag-ui-session/src/live-state-message-text.reducer.ts +++ b/pkgs/ag-ui-session/src/live-state-message-text.reducer.ts @@ -6,6 +6,7 @@ export function appendTextDelta( state: SessionLiveState, messageId: string, delta: string, + createdAt?: string, ): SessionLiveState { const messages = [...state.messages]; const index = messages.findIndex((message) => message.id === messageId); @@ -14,6 +15,7 @@ export function appendTextDelta( messages.push( createLiveStateMessage({ content: delta, + ...(createdAt === undefined ? {} : { createdAt }), id: messageId, role: "assistant", segments: [{ kind: "text", text: delta }], @@ -32,6 +34,7 @@ export function appendTextDelta( messages.push( createLiveStateMessage({ content: delta, + ...(createdAt === undefined ? {} : { createdAt }), id: messageId, role: "assistant", segments: [{ kind: "text", text: delta }], diff --git a/pkgs/ag-ui-session/src/live-state-message-tool.reducer.ts b/pkgs/ag-ui-session/src/live-state-message-tool.reducer.ts index 9e8526bd..6fb08aa7 100644 --- a/pkgs/ag-ui-session/src/live-state-message-tool.reducer.ts +++ b/pkgs/ag-ui-session/src/live-state-message-tool.reducer.ts @@ -17,6 +17,13 @@ interface ToolSegmentLocations { toolUse: SegmentLocation | null; } +function matchesToolRun( + segment: ToolResultSegment | ToolUseSegment, + runId: string | null | undefined, +): boolean { + return runId === undefined || segment.runId === runId; +} + function isGenericToolName(value: string): boolean { return value.trim().toLowerCase() === "tool"; } @@ -34,6 +41,7 @@ function findToolSegmentLocations( input: { messageId?: string; primaryKind: ToolUseSegment["kind"] | ToolResultSegment["kind"]; + runId?: string | null; toolCallId: string; }, ): ToolSegmentLocations { @@ -47,7 +55,11 @@ function findToolSegmentLocations( } for (const [segmentIndex, segment] of message.segments.entries()) { - if (segment.kind === "tool_use" && segment.toolCallId === input.toolCallId) { + if ( + segment.kind === "tool_use" && + segment.toolCallId === input.toolCallId && + matchesToolRun(segment, input.runId) + ) { const location = { messageIndex: currentMessageIndex, segment, segmentIndex }; if (input.primaryKind === "tool_use") { @@ -57,7 +69,11 @@ function findToolSegmentLocations( toolUse ??= location; } - if (segment.kind === "tool_result" && segment.toolCallId === input.toolCallId) { + if ( + segment.kind === "tool_result" && + segment.toolCallId === input.toolCallId && + matchesToolRun(segment, input.runId) + ) { const location = { messageIndex: currentMessageIndex, segment, segmentIndex }; if (input.primaryKind === "tool_result") { @@ -75,7 +91,9 @@ function findToolSegmentLocations( export function appendToolUse( state: SessionLiveState, input: { + createdAt?: string; parentMessageId: string | null; + runId?: string | null; toolCallId: string; toolCallName: string; }, @@ -85,6 +103,7 @@ export function appendToolUse( const locations = findToolSegmentLocations(messages, { ...(parentMessageId ? { messageId: parentMessageId } : {}), primaryKind: "tool_use", + ...(input.runId === undefined ? {} : { runId: input.runId }), toolCallId: input.toolCallId, }); const existingUse = locations.toolUse; @@ -131,6 +150,7 @@ export function appendToolUse( argsText: "", kind: "tool_use", path: null, + ...(input.runId === undefined ? {} : { runId: input.runId }), tool: toolName, toolCallId: input.toolCallId, }); @@ -154,6 +174,7 @@ export function appendToolUse( argsText: "", kind: "tool_use", path: null, + ...(input.runId === undefined ? {} : { runId: input.runId }), tool: input.toolCallName, toolCallId: input.toolCallId, }; @@ -162,6 +183,7 @@ export function appendToolUse( messages.push( createLiveStateMessage({ content: "", + ...(input.createdAt === undefined ? {} : { createdAt: input.createdAt }), id: input.parentMessageId, role: "assistant", segments: [toolSegment], @@ -180,6 +202,7 @@ export function appendToolUse( messages.push( createLiveStateMessage({ content: "", + ...(input.createdAt === undefined ? {} : { createdAt: input.createdAt }), id: input.parentMessageId, role: "assistant", segments: [toolSegment], @@ -205,12 +228,20 @@ export function appendToolUse( export function appendToolResult( state: SessionLiveState, - input: { content: string; messageId: string; toolCallId: string }, + input: { + content: string; + createdAt?: string; + messageId: string; + runId?: string | null; + toolCallId: string; + toolName?: string; + }, ): SessionLiveState { const messages = [...state.messages]; const locations = findToolSegmentLocations(messages, { messageId: input.messageId, primaryKind: "tool_result", + ...(input.runId === undefined ? {} : { runId: input.runId }), toolCallId: input.toolCallId, }); const existingResult = locations.toolResult; @@ -226,6 +257,7 @@ export function appendToolResult( segments[existingResult.segmentIndex] = { ...existingResult.segment, output: input.content, + tool: mergeToolName(existingResult.segment.tool, input.toolName ?? "tool"), }; messages[existingResult.messageIndex] = { ...current, @@ -254,6 +286,7 @@ export function appendToolResult( { kind: "tool_result", output: input.content, + ...(input.runId === undefined ? {} : { runId: input.runId }), tool: existingUse.segment.tool, toolCallId: input.toolCallId, }, @@ -272,7 +305,8 @@ export function appendToolResult( const toolSegment: SessionViewSegment = { kind: "tool_result", output: input.content, - tool: "tool", + ...(input.runId === undefined ? {} : { runId: input.runId }), + tool: input.toolName ?? "tool", toolCallId: input.toolCallId, }; @@ -280,6 +314,7 @@ export function appendToolResult( messages.push( createLiveStateMessage({ content: "", + ...(input.createdAt === undefined ? {} : { createdAt: input.createdAt }), id: input.messageId, role: "assistant", segments: [toolSegment], @@ -296,6 +331,7 @@ export function appendToolResult( messages.push( createLiveStateMessage({ content: "", + ...(input.createdAt === undefined ? {} : { createdAt: input.createdAt }), id: input.messageId, role: "assistant", segments: [toolSegment], @@ -321,7 +357,7 @@ export function appendToolResult( export function appendToolArgs( state: SessionLiveState, - input: { delta: string; toolCallId: string }, + input: { delta: string; runId?: string | null; toolCallId: string }, ): SessionLiveState { if (input.delta.length === 0) { return state; @@ -330,6 +366,7 @@ export function appendToolArgs( const messages = [...state.messages]; const existingUse = findToolSegmentLocations(messages, { primaryKind: "tool_use", + ...(input.runId === undefined ? {} : { runId: input.runId }), toolCallId: input.toolCallId, }).toolUse; @@ -344,7 +381,9 @@ export function appendToolArgs( } const segments = message.segments.map((segment) => - segment.kind === "tool_use" && segment.toolCallId === input.toolCallId + segment.kind === "tool_use" && + segment.toolCallId === input.toolCallId && + matchesToolRun(segment, input.runId) ? { ...segment, argsText: `${segment.argsText}${input.delta}`, @@ -362,3 +401,141 @@ export function appendToolArgs( messages, }); } + +function replaceToolArgs( + state: SessionLiveState, + input: { content: string; runId?: string | null; toolCallId: string }, +): SessionLiveState { + const messages = [...state.messages]; + const existingUse = findToolSegmentLocations(messages, { + primaryKind: "tool_use", + ...(input.runId === undefined ? {} : { runId: input.runId }), + toolCallId: input.toolCallId, + }).toolUse; + if (existingUse === null) { + return state; + } + const message = messages[existingUse.messageIndex]; + if (message === undefined) { + return state; + } + + messages[existingUse.messageIndex] = { + ...message, + segments: message.segments.map((segment) => + segment.kind === "tool_use" && + segment.toolCallId === input.toolCallId && + matchesToolRun(segment, input.runId) + ? Object.assign({}, segment, { argsText: input.content }) + : segment, + ), + }; + return touchSessionLiveState({ ...state, messages }); +} + +function appendToolResultDelta( + state: SessionLiveState, + input: { + createdAt?: string; + delta: string; + messageId: string; + runId?: string | null; + toolCallId: string; + toolName?: string; + }, +): SessionLiveState { + const messages = [...state.messages]; + const existingResult = findToolSegmentLocations(messages, { + messageId: input.messageId, + primaryKind: "tool_result", + ...(input.runId === undefined ? {} : { runId: input.runId }), + toolCallId: input.toolCallId, + }).toolResult; + if (existingResult === null) { + return appendToolResult(state, { + content: input.delta, + ...(input.createdAt === undefined ? {} : { createdAt: input.createdAt }), + messageId: input.messageId, + ...(input.runId === undefined ? {} : { runId: input.runId }), + toolCallId: input.toolCallId, + ...(input.toolName === undefined ? {} : { toolName: input.toolName }), + }); + } + const message = messages[existingResult.messageIndex]; + if (message === undefined) { + return state; + } + + messages[existingResult.messageIndex] = { + ...message, + segments: message.segments.map((segment) => + segment.kind === "tool_result" && + segment.toolCallId === input.toolCallId && + matchesToolRun(segment, input.runId) + ? Object.assign({}, segment, { + output: segment.output + input.delta, + tool: mergeToolName(segment.tool, input.toolName ?? "tool"), + }) + : segment, + ), + }; + return touchSessionLiveState({ ...state, messages }); +} + +export function applyToolCallUpdateToSessionLiveState( + state: SessionLiveState, + input: { + createdAt?: string; + inputDelta: string | null; + inputSnapshot: string | null; + outputDelta: string | null; + outputSnapshot: string | null; + parentMessageId: string | null; + resultMessageId: string; + runId: string | null; + toolCallId: string; + toolName: string; + }, +): SessionLiveState { + let next = appendToolUse(state, { + ...(input.createdAt === undefined ? {} : { createdAt: input.createdAt }), + parentMessageId: input.parentMessageId, + runId: input.runId, + toolCallId: input.toolCallId, + toolCallName: input.toolName, + }); + if (input.inputDelta !== null) { + next = appendToolArgs(next, { + delta: input.inputDelta, + runId: input.runId, + toolCallId: input.toolCallId, + }); + } else if (input.inputSnapshot !== null) { + next = replaceToolArgs(next, { + content: input.inputSnapshot, + runId: input.runId, + toolCallId: input.toolCallId, + }); + } + if (input.outputDelta !== null) { + next = appendToolResultDelta(next, { + delta: input.outputDelta, + ...(input.createdAt === undefined ? {} : { createdAt: input.createdAt }), + messageId: input.resultMessageId, + runId: input.runId, + toolCallId: input.toolCallId, + toolName: input.toolName, + }); + } else if (input.outputSnapshot !== null) { + next = appendToolResult(next, { + content: input.outputSnapshot, + ...(input.createdAt === undefined ? {} : { createdAt: input.createdAt }), + messageId: input.resultMessageId, + runId: input.runId, + toolCallId: input.toolCallId, + toolName: input.toolName, + }); + } + + return next; +} diff --git a/pkgs/ag-ui-session/src/live-state-message.reducer.ts b/pkgs/ag-ui-session/src/live-state-message.reducer.ts index b043949b..ae7ca6bf 100644 --- a/pkgs/ag-ui-session/src/live-state-message.reducer.ts +++ b/pkgs/ag-ui-session/src/live-state-message.reducer.ts @@ -1,7 +1,8 @@ export { + agUiEventTimestampToIso, createSessionLiveStateMessage, normalizeMessagePlan, - upsertMessage, + replaceMessageText, } from "./live-state-message-core.reducer"; export { appendReasoningDelta, startReasoning } from "./live-state-message-reasoning.reducer"; export { appendTextDelta } from "./live-state-message-text.reducer"; @@ -9,4 +10,9 @@ export { completePendingToolUses, completeToolUse, } from "./live-state-message-tool-completion.reducer"; -export { appendToolArgs, appendToolResult, appendToolUse } from "./live-state-message-tool.reducer"; +export { + appendToolArgs, + appendToolResult, + appendToolUse, + applyToolCallUpdateToSessionLiveState, +} from "./live-state-message-tool.reducer"; diff --git a/pkgs/ag-ui-session/src/live-state.reducer.ts b/pkgs/ag-ui-session/src/live-state.reducer.ts index 3f9e5b49..198f7695 100644 --- a/pkgs/ag-ui-session/src/live-state.reducer.ts +++ b/pkgs/ag-ui-session/src/live-state.reducer.ts @@ -7,6 +7,7 @@ import type { SessionLiveState, SessionViewMessage } from "./live-state"; import { updateCustomState } from "./live-state-custom.reducer"; import { applyJsonPatch } from "./live-state-json-patch.reducer"; import { + agUiEventTimestampToIso, appendReasoningDelta, appendTextDelta, appendToolArgs, @@ -15,8 +16,8 @@ import { completePendingToolUses, completeToolUse, createSessionLiveStateMessage, + replaceMessageText, startReasoning, - upsertMessage, } from "./live-state-message.reducer"; import { currentIsoTimestamp, @@ -132,17 +133,22 @@ function applyEvent(state: SessionLiveState, event: AgUiEvent): SessionLiveState return currentState; } - return upsertMessage( - currentState, - createSessionLiveStateMessage({ - content: "", - id: event.messageId, - role: event.role, - }), - ); + return replaceMessageText(currentState, { + content: "", + ...(event.timestamp === undefined + ? {} + : { createdAt: agUiEventTimestampToIso(event.timestamp) }), + id: event.messageId, + role: event.role, + }); } case EventType.TEXT_MESSAGE_CONTENT: { - return appendTextDelta(currentState, event.messageId, event.delta); + return appendTextDelta( + currentState, + event.messageId, + event.delta, + agUiEventTimestampToIso(event.timestamp), + ); } case EventType.TEXT_MESSAGE_CHUNK: { if (!event.messageId) { @@ -151,23 +157,33 @@ function applyEvent(state: SessionLiveState, event: AgUiEvent): SessionLiveState const withMessage = event.role && isVisibleMessageRole(event.role) - ? upsertMessage( - currentState, - createSessionLiveStateMessage({ - content: "", - id: event.messageId, - role: event.role, - }), - ) + ? replaceMessageText(currentState, { + content: "", + ...(event.timestamp === undefined + ? {} + : { createdAt: agUiEventTimestampToIso(event.timestamp) }), + id: event.messageId, + role: event.role, + }) : currentState; - return event.delta ? appendTextDelta(withMessage, event.messageId, event.delta) : withMessage; + return event.delta + ? appendTextDelta( + withMessage, + event.messageId, + event.delta, + agUiEventTimestampToIso(event.timestamp), + ) + : withMessage; } case EventType.TEXT_MESSAGE_END: { return currentState; } case EventType.TOOL_CALL_START: { return appendToolUse(currentState, { + ...(event.timestamp === undefined + ? {} + : { createdAt: agUiEventTimestampToIso(event.timestamp) }), parentMessageId: event.parentMessageId ?? null, toolCallId: event.toolCallId, toolCallName: event.toolCallName, @@ -186,6 +202,9 @@ function applyEvent(state: SessionLiveState, event: AgUiEvent): SessionLiveState const withTool = event.toolCallName ? appendToolUse(currentState, { + ...(event.timestamp === undefined + ? {} + : { createdAt: agUiEventTimestampToIso(event.timestamp) }), parentMessageId: event.parentMessageId ?? null, toolCallId: event.toolCallId, toolCallName: event.toolCallName, @@ -205,6 +224,9 @@ function applyEvent(state: SessionLiveState, event: AgUiEvent): SessionLiveState case EventType.TOOL_CALL_RESULT: { return appendToolResult(currentState, { content: event.content, + ...(event.timestamp === undefined + ? {} + : { createdAt: agUiEventTimestampToIso(event.timestamp) }), messageId: event.messageId, toolCallId: event.toolCallId, }); diff --git a/pkgs/ag-ui-session/src/live-state.ts b/pkgs/ag-ui-session/src/live-state.ts index 76217e59..b6035bd0 100644 --- a/pkgs/ag-ui-session/src/live-state.ts +++ b/pkgs/ag-ui-session/src/live-state.ts @@ -19,12 +19,14 @@ export type SessionViewSegment = argsText: string; kind: "tool_use"; path: string | null; + runId?: string | null; tool: string; toolCallId: string; } | { kind: "tool_result"; output: string; + runId?: string | null; tool: string; toolCallId: string; }; diff --git a/pkgs/ag-ui-session/src/session-live-state-schema.ts b/pkgs/ag-ui-session/src/session-live-state-schema.ts index a288845f..675a2377 100644 --- a/pkgs/ag-ui-session/src/session-live-state-schema.ts +++ b/pkgs/ag-ui-session/src/session-live-state-schema.ts @@ -26,12 +26,14 @@ export const SessionViewSegmentSchema = type.or( argsText: "string", kind: '"tool_use"', path: NullableString, + "runId?": OptionalNullableString, tool: "string", toolCallId: "string", }), type({ kind: '"tool_result"', output: "string", + "runId?": OptionalNullableString, tool: "string", toolCallId: "string", }), diff --git a/pkgs/ag-ui-session/tests/live-state.reducer.test.ts b/pkgs/ag-ui-session/tests/live-state.reducer.test.ts index 6619c32e..99c0b7ef 100644 --- a/pkgs/ag-ui-session/tests/live-state.reducer.test.ts +++ b/pkgs/ag-ui-session/tests/live-state.reducer.test.ts @@ -41,6 +41,18 @@ function runningRunUpdatedEvent(runId: string, driverInstanceId: string): AgUiEv }; } +function tasksReplacedEvent( + runId: string, + driverInstanceId: string, + tasks: NonNullable["tasks"], +): AgUiEvent { + return { + name: MOSOO_CUSTOM_EVENT.sessionTasksReplaced.name, + type: "CUSTOM", + value: { driverInstanceId, runId, tasks }, + }; +} + describe("session live-state transcript reducer", () => { test("replaces live state when a state snapshot arrives", () => { const userMessage = createSessionLiveStateMessage({ @@ -192,6 +204,32 @@ describe("session live-state transcript reducer", () => { ]); }); + test("replaces authoritative text without discarding tool segments", () => { + const nextState = applyAgUiEventsToSessionLiveState(baseState(), [ + { messageId: "assistant-1", role: "assistant", type: "TEXT_MESSAGE_START" }, + { delta: "obsolete", messageId: "assistant-1", type: "TEXT_MESSAGE_CONTENT" }, + { + parentMessageId: "assistant-1", + toolCallId: "tool-1", + toolCallName: "Shell", + type: "TOOL_CALL_START", + }, + { + delta: "replacement", + messageId: "assistant-1", + role: "assistant", + type: "TEXT_MESSAGE_CHUNK", + }, + { delta: " final", messageId: "assistant-1", type: "TEXT_MESSAGE_CONTENT" }, + ]); + + expect(nextState.messages[0]?.content).toBe("replacement final"); + expect(nextState.messages[0]?.segments).toEqual([ + { argsText: "", kind: "tool_use", path: null, tool: "Shell", toolCallId: "tool-1" }, + { kind: "text", text: "replacement final" }, + ]); + }); + test("normalizes tool result before tool start into one ordered tool call", () => { const nextState = applyAgUiEventsToSessionLiveState(baseState(), [ { @@ -243,6 +281,126 @@ describe("session live-state transcript reducer", () => { ]); }); + test("applies tool input and output snapshots as replacements and deltas as appends", () => { + const toolUpdate = ( + value: Partial< + Extract["value"] + >, + ): AgUiEvent => ({ + name: MOSOO_CUSTOM_EVENT.sessionToolUpdated.name, + type: "CUSTOM", + value: { + inputDelta: null, + inputSnapshot: null, + outputDelta: null, + outputSnapshot: null, + parentMessageId: null, + resultMessageId: "assistant-1", + runId: "run-1", + toolCallId: "tool-1", + toolName: "Shell", + ...value, + }, + }); + const nextState = applyAgUiEventsToSessionLiveState(baseState(), [ + toolUpdate({ + inputDelta: '{"cmd":', + outputDelta: "partial", + parentMessageId: "assistant-1", + }), + toolUpdate({ inputSnapshot: '{"cmd":"ls"', outputSnapshot: "snapshot" }), + toolUpdate({ inputDelta: ',"tail":true}', outputDelta: " tail" }), + ]); + + expect(nextState.messages[0]?.segments).toEqual([ + { + argsText: '{"cmd":"ls","tail":true}', + kind: "tool_use", + path: null, + runId: "run-1", + tool: "Shell", + toolCallId: "tool-1", + }, + { + kind: "tool_result", + output: "snapshot tail", + runId: "run-1", + tool: "Shell", + toolCallId: "tool-1", + }, + ]); + }); + + test("scopes exact tool updates by run when tool call ids are reused", () => { + const toolUpdate = ( + runId: string, + messageId: string, + inputSnapshot: string, + outputSnapshot: string, + ): AgUiEvent => ({ + name: MOSOO_CUSTOM_EVENT.sessionToolUpdated.name, + type: "CUSTOM", + value: { + inputDelta: null, + inputSnapshot, + outputDelta: null, + outputSnapshot, + parentMessageId: messageId, + resultMessageId: messageId, + runId, + toolCallId: "shared-tool-call", + toolName: "Shell", + }, + }); + const nextState = applyAgUiEventsToSessionLiveState(baseState(), [ + toolUpdate("run-old", "assistant-old", '{"cmd":"old"}', "old output"), + toolUpdate("run-new", "assistant-new", '{"cmd":"new"}', "new output"), + ]); + + expect(nextState.messages.map(({ id, segments }) => ({ id, segments }))).toEqual([ + { + id: "assistant-old", + segments: [ + { + argsText: '{"cmd":"old"}', + kind: "tool_use", + path: null, + runId: "run-old", + tool: "Shell", + toolCallId: "shared-tool-call", + }, + { + kind: "tool_result", + output: "old output", + runId: "run-old", + tool: "Shell", + toolCallId: "shared-tool-call", + }, + ], + }, + { + id: "assistant-new", + segments: [ + { + argsText: '{"cmd":"new"}', + kind: "tool_use", + path: null, + runId: "run-new", + tool: "Shell", + toolCallId: "shared-tool-call", + }, + { + kind: "tool_result", + output: "new output", + runId: "run-new", + tool: "Shell", + toolCallId: "shared-tool-call", + }, + ], + }, + ]); + }); + test("deduplicates repeated tool starts for the same tool call across transient messages", () => { const nextState = applyAgUiEventsToSessionLiveState(baseState(), [ { @@ -616,33 +774,9 @@ describe("session live-state transcript reducer", () => { test("atomically replaces and explicitly empties the current run task snapshot", () => { const nextState = applyAgUiEventsToSessionLiveState(baseState(), [ runningRunUpdatedEvent("run-1", "driver-1"), - { - name: MOSOO_CUSTOM_EVENT.sessionTasksReplaced.name, - type: "CUSTOM", - value: { - driverInstanceId: "driver-1", - runId: "run-1", - tasks: [{ taskId: "task-1", title: "First" }], - }, - }, - { - name: MOSOO_CUSTOM_EVENT.sessionTasksReplaced.name, - type: "CUSTOM", - value: { - driverInstanceId: "driver-1", - runId: "run-1", - tasks: [{ taskId: "task-2", taskType: "review" }], - }, - }, - { - name: MOSOO_CUSTOM_EVENT.sessionTasksReplaced.name, - type: "CUSTOM", - value: { - driverInstanceId: "driver-1", - runId: "run-1", - tasks: [], - }, - }, + tasksReplacedEvent("run-1", "driver-1", [{ taskId: "task-1", title: "First" }]), + tasksReplacedEvent("run-1", "driver-1", [{ taskId: "task-2", taskType: "review" }]), + tasksReplacedEvent("run-1", "driver-1", []), ]); expect(nextState.taskSnapshot).toEqual({ @@ -655,33 +789,9 @@ describe("session live-state transcript reducer", () => { test("rejects stale run and driver task snapshots", () => { const nextState = applyAgUiEventsToSessionLiveState(baseState(), [ runningRunUpdatedEvent("run-2", "driver-2"), - { - name: MOSOO_CUSTOM_EVENT.sessionTasksReplaced.name, - type: "CUSTOM", - value: { - driverInstanceId: "driver-2", - runId: "run-2", - tasks: [{ taskId: "current" }], - }, - }, - { - name: MOSOO_CUSTOM_EVENT.sessionTasksReplaced.name, - type: "CUSTOM", - value: { - driverInstanceId: "driver-1", - runId: "run-1", - tasks: [{ taskId: "old-run" }], - }, - }, - { - name: MOSOO_CUSTOM_EVENT.sessionTasksReplaced.name, - type: "CUSTOM", - value: { - driverInstanceId: "driver-1", - runId: "run-2", - tasks: [{ taskId: "old-driver" }], - }, - }, + tasksReplacedEvent("run-2", "driver-2", [{ taskId: "current" }]), + tasksReplacedEvent("run-1", "driver-1", [{ taskId: "old-run" }]), + tasksReplacedEvent("run-2", "driver-1", [{ taskId: "old-driver" }]), ]); expect(nextState.taskSnapshot?.tasks).toEqual([{ taskId: "current" }]); @@ -690,15 +800,7 @@ describe("session live-state transcript reducer", () => { test("does not restore a delayed task snapshot after agent replacement starts", () => { const nextState = applyAgUiEventsToSessionLiveState(baseState(), [ runningRunUpdatedEvent("run-1", "driver-1"), - { - name: MOSOO_CUSTOM_EVENT.sessionTasksReplaced.name, - type: "CUSTOM", - value: { - driverInstanceId: "driver-1", - runId: "run-1", - tasks: [{ taskId: "before-reschedule" }], - }, - }, + tasksReplacedEvent("run-1", "driver-1", [{ taskId: "before-reschedule" }]), { name: MOSOO_CUSTOM_EVENT.agentUpdating.name, type: "CUSTOM", @@ -708,15 +810,7 @@ describe("session live-state transcript reducer", () => { startedAt: "2026-05-26T00:00:01.000Z", }, }, - { - name: MOSOO_CUSTOM_EVENT.sessionTasksReplaced.name, - type: "CUSTOM", - value: { - driverInstanceId: "driver-1", - runId: "run-1", - tasks: [{ taskId: "delayed-old-driver" }], - }, - }, + tasksReplacedEvent("run-1", "driver-1", [{ taskId: "delayed-old-driver" }]), ]); expect(nextState.lifecycle).toBe("RESCHEDULING"); @@ -756,34 +850,16 @@ describe("session live-state transcript reducer", () => { ] as const)("fences replacement driver snapshots when %s", (_label, arrivalOrder) => { const stateBeforeReplacement = applyAgUiEventsToSessionLiveState(baseState(), [ runningRunUpdatedEvent("run-1", "driver-1"), - { - name: MOSOO_CUSTOM_EVENT.sessionTasksReplaced.name, - type: "CUSTOM", - value: { - driverInstanceId: "driver-1", - runId: "run-1", - tasks: [{ taskId: "before-reschedule" }], - }, - }, + tasksReplacedEvent("run-1", "driver-1", [{ taskId: "before-reschedule" }]), ]); const replacementEvents: AgUiEvent[] = [ runningRunUpdatedEvent("run-1", "driver-2"), - ...arrivalOrder.map( - (driverInstanceId) => - ({ - name: MOSOO_CUSTOM_EVENT.sessionTasksReplaced.name, - type: "CUSTOM", - value: { - driverInstanceId, - runId: "run-1", - tasks: [ - { - taskId: - driverInstanceId === "driver-2" ? "replacement-driver" : "delayed-old-driver", - }, - ], - }, - }) satisfies AgUiEvent, + ...arrivalOrder.map((driverInstanceId) => + tasksReplacedEvent("run-1", driverInstanceId, [ + { + taskId: driverInstanceId === "driver-2" ? "replacement-driver" : "delayed-old-driver", + }, + ]), ), ]; const nextState = applyAgUiEventsToSessionLiveState(stateBeforeReplacement, replacementEvents); @@ -798,15 +874,7 @@ describe("session live-state transcript reducer", () => { test("keeps the expected driver across a same-run API lifecycle update", () => { const nextState = applyAgUiEventsToSessionLiveState(baseState(), [ runningRunUpdatedEvent("run-1", "driver-1"), - { - name: MOSOO_CUSTOM_EVENT.sessionTasksReplaced.name, - type: "CUSTOM", - value: { - driverInstanceId: "driver-1", - runId: "run-1", - tasks: [{ taskId: "task-1" }], - }, - }, + tasksReplacedEvent("run-1", "driver-1", [{ taskId: "task-1" }]), { name: MOSOO_CUSTOM_EVENT.sessionRunUpdated.name, type: "CUSTOM", @@ -825,15 +893,7 @@ describe("session live-state transcript reducer", () => { test("accepts the same driver again after a websocket reconnect", () => { const nextState = applyAgUiEventsToSessionLiveState(baseState(), [ runningRunUpdatedEvent("run-1", "driver-1"), - { - name: MOSOO_CUSTOM_EVENT.sessionTasksReplaced.name, - type: "CUSTOM", - value: { - driverInstanceId: "driver-1", - runId: "run-1", - tasks: [{ taskId: "before-reconnect" }], - }, - }, + tasksReplacedEvent("run-1", "driver-1", [{ taskId: "before-reconnect" }]), { name: MOSOO_CUSTOM_EVENT.sessionInfraRescheduling.name, type: "CUSTOM", @@ -848,15 +908,7 @@ describe("session live-state transcript reducer", () => { type: "CUSTOM", value: { resumedAt: "2026-05-26T00:00:02.000Z" }, }, - { - name: MOSOO_CUSTOM_EVENT.sessionTasksReplaced.name, - type: "CUSTOM", - value: { - driverInstanceId: "driver-1", - runId: "run-1", - tasks: [{ taskId: "after-reconnect" }], - }, - }, + tasksReplacedEvent("run-1", "driver-1", [{ taskId: "after-reconnect" }]), ]); expect(nextState.taskSnapshot?.tasks).toEqual([{ taskId: "after-reconnect" }]); @@ -911,15 +963,7 @@ describe("session live-state transcript reducer", () => { ])("clears task snapshots on %s", (label, boundaryEvent) => { const stateWithTasks = applyAgUiEventsToSessionLiveState(baseState(), [ runningRunUpdatedEvent("run-1", "driver-1"), - { - name: MOSOO_CUSTOM_EVENT.sessionTasksReplaced.name, - type: "CUSTOM", - value: { - driverInstanceId: "driver-1", - runId: "run-1", - tasks: [{ taskId: "task-1" }], - }, - }, + tasksReplacedEvent("run-1", "driver-1", [{ taskId: "task-1" }]), ]); const nextState = applyAgUiEventsToSessionLiveState(stateWithTasks, [boundaryEvent]); @@ -965,6 +1009,59 @@ describe("session live-state transcript reducer", () => { expect(nextState.permissionRequests).toEqual([]); }); + test("permission events upsert and resolve one request without replacing its siblings", () => { + const runningState: SessionLiveState = { + ...baseState(), + run: { ...baseState().run, id: "run-1", status: "running" }, + }; + const request = (requestId: string) => ({ + driverInstanceId: "driver-1", + rawInput: requestId, + requestId, + runId: "run-1", + title: requestId, + toolCallId: requestId, + toolKind: "bash", + }); + const waitingState = applyAgUiEventsToSessionLiveState(runningState, [ + { + name: MOSOO_CUSTOM_EVENT.sessionPermissionsUpdated.name, + type: "CUSTOM", + value: { permissionRequest: request("permission-1"), permissionRequests: [] }, + }, + { + name: MOSOO_CUSTOM_EVENT.sessionPermissionsUpdated.name, + type: "CUSTOM", + value: { permissionRequest: request("permission-2"), permissionRequests: [] }, + }, + { + name: MOSOO_CUSTOM_EVENT.sessionPermissionsUpdated.name, + type: "CUSTOM", + value: { + permissionRequests: [], + resolvedRequestId: "permission-1", + runId: "run-1", + }, + }, + ]); + + expect(waitingState.run.status).toBe("waiting_input"); + expect(waitingState.permissionRequests).toEqual([request("permission-2")]); + const resolvedState = applyAgUiEventsToSessionLiveState(waitingState, [ + { + name: MOSOO_CUSTOM_EVENT.sessionPermissionsUpdated.name, + type: "CUSTOM", + value: { + permissionRequests: [], + resolvedRequestId: "permission-2", + runId: "run-1", + }, + }, + ]); + expect(resolvedState.run.status).toBe("running"); + expect(resolvedState.permissionRequests).toEqual([]); + }); + test("stopped custom event terminates the session and clears pending approvals", () => { const stateWithPermission: SessionLiveState = { ...baseState(), diff --git a/pkgs/agent-package/package.json b/pkgs/agent-package/package.json index a8fadf91..d9d0f97d 100644 --- a/pkgs/agent-package/package.json +++ b/pkgs/agent-package/package.json @@ -16,6 +16,6 @@ }, "devDependencies": { "typescript": "^6.0.3", - "vite-plus": "^0.1.23" + "vite-plus": "^0.3.0" } } diff --git a/pkgs/contracts/package.json b/pkgs/contracts/package.json index 1e21a603..7d993450 100644 --- a/pkgs/contracts/package.json +++ b/pkgs/contracts/package.json @@ -44,6 +44,6 @@ }, "devDependencies": { "typescript": "^6.0.3", - "vite-plus": "^0.1.23" + "vite-plus": "^0.3.0" } } diff --git a/pkgs/contracts/src/app/app.contract.ts b/pkgs/contracts/src/app/app.contract.ts index 91303322..43ed2c5d 100644 --- a/pkgs/contracts/src/app/app.contract.ts +++ b/pkgs/contracts/src/app/app.contract.ts @@ -33,7 +33,7 @@ export type AppDeploymentRunStatus = | "submitting" | "success"; -export type AppDeploymentTargetKind = "cloudflare_pages" | "cloudflare_worker"; +export type AppDeploymentTargetKind = "cloudflare_static_assets" | "cloudflare_worker"; export interface AppDeploymentRun { appId: AppId; diff --git a/pkgs/contracts/src/runtime/driver-instance.contract.ts b/pkgs/contracts/src/runtime/driver-instance.contract.ts index 623cd48c..49ff909a 100644 --- a/pkgs/contracts/src/runtime/driver-instance.contract.ts +++ b/pkgs/contracts/src/runtime/driver-instance.contract.ts @@ -10,7 +10,7 @@ export const DriverCapability = type({ id: DriverCapabilityId, status: '"supported" | "unsupported"', version: "1", -}); +}).onUndeclaredKey("reject"); export type DriverCapability = typeof DriverCapability.infer; export const DriverInstanceProtocol = type('"orpc-ws"'); diff --git a/pkgs/contracts/src/runtime/external-tool-effect.contract.ts b/pkgs/contracts/src/runtime/external-tool-effect.contract.ts index b14f100b..7a44be6e 100644 --- a/pkgs/contracts/src/runtime/external-tool-effect.contract.ts +++ b/pkgs/contracts/src/runtime/external-tool-effect.contract.ts @@ -1,7 +1,27 @@ import { type } from "arktype"; import { NonEmptyString } from "../validation/primitives.contract"; -import { McpExecuteCommandResult } from "./runtime-command.contract"; +import { + McpExecuteCommandResult, + RUNTIME_COMMAND_TERMINAL_PAYLOAD_MAX_UTF8_BYTES, +} from "./runtime-command.contract"; + +declare const TextEncoder: new () => { encode(input?: string): Uint8Array }; + +export const MCP_EXTERNAL_TOOL_EFFECT_SETTLEMENT_MAX_UTF8_BYTES = + RUNTIME_COMMAND_TERMINAL_PAYLOAD_MAX_UTF8_BYTES; + +/** Canonical UUID generated once for one Driver-side provider invocation. */ +export const ExternalToolEffectClaimToken = type( + /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/, +); +export type ExternalToolEffectClaimToken = typeof ExternalToolEffectClaimToken.infer; + +const externalToolEffectSettlementEncoder = new TextEncoder(); + +export function measureMcpExternalToolEffectSettlement(settlement: unknown): number { + return externalToolEffectSettlementEncoder.encode(JSON.stringify(settlement)).byteLength; +} /** * Durable state for a write-capable call made outside the Mosoo control plane. @@ -9,29 +29,66 @@ import { McpExecuteCommandResult } from "./runtime-command.contract"; * An effect never transitions out of `unknown` automatically: no generic MCP * receipt or reconciliation protocol exists, so replay would be unsafe. */ -export const ExternalToolEffectStatus = type('"intent" | "executing" | "succeeded" | "unknown"'); +export const ExternalToolEffectStatus = type('"intent" | "claimed" | "succeeded" | "unknown"'); export type ExternalToolEffectStatus = typeof ExternalToolEffectStatus.infer; -export const ExternalToolEffectAttemptStatus = type('"executing" | "succeeded" | "unknown"'); +export const ExternalToolEffectAttemptStatus = type('"claimed" | "succeeded" | "unknown"'); export type ExternalToolEffectAttemptStatus = typeof ExternalToolEffectAttemptStatus.infer; -export const ExternalToolEffectClaim = type({ - attempt: "number >= 1", +const externalToolEffectIntent = type({ + effectId: NonEmptyString, + kind: '"intent"', +}).onUndeclaredKey("reject"); +const externalToolEffectClaimed = type({ + attempt: "number.integer >= 1 & number.safe", effectId: NonEmptyString, idempotencyKey: NonEmptyString, - kind: '"execute"', + kind: '"claimed"', +}).onUndeclaredKey("reject"); +const externalToolEffectSucceeded = type({ + effectId: NonEmptyString, + kind: '"succeeded"', + result: McpExecuteCommandResult, +}).onUndeclaredKey("reject"); +const externalToolEffectUnknown = type({ + effectId: NonEmptyString, + kind: '"unknown"', +}).onUndeclaredKey("reject"); + +/** The ledger's canonical answer to observe, claim, and settlement RPCs. */ +export const ExternalToolEffectState = externalToolEffectIntent + .or(externalToolEffectClaimed) + .or(externalToolEffectSucceeded) + .or(externalToolEffectUnknown); +export type ExternalToolEffectState = typeof ExternalToolEffectState.infer; + +const externalToolEffectSucceededSettlement = type({ + kind: '"succeeded"', + "providerReceiptJson?": "string | null", + result: McpExecuteCommandResult, }) - .or( - type({ - effectId: NonEmptyString, - kind: '"completed"', - result: McpExecuteCommandResult, - }), - ) - .or( - type({ - effectId: NonEmptyString, - kind: '"unknown"', - }), - ); + .onUndeclaredKey("reject") + .narrow((settlement, context) => { + const byteLength = measureMcpExternalToolEffectSettlement(settlement); + + return byteLength <= MCP_EXTERNAL_TOOL_EFFECT_SETTLEMENT_MAX_UTF8_BYTES + ? true + : context.reject({ + actual: `${byteLength} UTF-8 bytes`, + expected: `at most ${MCP_EXTERNAL_TOOL_EFFECT_SETTLEMENT_MAX_UTF8_BYTES} UTF-8 bytes`, + }); + }); + +/** The only terminal observations a claim owner may settle. */ +export const ExternalToolEffectSettlement = externalToolEffectSucceededSettlement.or( + type({ + kind: '"unknown"', + }).onUndeclaredKey("reject"), +); +export type ExternalToolEffectSettlement = typeof ExternalToolEffectSettlement.infer; + +/** A successful claim always carries the grant required to execute once. */ +export const ExternalToolEffectClaim = externalToolEffectClaimed + .or(externalToolEffectSucceeded) + .or(externalToolEffectUnknown); export type ExternalToolEffectClaim = typeof ExternalToolEffectClaim.infer; diff --git a/pkgs/contracts/src/runtime/runtime-command.contract.ts b/pkgs/contracts/src/runtime/runtime-command.contract.ts index 3c7ac81f..cab85679 100644 --- a/pkgs/contracts/src/runtime/runtime-command.contract.ts +++ b/pkgs/contracts/src/runtime/runtime-command.contract.ts @@ -1,24 +1,62 @@ import { type } from "arktype"; -import { RunError } from "../session/session-run.contract"; +import { DURABLE_RUN_ERROR_MAX_UTF8_BYTES, DurableRunError } from "../session/session-run.contract"; import { NonEmptyString, parseSchemaValue } from "../validation/primitives.contract"; +declare const TextEncoder: new () => { encode(input?: string): Uint8Array }; + +const D1_TABLE_ROW_MAX_UTF8_BYTES = 2_000_000; +const RUNTIME_COMMAND_ROW_RESERVED_UTF8_BYTES = 128 * 1_024; + +/** + * A driver_command row stores the command beside exactly one terminal payload. + * The reserve covers every fixed column and SQLite record overhead below D1's + * 2,000,000-byte row limit. + */ +export const RUNTIME_COMMAND_TERMINAL_PAYLOAD_MAX_UTF8_BYTES = DURABLE_RUN_ERROR_MAX_UTF8_BYTES; +export const RUNTIME_COMMAND_MAX_UTF8_BYTES = + D1_TABLE_ROW_MAX_UTF8_BYTES - + RUNTIME_COMMAND_TERMINAL_PAYLOAD_MAX_UTF8_BYTES - + RUNTIME_COMMAND_ROW_RESERVED_UTF8_BYTES; + +const runtimeCommandEncoder = new TextEncoder(); + +export function measureRuntimeCommandJson(value: unknown): number { + return runtimeCommandEncoder.encode(JSON.stringify(value)).byteLength; +} + +function withinRuntimeCommandLimit( + value: unknown, + limit: number, + context: { reject(input: { actual: string; expected: string }): false }, +) { + const byteLength = measureRuntimeCommandJson(value); + + return byteLength <= limit + ? true + : context.reject({ + actual: `${byteLength} UTF-8 bytes`, + expected: `at most ${limit} UTF-8 bytes`, + }); +} + export const RuntimeCommandStatus = type( '"queued" | "delivered" | "accepted" | "completed" | "failed" | "expired" | "cancelled"', ); export type RuntimeCommandStatus = typeof RuntimeCommandStatus.infer; export const RuntimeCommandInput = type({ - "attachmentIds?": "string[]", + "attachmentIds?": NonEmptyString.array(), text: NonEmptyString, -}); +}).onUndeclaredKey("reject"); export type RuntimeCommandInput = typeof RuntimeCommandInput.infer; export const TurnCancelCommand = type({ commandId: NonEmptyString, kind: '"turn.cancel"', "reason?": "string", -}); + runId: NonEmptyString, +}).onUndeclaredKey("reject"); export type TurnCancelCommand = typeof TurnCancelCommand.infer; export const InputStartCommand = type({ @@ -27,14 +65,14 @@ export const InputStartCommand = type({ kind: '"input.start"', requestId: NonEmptyString, runId: NonEmptyString, -}); +}).onUndeclaredKey("reject"); export type InputStartCommand = typeof InputStartCommand.infer; export const SessionStopCommand = type({ commandId: NonEmptyString, kind: '"session.stop"', reason: NonEmptyString, -}); +}).onUndeclaredKey("reject"); export type SessionStopCommand = typeof SessionStopCommand.infer; export const McpExecuteCommand = type({ @@ -42,10 +80,11 @@ export const McpExecuteCommand = type({ commandId: NonEmptyString, kind: '"mcp.execute"', requestId: NonEmptyString, + runId: NonEmptyString, serverId: NonEmptyString, toolCallId: NonEmptyString, toolName: NonEmptyString, -}); +}).onUndeclaredKey("reject"); export type McpExecuteCommand = typeof McpExecuteCommand.infer; export const PermissionResolveCommand = type({ @@ -53,38 +92,50 @@ export const PermissionResolveCommand = type({ decision: '"allow_once" | "reject_once"', kind: '"permission.resolve"', requestId: NonEmptyString, -}); + runId: NonEmptyString, +}).onUndeclaredKey("reject"); export type PermissionResolveCommand = typeof PermissionResolveCommand.infer; export const RuntimeCommand = TurnCancelCommand.or(InputStartCommand) .or(McpExecuteCommand) .or(SessionStopCommand) - .or(PermissionResolveCommand); + .or(PermissionResolveCommand) + .narrow((command, context) => + withinRuntimeCommandLimit(command, RUNTIME_COMMAND_MAX_UTF8_BYTES, context), + ); export type RuntimeCommand = typeof RuntimeCommand.infer; export const InputStartCommandResult = type({ requestId: NonEmptyString, -}); +}).onUndeclaredKey("reject"); export type InputStartCommandResult = typeof InputStartCommandResult.infer; export const McpExecuteCommandResult = type({ + "isError?": "boolean", outputText: "string", requestId: NonEmptyString, serverId: NonEmptyString, toolName: NonEmptyString, -}); +}) + .onUndeclaredKey("reject") + .narrow((result, context) => + withinRuntimeCommandLimit(result, RUNTIME_COMMAND_TERMINAL_PAYLOAD_MAX_UTF8_BYTES, context), + ); export type McpExecuteCommandResult = typeof McpExecuteCommandResult.infer; export const RuntimeCommandResult = type("null") .or(InputStartCommandResult) - .or(McpExecuteCommandResult); + .or(McpExecuteCommandResult) + .narrow((result, context) => + withinRuntimeCommandLimit(result, RUNTIME_COMMAND_TERMINAL_PAYLOAD_MAX_UTF8_BYTES, context), + ); export type RuntimeCommandResult = typeof RuntimeCommandResult.infer; const runtimeCommandRecordBase = { ackedAt: "string | null", completedAt: "string | null", driverInstanceId: NonEmptyString, - error: RunError.or("null"), + error: DurableRunError.or("null"), expiresAt: "string | null", id: NonEmptyString, issuedAt: "string", @@ -98,13 +149,14 @@ export const RuntimeCommandRecord = type({ payload: TurnCancelCommand, result: "null", }) + .onUndeclaredKey("reject") .or( type({ ...runtimeCommandRecordBase, kind: '"input.start"', payload: InputStartCommand, result: type("null").or(InputStartCommandResult), - }), + }).onUndeclaredKey("reject"), ) .or( type({ @@ -112,7 +164,7 @@ export const RuntimeCommandRecord = type({ kind: '"mcp.execute"', payload: McpExecuteCommand, result: type("null").or(McpExecuteCommandResult), - }), + }).onUndeclaredKey("reject"), ) .or( type({ @@ -120,7 +172,7 @@ export const RuntimeCommandRecord = type({ kind: '"session.stop"', payload: SessionStopCommand, result: "null", - }), + }).onUndeclaredKey("reject"), ) .or( type({ @@ -128,8 +180,37 @@ export const RuntimeCommandRecord = type({ kind: '"permission.resolve"', payload: PermissionResolveCommand, result: "null", - }), - ); + }).onUndeclaredKey("reject"), + ) + .narrow((record, context) => { + const terminalPayloadMatchesStatus = + record.result !== null + ? record.status === "completed" && record.error === null + : record.error !== null + ? record.status !== "completed" && + ["failed", "expired", "cancelled"].includes(record.status) + : true; + if (!terminalPayloadMatchesStatus) { + return context.reject({ + actual: `result=${record.result === null ? "null" : "present"}, error=${record.error === null ? "null" : "present"}, status=${record.status}`, + expected: "result only for completed commands and error only for failed terminal commands", + }); + } + + const payloadWithinLimit = withinRuntimeCommandLimit( + record.payload, + RUNTIME_COMMAND_MAX_UTF8_BYTES, + context, + ); + + return payloadWithinLimit + ? withinRuntimeCommandLimit( + record.result, + RUNTIME_COMMAND_TERMINAL_PAYLOAD_MAX_UTF8_BYTES, + context, + ) + : payloadWithinLimit; + }); export type RuntimeCommandRecord = typeof RuntimeCommandRecord.infer; export function parseRuntimeCommand(value: unknown): RuntimeCommand { diff --git a/pkgs/contracts/src/runtime/sandbox.contract.ts b/pkgs/contracts/src/runtime/sandbox.contract.ts index 6032e430..ecfb71fc 100644 --- a/pkgs/contracts/src/runtime/sandbox.contract.ts +++ b/pkgs/contracts/src/runtime/sandbox.contract.ts @@ -21,7 +21,10 @@ export type SandboxSubjectKind = typeof SandboxSubjectKind.infer; export const SandboxStatus = type('"cold" | "restoring" | "active" | "backing_up" | "destroying"'); export type SandboxStatus = typeof SandboxStatus.infer; -export const SandboxSessionStatus = type('"active" | "closed" | "error"'); +export const SandboxOperationKind = type('"activate" | "hibernate" | "recreate" | "reset"'); +export type SandboxOperationKind = typeof SandboxOperationKind.infer; + +export const SandboxSessionStatus = type('"active" | "cleanup_pending" | "closed" | "error"'); export type SandboxSessionStatus = typeof SandboxSessionStatus.infer; export const SandboxBackupStatus = type('"creating" | "ready" | "restoring" | "failed" | "pruned"'); diff --git a/pkgs/contracts/src/session/session-run.contract.ts b/pkgs/contracts/src/session/session-run.contract.ts index b7e9d752..5be06cb3 100644 --- a/pkgs/contracts/src/session/session-run.contract.ts +++ b/pkgs/contracts/src/session/session-run.contract.ts @@ -3,6 +3,8 @@ import { type } from "arktype"; import type { AgentDeploymentVersionId, SessionRunId } from "../id/id.contract"; import { NonEmptyString, PrimitiveRecord } from "../validation/primitives.contract"; +declare const TextEncoder: new () => { encode(input?: string): Uint8Array }; + export const RunError = type({ code: NonEmptyString, details: PrimitiveRecord, @@ -11,6 +13,27 @@ export const RunError = type({ }); export type RunError = typeof RunError.infer; +export const DURABLE_RUN_ERROR_MAX_UTF8_BYTES = 1_020 * 1_024; + +const durableRunErrorEncoder = new TextEncoder(); + +export function measureDurableRunErrorJson(error: unknown): number { + return durableRunErrorEncoder.encode(JSON.stringify(error)).byteLength; +} + +/** Maximum error payload that can be persisted on a D1-backed terminal row. */ +export const DurableRunError = RunError.onUndeclaredKey("reject").narrow((error, context) => { + const byteLength = measureDurableRunErrorJson(error); + + return byteLength <= DURABLE_RUN_ERROR_MAX_UTF8_BYTES + ? true + : context.reject({ + actual: `${byteLength} UTF-8 bytes`, + expected: `at most ${DURABLE_RUN_ERROR_MAX_UTF8_BYTES} UTF-8 bytes`, + }); +}); +export type DurableRunError = typeof DurableRunError.infer; + export const SESSION_RUN_TRIGGERS = ["user_prompt", "retry", "resume", "system"] as const; export const SessionRunTrigger = type.enumerated(...SESSION_RUN_TRIGGERS); export type SessionRunTrigger = typeof SessionRunTrigger.infer; @@ -33,7 +56,7 @@ export interface SessionRunSummary { createdAt: string; deploymentVersionId: AgentDeploymentVersionId | null; deploymentVersionNumber: number | null; - error: RunError | null; + error: DurableRunError | null; id: SessionRunId; model: string | null; provider: string | null; diff --git a/pkgs/contracts/src/session/session.contract.ts b/pkgs/contracts/src/session/session.contract.ts index 0d925552..4a137f32 100644 --- a/pkgs/contracts/src/session/session.contract.ts +++ b/pkgs/contracts/src/session/session.contract.ts @@ -20,6 +20,8 @@ import type { import type { AgentMcpCredentialMode } from "../mcp/mcp.contract"; import type { SessionRunSummary, UserWarning } from "./session-run.contract"; +declare const TextEncoder: new () => { encode(input?: string): Uint8Array }; + export const SESSION_STATUSES = ["IDLE", "RUNNING", "RESCHEDULING", "TERMINATED"] as const; export type SessionStatus = (typeof SESSION_STATUSES)[number]; @@ -99,10 +101,17 @@ export type SessionMessageSegment = argsText: string; kind: "tool_use"; path: string | null; + runId?: SessionRunId | null; tool: string; toolCallId: string; } - | { kind: "tool_result"; output: string; tool: string; toolCallId: string }; + | { + kind: "tool_result"; + output: string; + runId?: SessionRunId | null; + tool: string; + toolCallId: string; + }; /** * The agent's current understanding of its work-to-do for this assistant @@ -497,21 +506,7 @@ const AGENT_TASK_SNAPSHOT_MAX_TASKS = 256; const AGENT_TASK_ID_MAX_UTF8_BYTES = 256; const AGENT_TASK_TEXT_MAX_CODE_UNITS = 4096; const AGENT_TASK_PAYLOAD_MAX_UTF8_BYTES = 1020 * 1024; - -function getUtf8ByteLength(value: string): number { - let bytes = 0; - - for (let index = 0; index < value.length; index += 1) { - const codePoint = value.codePointAt(index) ?? 0; - bytes += codePoint <= 0x7f ? 1 : codePoint <= 0x7ff ? 2 : codePoint <= 0xffff ? 3 : 4; - - if (codePoint > 0xffff) { - index += 1; - } - } - - return bytes; -} +const agentTaskTextEncoder = new TextEncoder(); export const AgentTask = type({ taskId: "string > 0", @@ -520,7 +515,7 @@ export const AgentTask = type({ }) .onUndeclaredKey("reject") .narrow((task, context) => { - if (getUtf8ByteLength(task.taskId) > AGENT_TASK_ID_MAX_UTF8_BYTES) { + if (agentTaskTextEncoder.encode(task.taskId).byteLength > AGENT_TASK_ID_MAX_UTF8_BYTES) { return context.reject({ actual: task.taskId, expected: `a taskId of at most ${AGENT_TASK_ID_MAX_UTF8_BYTES} UTF-8 bytes`, @@ -563,7 +558,7 @@ export const AgentTasksReplacedPayload = type({ }) .onUndeclaredKey("reject") .narrow((payload, context) => { - const byteLength = getUtf8ByteLength(JSON.stringify(payload)); + const byteLength = agentTaskTextEncoder.encode(JSON.stringify(payload)).byteLength; return byteLength <= AGENT_TASK_PAYLOAD_MAX_UTF8_BYTES ? true @@ -581,7 +576,9 @@ export const AgentTaskSnapshot = type({ }) .onUndeclaredKey("reject") .narrow((snapshot, context) => { - const byteLength = getUtf8ByteLength(JSON.stringify({ tasks: snapshot.tasks })); + const byteLength = agentTaskTextEncoder.encode( + JSON.stringify({ tasks: snapshot.tasks }), + ).byteLength; return byteLength <= AGENT_TASK_PAYLOAD_MAX_UTF8_BYTES ? true diff --git a/pkgs/contracts/src/validation/primitives.contract.ts b/pkgs/contracts/src/validation/primitives.contract.ts index 9d9aa6cf..c4435f72 100644 --- a/pkgs/contracts/src/validation/primitives.contract.ts +++ b/pkgs/contracts/src/validation/primitives.contract.ts @@ -16,7 +16,9 @@ export function parseSchemaValue(schema: SchemaParser, value: un export const NonEmptyString = type("string > 0"); export type NonEmptyString = typeof NonEmptyString.infer; -export const PrimitiveValue = type("string | number | boolean | null"); +const FiniteNumber = type("number").narrow((value) => Number.isFinite(value)); + +export const PrimitiveValue = type("string | boolean | null").or(FiniteNumber); export type PrimitiveValue = typeof PrimitiveValue.infer; export const PrimitiveRecord = type({ diff --git a/pkgs/contracts/tests/owner-boundaries.test.ts b/pkgs/contracts/tests/owner-boundaries.test.ts index cefb0deb..e3c45d53 100644 --- a/pkgs/contracts/tests/owner-boundaries.test.ts +++ b/pkgs/contracts/tests/owner-boundaries.test.ts @@ -1,5 +1,4 @@ import { describe, expect, test } from "bun:test"; -import { existsSync, readFileSync } from "node:fs"; import * as Contracts from "@mosoo/contracts"; import { @@ -15,6 +14,14 @@ import { parseAgentManifestInput, parseAgentPackageJson, } from "@mosoo/contracts/agent-manifest-parser"; +import { DriverCapability } from "@mosoo/contracts/driver-instance"; +import { + ExternalToolEffectClaim, + ExternalToolEffectSettlement, + ExternalToolEffectState, + MCP_EXTERNAL_TOOL_EFFECT_SETTLEMENT_MAX_UTF8_BYTES, + measureMcpExternalToolEffectSettlement, +} from "@mosoo/contracts/external-tool-effect"; import { SESSION_RESOURCE_MOUNT_DIR, createAccountAvatarPath, @@ -36,34 +43,83 @@ import { isCustomRuntimeModelProvider, parseRuntimeModelIdentity, } from "@mosoo/contracts/models"; -import { parseRuntimeCommand } from "@mosoo/contracts/runtime-command"; +import { + RUNTIME_COMMAND_MAX_UTF8_BYTES, + RUNTIME_COMMAND_TERMINAL_PAYLOAD_MAX_UTF8_BYTES, + RuntimeCommandResult, + RuntimeCommandRecord, + measureRuntimeCommandJson, + parseRuntimeCommand, +} from "@mosoo/contracts/runtime-command"; import { AGENT_SESSION_ARCHIVED_READ_ONLY_REASON, AGENT_SESSION_TERMINAL_READ_ONLY_REASON, getAgentSessionUserLifecycleProjection, } from "@mosoo/contracts/session"; +import { DurableRunError } from "@mosoo/contracts/session-run"; +import { PrimitiveRecord } from "@mosoo/contracts/validation"; const FILE_ID = "01J00000000000000000000001" as FileId; const SESSION_ID = "01J00000000000000000000002" as SessionId; const ACCOUNT_ID = "01J00000000000000000000003" as AccountId; -function readFixture(path: string): string { - return readFileSync(new URL(path, import.meta.url), "utf8"); +function textFieldAtJsonSize( + targetBytes: number, + create: (text: string) => Value, + unit = "x", +): Value { + const baseBytes = measureRuntimeCommandJson(create("")); + const unitBytes = measureRuntimeCommandJson(create(unit)) - baseBytes; + const remaining = targetBytes - baseBytes; + const value = create( + unit.repeat(Math.floor(remaining / unitBytes)) + "x".repeat(remaining % unitBytes), + ); + + expect(measureRuntimeCommandJson(value)).toBe(targetBytes); + return value; +} + +function mcpCommandAtSize(targetBytes: number, unit = "x") { + return textFieldAtJsonSize( + targetBytes, + (argumentsJson) => ({ + argumentsJson, + commandId: "command-1", + kind: "mcp.execute" as const, + requestId: "request-1", + runId: "run-1", + serverId: "server-1", + toolCallId: "tool-call-1", + toolName: "tool-1", + }), + unit, + ); +} + +function mcpSettlementAtSize(targetBytes: number, unit: string) { + const create = (providerReceiptJson: string) => ({ + kind: "succeeded" as const, + providerReceiptJson, + result: { + outputText: "created", + requestId: "request-1", + serverId: "server-1", + toolName: "tool-1", + }, + }); + const baseBytes = measureMcpExternalToolEffectSettlement(create("")); + const unitBytes = measureMcpExternalToolEffectSettlement(create(unit)) - baseBytes; + const remaining = targetBytes - baseBytes; + const settlement = create( + unit.repeat(Math.floor(remaining / unitBytes)) + "x".repeat(remaining % unitBytes), + ); + + expect(measureMcpExternalToolEffectSettlement(settlement)).toBe(targetBytes); + return settlement; } describe("contracts owner boundaries", () => { test("does not expose the old permission package surface", () => { - const packageJson = readFixture("../package.json"); - const indexSource = readFixture("../src/index.ts"); - - expect(packageJson).not.toContain('"./permission"'); - expect(indexSource).not.toContain("permission.contract"); - expect(indexSource).not.toContain("./permission/"); - expect(packageJson).not.toContain("providers.company.create"); - expect(packageJson).not.toContain("agents.acl."); - expect(existsSync(new URL("../src/permission/permission.contract.ts", import.meta.url))).toBe( - false, - ); expect("Permission" in Contracts).toBe(false); expect("can" in Contracts).toBe(false); }); @@ -205,6 +261,15 @@ describe("contracts owner boundaries", () => { }).kind, ).toBe("input.start"); + expect(() => + parseRuntimeCommand({ + commandId: "cmd_1", + input: { attachmentIds: [""], text: "Run it." }, + kind: "input.start", + requestId: "req_1", + runId: "run_1", + }), + ).toThrow(); expect(() => parseRuntimeCommand({ commandId: "cmd_1", @@ -223,6 +288,218 @@ describe("contracts owner boundaries", () => { ).toThrow(); }); + test.each([ + ["turn.cancel", { commandId: "cmd_1", kind: "turn.cancel" }], + [ + "input.start", + { + commandId: "cmd_1", + input: { text: "Run it." }, + kind: "input.start", + requestId: "req_1", + runId: "run_1", + }, + ], + ["session.stop", { commandId: "cmd_1", kind: "session.stop", reason: "done" }], + [ + "mcp.execute", + { + argumentsJson: "{}", + commandId: "cmd_1", + kind: "mcp.execute", + requestId: "req_1", + runId: "run_1", + serverId: "server_1", + toolCallId: "tool_call_1", + toolName: "createIssue", + }, + ], + [ + "permission.resolve", + { + commandId: "cmd_1", + decision: "allow_once", + kind: "permission.resolve", + requestId: "req_1", + runId: "run_1", + }, + ], + ] as const)("keeps persisted %s commands exact", (_kind, command) => { + expect(() => parseRuntimeCommand({ ...command, debug: true })).toThrow(); + }); + + test("keeps nested command inputs and terminal payloads exact", () => { + expect(() => + parseRuntimeCommand({ + commandId: "cmd_1", + input: { debug: true, text: "Run it." }, + kind: "input.start", + requestId: "req_1", + runId: "run_1", + }), + ).toThrow(); + expect(RuntimeCommandResult.allows({ debug: true, requestId: "req_1" })).toBeFalse(); + expect( + RuntimeCommandResult.allows({ + debug: true, + outputText: "created", + requestId: "req_1", + serverId: "server_1", + toolName: "createIssue", + }), + ).toBeFalse(); + expect( + DurableRunError.allows({ + code: "driver.failed", + debug: true, + details: {}, + message: "failed", + retryable: false, + }), + ).toBeFalse(); + expect( + ExternalToolEffectSettlement.allows({ + debug: true, + kind: "succeeded", + result: { + outputText: "created", + requestId: "req_1", + serverId: "server_1", + toolName: "createIssue", + }, + }), + ).toBeFalse(); + expect( + ExternalToolEffectState.allows({ + debug: true, + effectId: "effect_1", + kind: "succeeded", + result: { + outputText: "created", + requestId: "req_1", + serverId: "server_1", + toolName: "createIssue", + }, + }), + ).toBeFalse(); + expect( + ExternalToolEffectClaim.allows({ + attempt: 1, + debug: true, + effectId: "effect_1", + idempotencyKey: "effect_1", + kind: "claimed", + }), + ).toBeFalse(); + expect( + DriverCapability.allows({ + debug: true, + id: "mcp_execute", + status: "supported", + version: 1, + }), + ).toBeFalse(); + + const commandRecord = { + ackedAt: null, + completedAt: null, + driverInstanceId: "driver_1", + error: null, + expiresAt: null, + id: "cmd_1", + issuedAt: "2026-01-01T00:00:00.000Z", + kind: "input.start" as const, + payload: { + commandId: "cmd_1", + input: { text: "Run it." }, + kind: "input.start" as const, + requestId: "req_1", + runId: "run_1", + }, + result: null, + seq: 0, + status: "queued" as const, + }; + expect(RuntimeCommandRecord.allows({ ...commandRecord, debug: true })).toBeFalse(); + expect( + RuntimeCommandRecord.allows({ + ...commandRecord, + error: { + code: "driver.failed", + details: {}, + message: "failed", + retryable: false, + }, + result: { requestId: "req_1" }, + status: "completed", + }), + ).toBeFalse(); + expect(PrimitiveRecord.allows({ fields: Number.NaN })).toBeFalse(); + expect( + DurableRunError.allows({ + code: "driver.failed", + details: { durationMs: Number.POSITIVE_INFINITY }, + message: "failed", + retryable: false, + }), + ).toBeFalse(); + }); + + test.each(["x", "界", "\0"])( + "bounds canonical runtime command JSON after %p UTF-8 encoding", + (unit) => { + const exact = mcpCommandAtSize(RUNTIME_COMMAND_MAX_UTF8_BYTES, unit); + + expect(parseRuntimeCommand(exact)).toEqual(exact); + expect(() => + parseRuntimeCommand({ ...exact, argumentsJson: `${exact.argumentsJson}x` }), + ).toThrow(); + }, + ); + + test.each(["x", "界", "\0"])( + "bounds both driver command terminal columns after %p UTF-8 encoding", + (unit) => { + const result = textFieldAtJsonSize( + RUNTIME_COMMAND_TERMINAL_PAYLOAD_MAX_UTF8_BYTES, + (outputText) => ({ + outputText, + requestId: "request-1", + serverId: "server-1", + toolName: "tool-1", + }), + unit, + ); + const error = textFieldAtJsonSize( + RUNTIME_COMMAND_TERMINAL_PAYLOAD_MAX_UTF8_BYTES, + (message) => ({ code: "driver.failed", details: {}, message, retryable: false }), + unit, + ); + + expect(RuntimeCommandResult.allows(result)).toBeTrue(); + expect( + RuntimeCommandResult.allows({ ...result, outputText: `${result.outputText}x` }), + ).toBeFalse(); + expect(DurableRunError.allows(error)).toBeTrue(); + expect(DurableRunError.allows({ ...error, message: `${error.message}x` })).toBeFalse(); + }, + ); + + test.each(["x", "界", "\0"])( + "bounds the whole MCP settlement envelope after %p UTF-8 encoding", + (unit) => { + const exact = mcpSettlementAtSize(MCP_EXTERNAL_TOOL_EFFECT_SETTLEMENT_MAX_UTF8_BYTES, unit); + + expect(ExternalToolEffectSettlement.allows(exact)).toBeTrue(); + expect( + ExternalToolEffectSettlement.allows({ + ...exact, + providerReceiptJson: `${exact.providerReceiptJson}x`, + }), + ).toBeFalse(); + }, + ); + test("runtime model identity admits typed provider model runtime triples", () => { const identity = parseRuntimeModelIdentity({ modelId: " gpt-5 ", diff --git a/pkgs/db/drizzle/0013_durable-mcp-effect-v3.sql b/pkgs/db/drizzle/0013_durable-mcp-effect-v3.sql new file mode 100644 index 00000000..7ef2979c --- /dev/null +++ b/pkgs/db/drizzle/0013_durable-mcp-effect-v3.sql @@ -0,0 +1,987 @@ +CREATE TABLE `__durable_mcp_v3_loss_guard` ( + `candidate_count` integer NOT NULL, + CONSTRAINT "durable_mcp_v3_loss_guard_check" CHECK(`candidate_count` = 0) +); +--> statement-breakpoint +WITH `effect_source` AS ( + SELECT + `effect`.`id` AS `effect_id`, + `effect`.`status` AS `effect_status`, + `effect`.`provider_receipt_json` AS `effect_provider_receipt_json`, + `effect`.`result_json` AS `effect_result_json`, + `command`.`error_json` AS `command_error_json`, + `command`.`payload_json` AS `command_payload_json`, + `command`.`result_json` AS `command_result_json`, + `command`.`status` AS `command_status`, + max( + COALESCE(length(CAST(`effect`.`result_json` AS BLOB)), 0), + COALESCE(( + SELECT max(length(CAST(`attempt`.`result_json` AS BLOB))) + FROM `external_tool_effect_attempt` AS `attempt` + WHERE `attempt`.`effect_id` = `effect`.`id` + AND `attempt`.`status` = 'succeeded' + ), 0), + COALESCE(length(CAST(`command`.`result_json` AS BLOB)), 0) + ) AS `original_result_bytes` + FROM `external_tool_effect` AS `effect` + INNER JOIN `driver_command` AS `command` ON `command`.`id` = `effect`.`command_id` +), +`effect_result_classified` AS ( + SELECT + `effect_source`.*, + `effect_status` = 'succeeded' AND ( + `original_result_bytes` > 1044480 + OR length(CAST('{"kind":"succeeded","result":' || `effect_result_json` || '}' AS BLOB)) > 1044480 + ) AS `result_omitted` + FROM `effect_source` +), +`effect_result_target` AS ( + SELECT + `effect_result_classified`.*, + CASE + WHEN `result_omitted` THEN + '{"isError":true,"outputText":' || json_quote('Stored MCP result omitted because it contained ' || `original_result_bytes` || ' UTF-8 bytes.') || + ',"requestId":' || json_quote(json_extract(`command_payload_json`, '$.requestId')) || + ',"serverId":' || json_quote(json_extract(`command_payload_json`, '$.serverId')) || + ',"toolName":' || json_quote(json_extract(`command_payload_json`, '$.toolName')) || '}' + WHEN `effect_status` = 'succeeded' THEN `effect_result_json` + ELSE NULL + END AS `normalized_result_json` + FROM `effect_result_classified` +), +`effect_target` AS ( + SELECT + `effect_result_target`.*, + CASE + WHEN `effect_status` <> 'succeeded' THEN NULL + WHEN `effect_provider_receipt_json` IS NULL THEN NULL + WHEN length(CAST( + '{"kind":"succeeded","providerReceiptJson":' || json_quote(`effect_provider_receipt_json`) || + ',"result":' || `normalized_result_json` || '}' + AS BLOB)) > 1044480 THEN NULL + ELSE `effect_provider_receipt_json` + END AS `normalized_provider_receipt_json` + FROM `effect_result_target` +), +`loss_candidates` (`category`, `id`) AS ( + SELECT `category`.`value`, `command`.`id` + FROM `driver_command` AS `command` + CROSS JOIN json_each(json_array( + 'command_payload_conflict', + 'mcp_argument_omission', + 'input_text_omission', + 'input_start_result_omission', + 'control_reason_omission', + 'permission_payload_rewrite', + 'command_error_omission' + )) AS `category` + WHERE CASE `category`.`value` + WHEN 'command_payload_conflict' THEN + EXISTS ( + SELECT `key` + FROM json_each(`command`.`payload_json`) + GROUP BY `key` + HAVING COUNT(*) > 1 + ) + OR ( + `command`.`kind` = 'input.start' + AND EXISTS ( + SELECT `key` + FROM json_each(`command`.`payload_json`, '$.input') + GROUP BY `key` + HAVING COUNT(*) > 1 + ) + ) + WHEN 'mcp_argument_omission' THEN + `command`.`kind` = 'mcp.execute' + AND length(CAST(json_set( + `command`.`payload_json`, + '$.commandId', `command`.`id`, + '$.runId', ( + SELECT `effect`.`session_run_id` + FROM `external_tool_effect` AS `effect` + WHERE `effect`.`command_id` = `command`.`id` + LIMIT 1 + ) + ) AS BLOB)) > 824448 + WHEN 'input_text_omission' THEN + `command`.`kind` = 'input.start' + AND length(CAST(`command`.`payload_json` AS BLOB)) > 824448 + WHEN 'input_start_result_omission' THEN + `command`.`kind` = 'input.start' + AND `command`.`result_json` IS NOT NULL + AND json_type(`command`.`result_json`) <> 'null' + AND ( + length(CAST(`command`.`payload_json` AS BLOB)) > 824448 + OR length(CAST(`command`.`result_json` AS BLOB)) > 1044480 + ) + WHEN 'control_reason_omission' THEN + `command`.`kind` IN ('turn.cancel', 'session.stop') + AND length(CAST(`command`.`payload_json` AS BLOB)) > 824448 + WHEN 'permission_payload_rewrite' THEN + `command`.`kind` = 'permission.resolve' + AND length(CAST(`command`.`payload_json` AS BLOB)) > 824448 + WHEN 'command_error_omission' THEN + `command`.`error_json` IS NOT NULL + AND length(CAST(`command`.`error_json` AS BLOB)) > 1044480 + AND NOT EXISTS ( + SELECT 1 + FROM `external_tool_effect` AS `effect` + WHERE `effect`.`command_id` = `command`.`id` AND `effect`.`status` = 'succeeded' + ) + ELSE 0 + END + UNION ALL + SELECT `category`.`value`, `target`.`effect_id` + FROM `effect_target` AS `target` + CROSS JOIN json_each(json_array( + 'mcp_result_omission', + 'mcp_result_conflict', + 'provider_receipt_loss', + 'mcp_command_terminal_conflict' + )) AS `category` + WHERE CASE `category`.`value` + WHEN 'mcp_result_omission' THEN `target`.`result_omitted` + WHEN 'mcp_result_conflict' THEN + ( + NOT `target`.`result_omitted` + AND ( + (`target`.`effect_status` <> 'succeeded' AND `target`.`effect_result_json` IS NOT NULL) + OR ( + `target`.`effect_status` = 'succeeded' + AND `target`.`command_result_json` IS NOT NULL + AND json_type(`target`.`command_result_json`) <> 'null' + AND `target`.`command_result_json` IS NOT `target`.`effect_result_json` + ) + OR EXISTS ( + SELECT 1 + FROM `external_tool_effect_attempt` AS `attempt` + WHERE `attempt`.`effect_id` = `target`.`effect_id` + AND `attempt`.`result_json` IS NOT NULL + AND ( + `attempt`.`status` <> 'succeeded' + OR `target`.`normalized_result_json` IS NULL + OR `attempt`.`result_json` IS NOT `target`.`effect_result_json` + ) + ) + ) + ) + OR ( + `target`.`effect_result_json` IS NOT NULL + AND EXISTS ( + SELECT `key` + FROM json_each(`target`.`effect_result_json`) + GROUP BY `key` + HAVING COUNT(*) > 1 + ) + ) + OR ( + `target`.`command_result_json` IS NOT NULL + AND EXISTS ( + SELECT `key` + FROM json_each(`target`.`command_result_json`) + GROUP BY `key` + HAVING COUNT(*) > 1 + ) + ) + OR EXISTS ( + SELECT 1 + FROM `external_tool_effect_attempt` AS `attempt` + WHERE `attempt`.`effect_id` = `target`.`effect_id` + AND `attempt`.`result_json` IS NOT NULL + AND EXISTS ( + SELECT `key` + FROM json_each(`attempt`.`result_json`) + GROUP BY `key` + HAVING COUNT(*) > 1 + ) + ) + WHEN 'provider_receipt_loss' THEN + `target`.`effect_provider_receipt_json` IS NOT `target`.`normalized_provider_receipt_json` + OR EXISTS ( + SELECT 1 + FROM `external_tool_effect_attempt` AS `attempt` + WHERE `attempt`.`effect_id` = `target`.`effect_id` + AND `attempt`.`provider_receipt_json` IS NOT NULL + AND `attempt`.`provider_receipt_json` IS NOT CASE + WHEN `attempt`.`status` = 'succeeded' THEN `target`.`normalized_provider_receipt_json` + ELSE NULL + END + ) + WHEN 'mcp_command_terminal_conflict' THEN + `target`.`effect_status` = 'succeeded' + AND ( + `target`.`command_status` <> 'completed' OR `target`.`command_error_json` IS NOT NULL + ) + ELSE 0 + END + UNION ALL + SELECT 'orphan_effect', `effect`.`id` + FROM `external_tool_effect` AS `effect` + WHERE NOT EXISTS ( + SELECT 1 FROM `driver_command` AS `command` WHERE `command`.`id` = `effect`.`command_id` + ) + UNION ALL + SELECT DISTINCT 'attempt_completion_time_fabrication', `attempt`.`effect_id` + FROM `external_tool_effect_attempt` AS `attempt` + WHERE `attempt`.`status` IN ('succeeded', 'unknown') + AND `attempt`.`completed_at` IS NULL + UNION ALL + SELECT 'session_run_error_omission', `run`.`id` + FROM `session_run` AS `run` + WHERE `run`.`error_code` IS NOT NULL + AND `run`.`error_message` IS NOT NULL + AND length(CAST( + '{"code":' || json_quote(`run`.`error_code`) || + ',"details":' || COALESCE(NULLIF(`run`.`error_details_json`, ''), '{}') || + ',"message":' || json_quote(`run`.`error_message`) || + ',"retryable":false}' + AS BLOB)) > 1044480 +) +INSERT INTO `__durable_mcp_v3_loss_guard` (`candidate_count`) +SELECT COUNT(*) FROM `loss_candidates`; +--> statement-breakpoint +DROP TABLE `__durable_mcp_v3_loss_guard`; +--> statement-breakpoint +CREATE TABLE `__durable_mcp_v3_nonterminal_guard` ( + `nonterminal_count` integer NOT NULL, + CONSTRAINT "durable_mcp_v3_nonterminal_guard_check" CHECK(`nonterminal_count` = 0) +); +--> statement-breakpoint +INSERT INTO `__durable_mcp_v3_nonterminal_guard` (`nonterminal_count`) SELECT COUNT(*) FROM `driver_command` WHERE `status` IN ('queued', 'delivered', 'accepted'); +--> statement-breakpoint +INSERT INTO `__durable_mcp_v3_nonterminal_guard` (`nonterminal_count`) +SELECT COUNT(*) +FROM `driver_command` AS `command` +WHERE `command`.`kind` = 'mcp.execute' + AND NOT EXISTS (SELECT 1 FROM `external_tool_effect` AS `effect` WHERE `effect`.`command_id` = `command`.`id`); +--> statement-breakpoint +DROP TABLE `__durable_mcp_v3_nonterminal_guard`; +--> statement-breakpoint +ALTER TABLE `driver_command` ADD `driver_generation` integer + CONSTRAINT "driver_command_generation_check" CHECK(`driver_generation` IS NULL OR (typeof(`driver_generation`) = 'integer' AND `driver_generation` BETWEEN 0 AND 9007199254740991)) + CONSTRAINT "driver_command_nonterminal_generation_check" CHECK(`status` IN ('completed', 'failed', 'expired', 'cancelled') OR `driver_generation` IS NOT NULL); +--> statement-breakpoint +CREATE TABLE `__durable_mcp_v3_source_guard` ( + `violation_count` integer NOT NULL, + CONSTRAINT "durable_mcp_v3_source_guard_check" CHECK(`violation_count` = 0) +); +--> statement-breakpoint +INSERT INTO `__durable_mcp_v3_source_guard` (`violation_count`) +SELECT COUNT(*) +FROM `driver_command` +WHERE NOT json_valid(`payload_json`) + OR (`result_json` IS NOT NULL AND NOT json_valid(`result_json`)) + OR (`error_json` IS NOT NULL AND NOT json_valid(`error_json`)); +--> statement-breakpoint +INSERT INTO `__durable_mcp_v3_source_guard` (`violation_count`) +SELECT COUNT(*) +FROM `driver_command` AS `command` +WHERE json_type(`command`.`payload_json`) IS NOT 'object' + OR json_type(`command`.`payload_json`, '$.commandId') IS NOT 'text' + OR length(json_extract(`command`.`payload_json`, '$.commandId')) = 0 + OR json_extract(`command`.`payload_json`, '$.commandId') IS NOT `command`.`id` + OR json_type(`command`.`payload_json`, '$.kind') IS NOT 'text' + OR json_extract(`command`.`payload_json`, '$.kind') IS NOT `command`.`kind` + OR `command`.`kind` NOT IN ('turn.cancel', 'input.start', 'mcp.execute', 'session.stop', 'permission.resolve') + OR (`command`.`kind` = 'turn.cancel' AND EXISTS (SELECT 1 FROM json_each(`command`.`payload_json`) WHERE `key` NOT IN ('commandId', 'kind', 'reason'))) + OR (`command`.`kind` = 'turn.cancel' AND json_type(`command`.`payload_json`, '$.reason') IS NOT NULL AND json_type(`command`.`payload_json`, '$.reason') IS NOT 'text') + OR (`command`.`kind` = 'input.start' AND ( + json_type(`command`.`payload_json`, '$.requestId') IS NOT 'text' + OR length(json_extract(`command`.`payload_json`, '$.requestId')) = 0 + OR json_type(`command`.`payload_json`, '$.runId') IS NOT 'text' + OR length(json_extract(`command`.`payload_json`, '$.runId')) = 0 + OR json_type(`command`.`payload_json`, '$.input') IS NOT 'object' + OR json_type(`command`.`payload_json`, '$.input.text') IS NOT 'text' + OR length(json_extract(`command`.`payload_json`, '$.input.text')) = 0 + OR (json_type(`command`.`payload_json`, '$.input.attachmentIds') IS NOT NULL AND json_type(`command`.`payload_json`, '$.input.attachmentIds') IS NOT 'array') + OR EXISTS (SELECT 1 FROM json_each(`command`.`payload_json`, '$.input.attachmentIds') WHERE `type` <> 'text' OR length(`value`) = 0) + OR EXISTS (SELECT 1 FROM json_each(`command`.`payload_json`) WHERE `key` NOT IN ('commandId', 'input', 'kind', 'requestId', 'runId')) + OR EXISTS (SELECT 1 FROM json_each(`command`.`payload_json`, '$.input') WHERE `key` NOT IN ('attachmentIds', 'text')) + )) + OR (`command`.`kind` = 'mcp.execute' AND ( + json_type(`command`.`payload_json`, '$.argumentsJson') IS NOT 'text' + OR json_type(`command`.`payload_json`, '$.requestId') IS NOT 'text' + OR length(json_extract(`command`.`payload_json`, '$.requestId')) = 0 + OR json_type(`command`.`payload_json`, '$.serverId') IS NOT 'text' + OR length(json_extract(`command`.`payload_json`, '$.serverId')) = 0 + OR json_type(`command`.`payload_json`, '$.toolCallId') IS NOT 'text' + OR length(json_extract(`command`.`payload_json`, '$.toolCallId')) = 0 + OR json_type(`command`.`payload_json`, '$.toolName') IS NOT 'text' + OR length(json_extract(`command`.`payload_json`, '$.toolName')) = 0 + OR EXISTS (SELECT 1 FROM json_each(`command`.`payload_json`) WHERE `key` NOT IN ('argumentsJson', 'commandId', 'kind', 'requestId', 'serverId', 'toolCallId', 'toolName')) + )) + OR (`command`.`kind` = 'session.stop' AND ( + json_type(`command`.`payload_json`, '$.reason') IS NOT 'text' + OR length(json_extract(`command`.`payload_json`, '$.reason')) = 0 + OR EXISTS (SELECT 1 FROM json_each(`command`.`payload_json`) WHERE `key` NOT IN ('commandId', 'kind', 'reason')) + )) + OR (`command`.`kind` = 'permission.resolve' AND ( + json_type(`command`.`payload_json`, '$.requestId') IS NOT 'text' + OR length(json_extract(`command`.`payload_json`, '$.requestId')) = 0 + OR json_type(`command`.`payload_json`, '$.decision') IS NOT 'text' + OR json_extract(`command`.`payload_json`, '$.decision') NOT IN ('allow_once', 'reject_once') + OR EXISTS (SELECT 1 FROM json_each(`command`.`payload_json`) WHERE `key` NOT IN ('commandId', 'decision', 'kind', 'requestId')) + )); +--> statement-breakpoint +INSERT INTO `__durable_mcp_v3_source_guard` (`violation_count`) +SELECT COUNT(*) +FROM `driver_command` AS `command` +WHERE (`command`.`result_json` IS NOT NULL AND json_type(`command`.`result_json`) NOT IN ('null', 'object')) + OR (`command`.`error_json` IS NOT NULL AND json_type(`command`.`error_json`) IS NOT 'object') + OR (`command`.`kind` IN ('turn.cancel', 'session.stop', 'permission.resolve') AND `command`.`result_json` IS NOT NULL AND json_type(`command`.`result_json`) IS NOT 'null') + OR (`command`.`kind` = 'input.start' AND `command`.`result_json` IS NOT NULL AND json_type(`command`.`result_json`) IS NOT 'null' AND ( + json_type(`command`.`result_json`, '$.requestId') IS NOT 'text' + OR length(json_extract(`command`.`result_json`, '$.requestId')) = 0 + OR EXISTS (SELECT 1 FROM json_each(`command`.`result_json`) WHERE `key` <> 'requestId') + )) + OR (`command`.`kind` = 'mcp.execute' AND `command`.`result_json` IS NOT NULL AND json_type(`command`.`result_json`) IS NOT 'null' AND ( + json_type(`command`.`result_json`, '$.outputText') IS NOT 'text' + OR json_type(`command`.`result_json`, '$.requestId') IS NOT 'text' + OR length(json_extract(`command`.`result_json`, '$.requestId')) = 0 + OR json_type(`command`.`result_json`, '$.serverId') IS NOT 'text' + OR length(json_extract(`command`.`result_json`, '$.serverId')) = 0 + OR json_type(`command`.`result_json`, '$.toolName') IS NOT 'text' + OR length(json_extract(`command`.`result_json`, '$.toolName')) = 0 + OR (json_type(`command`.`result_json`, '$.isError') IS NOT NULL AND json_type(`command`.`result_json`, '$.isError') NOT IN ('true', 'false')) + OR EXISTS (SELECT 1 FROM json_each(`command`.`result_json`) WHERE `key` NOT IN ('isError', 'outputText', 'requestId', 'serverId', 'toolName')) + )) + OR (`command`.`error_json` IS NOT NULL AND ( + json_type(`command`.`error_json`, '$.code') IS NOT 'text' + OR length(json_extract(`command`.`error_json`, '$.code')) = 0 + OR json_type(`command`.`error_json`, '$.message') IS NOT 'text' + OR length(json_extract(`command`.`error_json`, '$.message')) = 0 + OR (json_type(`command`.`error_json`, '$.retryable') IS NOT 'true' AND json_type(`command`.`error_json`, '$.retryable') IS NOT 'false') + OR json_type(`command`.`error_json`, '$.details') IS NOT 'object' + OR EXISTS (SELECT 1 FROM json_each(`command`.`error_json`, '$.details') WHERE `type` NOT IN ('null', 'integer', 'real', 'text', 'true', 'false')) + OR EXISTS (SELECT 1 FROM json_each(`command`.`error_json`) WHERE `key` NOT IN ('code', 'details', 'message', 'retryable')) + )); +--> statement-breakpoint +INSERT INTO `__durable_mcp_v3_source_guard` (`violation_count`) +SELECT + (SELECT COUNT(*) FROM `external_tool_effect` WHERE `result_json` IS NOT NULL AND NOT json_valid(`result_json`)) + + (SELECT COUNT(*) FROM `external_tool_effect_attempt` WHERE `result_json` IS NOT NULL AND NOT json_valid(`result_json`)); +--> statement-breakpoint +INSERT INTO `__durable_mcp_v3_source_guard` (`violation_count`) +SELECT + (SELECT COUNT(*) FROM `external_tool_effect` WHERE (`status` = 'succeeded' AND `result_json` IS NULL) OR (`result_json` IS NOT NULL AND ( + json_type(`result_json`) IS NOT 'object' + OR json_type(`result_json`, '$.outputText') IS NOT 'text' + OR json_type(`result_json`, '$.requestId') IS NOT 'text' + OR length(json_extract(`result_json`, '$.requestId')) = 0 + OR json_type(`result_json`, '$.serverId') IS NOT 'text' + OR length(json_extract(`result_json`, '$.serverId')) = 0 + OR json_type(`result_json`, '$.toolName') IS NOT 'text' + OR length(json_extract(`result_json`, '$.toolName')) = 0 + OR (json_type(`result_json`, '$.isError') IS NOT NULL AND json_type(`result_json`, '$.isError') NOT IN ('true', 'false')) + OR EXISTS (SELECT 1 FROM json_each(`result_json`) WHERE `key` NOT IN ('isError', 'outputText', 'requestId', 'serverId', 'toolName')) + ))) + + (SELECT COUNT(*) FROM `external_tool_effect_attempt` WHERE (`status` = 'succeeded' AND `result_json` IS NULL) OR (`result_json` IS NOT NULL AND ( + json_type(`result_json`) IS NOT 'object' + OR json_type(`result_json`, '$.outputText') IS NOT 'text' + OR json_type(`result_json`, '$.requestId') IS NOT 'text' + OR length(json_extract(`result_json`, '$.requestId')) = 0 + OR json_type(`result_json`, '$.serverId') IS NOT 'text' + OR length(json_extract(`result_json`, '$.serverId')) = 0 + OR json_type(`result_json`, '$.toolName') IS NOT 'text' + OR length(json_extract(`result_json`, '$.toolName')) = 0 + OR (json_type(`result_json`, '$.isError') IS NOT NULL AND json_type(`result_json`, '$.isError') NOT IN ('true', 'false')) + OR EXISTS (SELECT 1 FROM json_each(`result_json`) WHERE `key` NOT IN ('isError', 'outputText', 'requestId', 'serverId', 'toolName')) + ))); +--> statement-breakpoint +INSERT INTO `__durable_mcp_v3_source_guard` (`violation_count`) +SELECT COUNT(*) +FROM `external_tool_effect` AS `effect` +INNER JOIN `driver_command` AS `command` ON `command`.`id` = `effect`.`command_id` +WHERE `command`.`kind` <> 'mcp.execute' + OR `effect`.`driver_instance_id` IS NOT `command`.`driver_instance_id` + OR `effect`.`server_id` IS NOT json_extract(`command`.`payload_json`, '$.serverId') + OR `effect`.`tool_name` IS NOT json_extract(`command`.`payload_json`, '$.toolName') + OR (`command`.`result_json` IS NOT NULL AND json_type(`command`.`result_json`) <> 'null' AND ( + json_extract(`command`.`result_json`, '$.requestId') IS NOT json_extract(`command`.`payload_json`, '$.requestId') + OR json_extract(`command`.`result_json`, '$.serverId') IS NOT `effect`.`server_id` + OR json_extract(`command`.`result_json`, '$.toolName') IS NOT `effect`.`tool_name` + )) + OR (`effect`.`result_json` IS NOT NULL AND ( + json_extract(`effect`.`result_json`, '$.requestId') IS NOT json_extract(`command`.`payload_json`, '$.requestId') + OR json_extract(`effect`.`result_json`, '$.serverId') IS NOT `effect`.`server_id` + OR json_extract(`effect`.`result_json`, '$.toolName') IS NOT `effect`.`tool_name` + )) + OR EXISTS ( + SELECT 1 + FROM `external_tool_effect_attempt` AS `attempt` + WHERE `attempt`.`effect_id` = `effect`.`id` + AND `attempt`.`result_json` IS NOT NULL + AND ( + json_extract(`attempt`.`result_json`, '$.requestId') IS NOT json_extract(`command`.`payload_json`, '$.requestId') + OR json_extract(`attempt`.`result_json`, '$.serverId') IS NOT `effect`.`server_id` + OR json_extract(`attempt`.`result_json`, '$.toolName') IS NOT `effect`.`tool_name` + ) + ); +--> statement-breakpoint +INSERT INTO `__durable_mcp_v3_source_guard` (`violation_count`) +SELECT COUNT(*) +FROM `session_run` +WHERE (`error_code` IS NULL AND (`error_message` IS NOT NULL OR `error_details_json` IS NOT NULL)) + OR (`error_message` IS NULL AND (`error_code` IS NOT NULL OR `error_details_json` IS NOT NULL)) + OR (`error_code` IS NOT NULL AND length(`error_code`) = 0) + OR (`error_message` IS NOT NULL AND length(`error_message`) = 0) + OR (`error_details_json` IS NOT NULL AND NOT json_valid(`error_details_json`)); +--> statement-breakpoint +INSERT INTO `__durable_mcp_v3_source_guard` (`violation_count`) +SELECT COUNT(*) +FROM `session_run` +WHERE `error_details_json` IS NOT NULL + AND ( + json_type(`error_details_json`) IS NOT 'object' + OR EXISTS (SELECT 1 FROM json_each(`error_details_json`) WHERE `type` NOT IN ('null', 'integer', 'real', 'text', 'true', 'false')) + ); +--> statement-breakpoint +DROP TABLE `__durable_mcp_v3_source_guard`; +--> statement-breakpoint +UPDATE `driver_command` SET `result_json` = NULL WHERE `result_json` IS NOT NULL AND json_type(`result_json`) = 'null'; +--> statement-breakpoint +CREATE TABLE `__durable_mcp_v3_oversized_terminal_command` ( + `id` text PRIMARY KEY NOT NULL +); +--> statement-breakpoint +INSERT INTO `__durable_mcp_v3_oversized_terminal_command` (`id`) +SELECT `command`.`id` +FROM `driver_command` AS `command` +WHERE CASE + WHEN `command`.`kind` = 'mcp.execute' THEN length(CAST(json_set( + `command`.`payload_json`, + '$.commandId', `command`.`id`, + '$.runId', (SELECT `effect`.`session_run_id` FROM `external_tool_effect` AS `effect` WHERE `effect`.`command_id` = `command`.`id` LIMIT 1) + ) AS BLOB)) > 824448 + ELSE length(CAST(`command`.`payload_json` AS BLOB)) > 824448 +END; +--> statement-breakpoint +UPDATE `driver_command` +SET `payload_json` = json_set( + `payload_json`, + '$.commandId', `id`, + '$.runId', (SELECT `effect`.`session_run_id` FROM `external_tool_effect` AS `effect` WHERE `effect`.`command_id` = `driver_command`.`id` LIMIT 1), + '$.serverId', (SELECT `effect`.`server_id` FROM `external_tool_effect` AS `effect` WHERE `effect`.`command_id` = `driver_command`.`id` LIMIT 1) +) +WHERE `kind` = 'mcp.execute' AND `id` NOT IN (SELECT `id` FROM `__durable_mcp_v3_oversized_terminal_command`); +--> statement-breakpoint +UPDATE `driver_command` +SET `payload_json` = json_object( + 'argumentsJson', '{"omitted":"Stored MCP arguments were omitted during the durable MCP v3 migration."}', + 'commandId', `id`, + 'kind', 'mcp.execute', + 'requestId', json_extract(`payload_json`, '$.requestId'), + 'runId', (SELECT `effect`.`session_run_id` FROM `external_tool_effect` AS `effect` WHERE `effect`.`command_id` = `driver_command`.`id` LIMIT 1), + 'serverId', json_extract(`payload_json`, '$.serverId'), + 'toolCallId', json_extract(`payload_json`, '$.toolCallId'), + 'toolName', json_extract(`payload_json`, '$.toolName') +) +WHERE `kind` = 'mcp.execute' AND `id` IN (SELECT `id` FROM `__durable_mcp_v3_oversized_terminal_command`); +--> statement-breakpoint +UPDATE `driver_command` +SET `payload_json` = CASE `kind` + WHEN 'input.start' THEN json_object( + 'commandId', `id`, + 'input', CASE WHEN json_type(`payload_json`, '$.input.attachmentIds') = 'array' + THEN json_object( + 'attachmentIds', json(json_extract(`payload_json`, '$.input.attachmentIds')), + 'text', 'Stored input text was omitted during the durable MCP v3 migration.' + ) + ELSE json_object('text', 'Stored input text was omitted during the durable MCP v3 migration.') + END, + 'kind', 'input.start', + 'requestId', json_extract(`payload_json`, '$.requestId'), + 'runId', json_extract(`payload_json`, '$.runId') + ) + WHEN 'turn.cancel' THEN json_object('commandId', `id`, 'kind', 'turn.cancel', 'reason', 'Stored control reason was omitted during the durable MCP v3 migration.') + WHEN 'session.stop' THEN json_object('commandId', `id`, 'kind', 'session.stop', 'reason', 'Stored control reason was omitted during the durable MCP v3 migration.') + WHEN 'permission.resolve' THEN json_object( + 'commandId', `id`, + 'decision', json_extract(`payload_json`, '$.decision'), + 'kind', 'permission.resolve', + 'requestId', json_extract(`payload_json`, '$.requestId') + ) + ELSE `payload_json` +END +WHERE `id` IN (SELECT `id` FROM `__durable_mcp_v3_oversized_terminal_command`) AND `kind` <> 'mcp.execute'; +--> statement-breakpoint +CREATE TABLE `__durable_mcp_v3_legacy_claim_token` ( + `attempt` integer NOT NULL, + `claim_token` text NOT NULL, + `effect_id` text NOT NULL, + PRIMARY KEY(`effect_id`, `attempt`) +); +--> statement-breakpoint +INSERT INTO `__durable_mcp_v3_legacy_claim_token` (`attempt`, `claim_token`, `effect_id`) +SELECT + `attempt`, + lower(hex(randomblob(4)) || '-' || hex(randomblob(2)) || '-4' || substr(hex(randomblob(2)), 2) || '-8' || substr(hex(randomblob(2)), 2) || '-' || hex(randomblob(6))), + `effect_id` +FROM `external_tool_effect_attempt`; +--> statement-breakpoint +CREATE TABLE `__new_external_tool_effect` ( + `attempt_count` integer DEFAULT 0 NOT NULL, + `claim_token` text, + `command_id` text CHECK ("command_id" = upper("command_id") AND length("command_id") = 26 AND substr("command_id", 1, 1) GLOB '[0-7]' AND "command_id" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*') NOT NULL, + `created_at` integer NOT NULL, + `driver_instance_id` text CHECK ("driver_instance_id" = upper("driver_instance_id") AND length("driver_instance_id") = 26 AND substr("driver_instance_id", 1, 1) GLOB '[0-7]' AND "driver_instance_id" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*') NOT NULL, + `id` text CHECK ("id" = upper("id") AND length("id") = 26 AND substr("id", 1, 1) GLOB '[0-7]' AND "id" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*') PRIMARY KEY NOT NULL, + `idempotency_key` text NOT NULL, + `provider_receipt_json` text, + `result_json` text, + `server_id` text CHECK ("server_id" = upper("server_id") AND length("server_id") = 26 AND substr("server_id", 1, 1) GLOB '[0-7]' AND "server_id" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*') NOT NULL, + `session_run_id` text CHECK ("session_run_id" = upper("session_run_id") AND length("session_run_id") = 26 AND substr("session_run_id", 1, 1) GLOB '[0-7]' AND "session_run_id" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*') NOT NULL, + `status` text NOT NULL, + `tool_name` text NOT NULL, + `updated_at` integer NOT NULL, + FOREIGN KEY (`command_id`) REFERENCES `driver_command`(`id`) ON UPDATE no action ON DELETE cascade, + FOREIGN KEY (`driver_instance_id`) REFERENCES `driver_instance`(`id`) ON UPDATE no action ON DELETE cascade, + FOREIGN KEY (`session_run_id`) REFERENCES `session_run`(`id`) ON UPDATE no action ON DELETE cascade, + CONSTRAINT "external_tool_effect_status_check" CHECK("__new_external_tool_effect"."status" IN ('intent', 'claimed', 'succeeded', 'unknown')), + CONSTRAINT "external_tool_effect_claim_token_uuid_check" CHECK("__new_external_tool_effect"."claim_token" IS NULL OR (length("__new_external_tool_effect"."claim_token") = 36 AND length(replace("__new_external_tool_effect"."claim_token", '-', '')) = 32 AND "__new_external_tool_effect"."claim_token" = lower("__new_external_tool_effect"."claim_token") AND substr("__new_external_tool_effect"."claim_token", 9, 1) = '-' AND substr("__new_external_tool_effect"."claim_token", 14, 1) = '-' AND substr("__new_external_tool_effect"."claim_token", 15, 1) = '4' AND substr("__new_external_tool_effect"."claim_token", 19, 1) = '-' AND substr("__new_external_tool_effect"."claim_token", 20, 1) GLOB '[89ab]' AND substr("__new_external_tool_effect"."claim_token", 24, 1) = '-' AND replace("__new_external_tool_effect"."claim_token", '-', '') NOT GLOB '*[^0-9a-f]*')) +); +--> statement-breakpoint +WITH `effect_source` AS ( + SELECT + `effect`.*, + `command`.`payload_json` AS `command_payload_json`, + max( + COALESCE(length(CAST(`effect`.`result_json` AS BLOB)), 0), + COALESCE((SELECT max(length(CAST(`attempt`.`result_json` AS BLOB))) FROM `external_tool_effect_attempt` AS `attempt` WHERE `attempt`.`effect_id` = `effect`.`id` AND `attempt`.`status` = 'succeeded'), 0), + COALESCE(length(CAST(`command`.`result_json` AS BLOB)), 0) + ) AS `original_result_bytes` + FROM `external_tool_effect` AS `effect` + INNER JOIN `driver_command` AS `command` ON `command`.`id` = `effect`.`command_id` +), +`effect_result_normalized` AS ( + SELECT + `effect_source`.*, + CASE + WHEN `status` = 'succeeded' AND ( + `original_result_bytes` > 1044480 + OR length(CAST('{"kind":"succeeded","result":' || `result_json` || '}' AS BLOB)) > 1044480 + ) THEN + '{"isError":true,"outputText":' || json_quote('Stored MCP result omitted because it contained ' || `original_result_bytes` || ' UTF-8 bytes.') || + ',"requestId":' || json_quote(json_extract(`command_payload_json`, '$.requestId')) || + ',"serverId":' || json_quote(json_extract(`command_payload_json`, '$.serverId')) || + ',"toolName":' || json_quote(json_extract(`command_payload_json`, '$.toolName')) || '}' + WHEN `status` = 'succeeded' THEN `result_json` + ELSE NULL + END AS `normalized_result_json` + FROM `effect_source` +), +`effect_normalized` AS ( + SELECT + `effect_result_normalized`.*, + CASE + WHEN `status` <> 'succeeded' THEN NULL + WHEN `provider_receipt_json` IS NULL THEN NULL + WHEN length(CAST( + '{"kind":"succeeded","providerReceiptJson":' || json_quote(`provider_receipt_json`) || + ',"result":' || `normalized_result_json` || '}' + AS BLOB)) > 1044480 THEN NULL + ELSE `provider_receipt_json` + END AS `normalized_provider_receipt_json` + FROM `effect_result_normalized` +) +INSERT INTO `__new_external_tool_effect`("attempt_count", "claim_token", "command_id", "created_at", "driver_instance_id", "id", "idempotency_key", "provider_receipt_json", "result_json", "server_id", "session_run_id", "status", "tool_name", "updated_at") +SELECT + `attempt_count`, + (SELECT `token`.`claim_token` FROM `__durable_mcp_v3_legacy_claim_token` AS `token` WHERE `token`.`effect_id` = `effect_normalized`.`id` AND `token`.`attempt` = `effect_normalized`.`attempt_count` LIMIT 1), + `command_id`, + `created_at`, + `driver_instance_id`, + `id`, + `idempotency_key`, + `normalized_provider_receipt_json`, + `normalized_result_json`, + `server_id`, + `session_run_id`, + CASE WHEN `status` = 'executing' THEN 'unknown' ELSE `status` END, + json_extract(`command_payload_json`, '$.toolName'), + CASE WHEN `status` = 'executing' THEN max(`created_at`, `updated_at`, unixepoch('now') * 1000) ELSE `updated_at` END +FROM `effect_normalized`;--> statement-breakpoint +CREATE TABLE `__new_external_tool_effect_attempt` ( + `attempt` integer NOT NULL, + `claim_token` text NOT NULL, + `completed_at` integer, + `created_at` integer NOT NULL, + `effect_id` text CHECK ("effect_id" = upper("effect_id") AND length("effect_id") = 26 AND substr("effect_id", 1, 1) GLOB '[0-7]' AND "effect_id" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*') NOT NULL, + `provider_receipt_json` text, + `result_json` text, + `status` text NOT NULL, + PRIMARY KEY(`effect_id`, `attempt`), + FOREIGN KEY (`effect_id`) REFERENCES `__new_external_tool_effect`(`id`) ON UPDATE no action ON DELETE cascade, + CONSTRAINT "external_tool_effect_attempt_status_check" CHECK("__new_external_tool_effect_attempt"."status" IN ('claimed', 'succeeded', 'unknown')), + CONSTRAINT "external_tool_effect_attempt_claim_token_uuid_check" CHECK(length("__new_external_tool_effect_attempt"."claim_token") = 36 AND length(replace("__new_external_tool_effect_attempt"."claim_token", '-', '')) = 32 AND "__new_external_tool_effect_attempt"."claim_token" = lower("__new_external_tool_effect_attempt"."claim_token") AND substr("__new_external_tool_effect_attempt"."claim_token", 9, 1) = '-' AND substr("__new_external_tool_effect_attempt"."claim_token", 14, 1) = '-' AND substr("__new_external_tool_effect_attempt"."claim_token", 15, 1) = '4' AND substr("__new_external_tool_effect_attempt"."claim_token", 19, 1) = '-' AND substr("__new_external_tool_effect_attempt"."claim_token", 20, 1) GLOB '[89ab]' AND substr("__new_external_tool_effect_attempt"."claim_token", 24, 1) = '-' AND replace("__new_external_tool_effect_attempt"."claim_token", '-', '') NOT GLOB '*[^0-9a-f]*') +); +--> statement-breakpoint +INSERT INTO `__new_external_tool_effect_attempt`("attempt", "claim_token", "completed_at", "created_at", "effect_id", "provider_receipt_json", "result_json", "status") +SELECT + `attempt`, + (SELECT `token`.`claim_token` FROM `__durable_mcp_v3_legacy_claim_token` AS `token` WHERE `token`.`effect_id` = `external_tool_effect_attempt`.`effect_id` AND `token`.`attempt` = `external_tool_effect_attempt`.`attempt`), + CASE WHEN `status` = 'executing' THEN max( + COALESCE(`completed_at`, 0), + `created_at`, + (SELECT `effect`.`updated_at` FROM `__new_external_tool_effect` AS `effect` WHERE `effect`.`id` = `external_tool_effect_attempt`.`effect_id`) + ) ELSE `completed_at` END, + `created_at`, + `effect_id`, + CASE WHEN `status` = 'succeeded' THEN (SELECT `effect`.`provider_receipt_json` FROM `__new_external_tool_effect` AS `effect` WHERE `effect`.`id` = `external_tool_effect_attempt`.`effect_id`) ELSE NULL END, + CASE WHEN `status` = 'succeeded' THEN (SELECT `effect`.`result_json` FROM `__new_external_tool_effect` AS `effect` WHERE `effect`.`id` = `external_tool_effect_attempt`.`effect_id`) ELSE NULL END, + CASE WHEN `status` = 'executing' THEN 'unknown' ELSE `status` END +FROM `external_tool_effect_attempt`;--> statement-breakpoint +DROP TABLE `external_tool_effect_attempt`;--> statement-breakpoint +DROP TABLE `external_tool_effect`;--> statement-breakpoint +ALTER TABLE `__new_external_tool_effect` RENAME TO `external_tool_effect`;--> statement-breakpoint +ALTER TABLE `__new_external_tool_effect_attempt` RENAME TO `external_tool_effect_attempt`;--> statement-breakpoint +UPDATE `driver_command` +SET + `completed_at` = COALESCE(`completed_at`, (SELECT `effect`.`updated_at` FROM `external_tool_effect` AS `effect` WHERE `effect`.`command_id` = `driver_command`.`id` AND `effect`.`status` = 'succeeded' LIMIT 1)), + `error_json` = NULL, + `result_json` = (SELECT `effect`.`result_json` FROM `external_tool_effect` AS `effect` WHERE `effect`.`command_id` = `driver_command`.`id` AND `effect`.`status` = 'succeeded' LIMIT 1), + `status` = 'completed' +WHERE EXISTS (SELECT 1 FROM `external_tool_effect` AS `effect` WHERE `effect`.`command_id` = `driver_command`.`id` AND `effect`.`status` = 'succeeded'); +--> statement-breakpoint +UPDATE `driver_command` +SET `result_json` = json_object('requestId', json_extract(`payload_json`, '$.requestId')) +WHERE `kind` = 'input.start' + AND `result_json` IS NOT NULL + AND (`id` IN (SELECT `id` FROM `__durable_mcp_v3_oversized_terminal_command`) OR length(CAST(`result_json` AS BLOB)) > 1044480); +--> statement-breakpoint +UPDATE `driver_command` +SET `error_json` = json_object( + 'code', 'storage.payload_omitted', + 'details', json_object('originalBytes', length(CAST(`error_json` AS BLOB))), + 'message', 'Stored runtime command error exceeded the durable payload limit and was omitted.', + 'retryable', json('false') +) +WHERE `error_json` IS NOT NULL AND length(CAST(`error_json` AS BLOB)) > 1044480; +--> statement-breakpoint +WITH `oversized_session_run_error` AS ( + SELECT + `id`, + length(CAST( + '{"code":' || json_quote(`error_code`) || + ',"details":' || COALESCE(NULLIF(`error_details_json`, ''), '{}') || + ',"message":' || json_quote(`error_message`) || + ',"retryable":false}' + AS BLOB)) AS `original_bytes` + FROM `session_run` + WHERE `error_code` IS NOT NULL AND `error_message` IS NOT NULL +) +UPDATE `session_run` +SET + `error_code` = 'storage.payload_omitted', + `error_details_json` = json_object('originalBytes', (SELECT `original_bytes` FROM `oversized_session_run_error` WHERE `oversized_session_run_error`.`id` = `session_run`.`id`)), + `error_message` = 'Stored Session Run error exceeded the durable payload limit and was omitted.' +WHERE `id` IN (SELECT `id` FROM `oversized_session_run_error` WHERE `original_bytes` > 1044480); +--> statement-breakpoint +DROP TABLE `__durable_mcp_v3_oversized_terminal_command`; +--> statement-breakpoint +DROP TABLE `__durable_mcp_v3_legacy_claim_token`; +--> statement-breakpoint +CREATE UNIQUE INDEX `external_tool_effect_command_idx` ON `external_tool_effect` (`command_id`);--> statement-breakpoint +CREATE UNIQUE INDEX `external_tool_effect_idempotency_key_idx` ON `external_tool_effect` (`idempotency_key`);--> statement-breakpoint +CREATE INDEX `external_tool_effect_run_status_idx` ON `external_tool_effect` (`session_run_id`,`status`,`id`);--> statement-breakpoint +CREATE INDEX `external_tool_effect_driver_status_idx` ON `external_tool_effect` (`driver_instance_id`,`status`);--> statement-breakpoint +CREATE INDEX `external_tool_effect_attempt_status_idx` ON `external_tool_effect_attempt` (`status`,`created_at`); +--> statement-breakpoint +CREATE TABLE `__durable_mcp_v3_final_guard` ( + `violation_count` integer NOT NULL, + CONSTRAINT "durable_mcp_v3_final_guard_check" CHECK(`violation_count` = 0) +); +--> statement-breakpoint +INSERT INTO `__durable_mcp_v3_final_guard` (`violation_count`) +SELECT COUNT(*) FROM `driver_command` WHERE `status` IN ('queued', 'delivered', 'accepted'); +--> statement-breakpoint +INSERT INTO `__durable_mcp_v3_final_guard` (`violation_count`) +SELECT COUNT(*) +FROM `driver_command` +WHERE `status` NOT IN ('completed', 'failed', 'expired', 'cancelled') + OR (`driver_generation` IS NOT NULL AND (typeof(`driver_generation`) <> 'integer' OR `driver_generation` NOT BETWEEN 0 AND 9007199254740991)) + OR NOT json_valid(`payload_json`) + OR length(CAST(`payload_json` AS BLOB)) > 824448; +--> statement-breakpoint +INSERT INTO `__durable_mcp_v3_final_guard` (`violation_count`) +SELECT COUNT(*) +FROM `driver_command` AS `command` +WHERE json_type(`command`.`payload_json`) IS NOT 'object' + OR json_type(`command`.`payload_json`, '$.commandId') IS NOT 'text' + OR length(json_extract(`command`.`payload_json`, '$.commandId')) = 0 + OR json_extract(`command`.`payload_json`, '$.commandId') IS NOT `command`.`id` + OR json_type(`command`.`payload_json`, '$.kind') IS NOT 'text' + OR json_extract(`command`.`payload_json`, '$.kind') IS NOT `command`.`kind` + OR `command`.`kind` NOT IN ('turn.cancel', 'input.start', 'mcp.execute', 'session.stop', 'permission.resolve') + OR (`command`.`kind` = 'turn.cancel' AND EXISTS (SELECT 1 FROM json_each(`command`.`payload_json`) WHERE `key` NOT IN ('commandId', 'kind', 'reason'))) + OR (`command`.`kind` = 'turn.cancel' AND json_type(`command`.`payload_json`, '$.reason') IS NOT NULL AND json_type(`command`.`payload_json`, '$.reason') IS NOT 'text') + OR (`command`.`kind` = 'input.start' AND ( + json_type(`command`.`payload_json`, '$.requestId') IS NOT 'text' + OR length(json_extract(`command`.`payload_json`, '$.requestId')) = 0 + OR json_type(`command`.`payload_json`, '$.runId') IS NOT 'text' + OR length(json_extract(`command`.`payload_json`, '$.runId')) = 0 + OR json_type(`command`.`payload_json`, '$.input') IS NOT 'object' + OR json_type(`command`.`payload_json`, '$.input.text') IS NOT 'text' + OR length(json_extract(`command`.`payload_json`, '$.input.text')) = 0 + OR (json_type(`command`.`payload_json`, '$.input.attachmentIds') IS NOT NULL AND json_type(`command`.`payload_json`, '$.input.attachmentIds') IS NOT 'array') + OR EXISTS (SELECT 1 FROM json_each(`command`.`payload_json`, '$.input.attachmentIds') WHERE `type` <> 'text' OR length(`value`) = 0) + OR EXISTS (SELECT 1 FROM json_each(`command`.`payload_json`) WHERE `key` NOT IN ('commandId', 'input', 'kind', 'requestId', 'runId')) + OR EXISTS (SELECT 1 FROM json_each(`command`.`payload_json`, '$.input') WHERE `key` NOT IN ('attachmentIds', 'text')) + )) + OR (`command`.`kind` = 'mcp.execute' AND ( + json_type(`command`.`payload_json`, '$.argumentsJson') IS NOT 'text' + OR json_type(`command`.`payload_json`, '$.requestId') IS NOT 'text' + OR length(json_extract(`command`.`payload_json`, '$.requestId')) = 0 + OR json_type(`command`.`payload_json`, '$.runId') IS NOT 'text' + OR length(json_extract(`command`.`payload_json`, '$.runId')) = 0 + OR json_type(`command`.`payload_json`, '$.serverId') IS NOT 'text' + OR length(json_extract(`command`.`payload_json`, '$.serverId')) = 0 + OR json_type(`command`.`payload_json`, '$.toolCallId') IS NOT 'text' + OR length(json_extract(`command`.`payload_json`, '$.toolCallId')) = 0 + OR json_type(`command`.`payload_json`, '$.toolName') IS NOT 'text' + OR length(json_extract(`command`.`payload_json`, '$.toolName')) = 0 + OR EXISTS (SELECT 1 FROM json_each(`command`.`payload_json`) WHERE `key` NOT IN ('argumentsJson', 'commandId', 'kind', 'requestId', 'runId', 'serverId', 'toolCallId', 'toolName')) + )) + OR (`command`.`kind` = 'session.stop' AND ( + json_type(`command`.`payload_json`, '$.reason') IS NOT 'text' + OR length(json_extract(`command`.`payload_json`, '$.reason')) = 0 + OR EXISTS (SELECT 1 FROM json_each(`command`.`payload_json`) WHERE `key` NOT IN ('commandId', 'kind', 'reason')) + )) + OR (`command`.`kind` = 'permission.resolve' AND ( + json_type(`command`.`payload_json`, '$.requestId') IS NOT 'text' + OR length(json_extract(`command`.`payload_json`, '$.requestId')) = 0 + OR json_type(`command`.`payload_json`, '$.decision') IS NOT 'text' + OR json_extract(`command`.`payload_json`, '$.decision') NOT IN ('allow_once', 'reject_once') + OR EXISTS (SELECT 1 FROM json_each(`command`.`payload_json`) WHERE `key` NOT IN ('commandId', 'decision', 'kind', 'requestId')) + )); +--> statement-breakpoint +INSERT INTO `__durable_mcp_v3_final_guard` (`violation_count`) +SELECT COUNT(*) +FROM `driver_command` +WHERE (`result_json` IS NOT NULL AND (NOT json_valid(`result_json`) OR json_type(`result_json`) IS NOT 'object' OR length(CAST(`result_json` AS BLOB)) > 1044480)) + OR (`error_json` IS NOT NULL AND (NOT json_valid(`error_json`) OR json_type(`error_json`) IS NOT 'object' OR length(CAST(`error_json` AS BLOB)) > 1044480)) + OR (`result_json` IS NOT NULL AND `error_json` IS NOT NULL) + OR (`status` = 'completed' AND `error_json` IS NOT NULL) + OR (`status` IN ('failed', 'expired', 'cancelled') AND `result_json` IS NOT NULL); +--> statement-breakpoint +INSERT INTO `__durable_mcp_v3_final_guard` (`violation_count`) +SELECT COUNT(*) +FROM `driver_command` AS `command` +WHERE (`command`.`kind` IN ('turn.cancel', 'session.stop', 'permission.resolve') AND `command`.`result_json` IS NOT NULL) + OR (`command`.`kind` = 'input.start' AND `command`.`result_json` IS NOT NULL AND ( + json_type(`command`.`result_json`, '$.requestId') IS NOT 'text' + OR length(json_extract(`command`.`result_json`, '$.requestId')) = 0 + OR EXISTS (SELECT 1 FROM json_each(`command`.`result_json`) WHERE `key` <> 'requestId') + )) + OR (`command`.`kind` = 'mcp.execute' AND `command`.`result_json` IS NOT NULL AND ( + json_type(`command`.`result_json`, '$.outputText') IS NOT 'text' + OR json_type(`command`.`result_json`, '$.requestId') IS NOT 'text' + OR length(json_extract(`command`.`result_json`, '$.requestId')) = 0 + OR json_type(`command`.`result_json`, '$.serverId') IS NOT 'text' + OR length(json_extract(`command`.`result_json`, '$.serverId')) = 0 + OR json_type(`command`.`result_json`, '$.toolName') IS NOT 'text' + OR length(json_extract(`command`.`result_json`, '$.toolName')) = 0 + OR (json_type(`command`.`result_json`, '$.isError') IS NOT NULL AND json_type(`command`.`result_json`, '$.isError') NOT IN ('true', 'false')) + OR EXISTS (SELECT 1 FROM json_each(`command`.`result_json`) WHERE `key` NOT IN ('isError', 'outputText', 'requestId', 'serverId', 'toolName')) + )); +--> statement-breakpoint +INSERT INTO `__durable_mcp_v3_final_guard` (`violation_count`) +SELECT COUNT(*) +FROM `driver_command` +WHERE `error_json` IS NOT NULL + AND ( + json_type(`error_json`, '$.code') IS NOT 'text' + OR length(json_extract(`error_json`, '$.code')) = 0 + OR json_type(`error_json`, '$.message') IS NOT 'text' + OR length(json_extract(`error_json`, '$.message')) = 0 + OR (json_type(`error_json`, '$.retryable') IS NOT 'true' AND json_type(`error_json`, '$.retryable') IS NOT 'false') + OR json_type(`error_json`, '$.details') IS NOT 'object' + OR EXISTS (SELECT 1 FROM json_each(`error_json`, '$.details') WHERE `type` NOT IN ('null', 'integer', 'real', 'text', 'true', 'false')) + OR EXISTS (SELECT 1 FROM json_each(`error_json`) WHERE `key` NOT IN ('code', 'details', 'message', 'retryable')) + ); +--> statement-breakpoint +INSERT INTO `__durable_mcp_v3_final_guard` (`violation_count`) +SELECT + (SELECT COUNT(*) FROM `external_tool_effect` WHERE `result_json` IS NOT NULL AND (NOT json_valid(`result_json`) OR json_type(`result_json`) IS NOT 'object' OR length(CAST(`result_json` AS BLOB)) > 1044480)) + + (SELECT COUNT(*) FROM `external_tool_effect_attempt` WHERE `result_json` IS NOT NULL AND (NOT json_valid(`result_json`) OR json_type(`result_json`) IS NOT 'object' OR length(CAST(`result_json` AS BLOB)) > 1044480)); +--> statement-breakpoint +INSERT INTO `__durable_mcp_v3_final_guard` (`violation_count`) +SELECT + (SELECT COUNT(*) FROM `external_tool_effect` WHERE `result_json` IS NOT NULL AND ( + json_type(`result_json`, '$.outputText') IS NOT 'text' + OR json_type(`result_json`, '$.requestId') IS NOT 'text' + OR length(json_extract(`result_json`, '$.requestId')) = 0 + OR json_type(`result_json`, '$.serverId') IS NOT 'text' + OR length(json_extract(`result_json`, '$.serverId')) = 0 + OR json_type(`result_json`, '$.toolName') IS NOT 'text' + OR length(json_extract(`result_json`, '$.toolName')) = 0 + OR (json_type(`result_json`, '$.isError') IS NOT NULL AND json_type(`result_json`, '$.isError') NOT IN ('true', 'false')) + OR EXISTS (SELECT 1 FROM json_each(`result_json`) WHERE `key` NOT IN ('isError', 'outputText', 'requestId', 'serverId', 'toolName')) + )) + + (SELECT COUNT(*) FROM `external_tool_effect_attempt` WHERE `result_json` IS NOT NULL AND ( + json_type(`result_json`, '$.outputText') IS NOT 'text' + OR json_type(`result_json`, '$.requestId') IS NOT 'text' + OR length(json_extract(`result_json`, '$.requestId')) = 0 + OR json_type(`result_json`, '$.serverId') IS NOT 'text' + OR length(json_extract(`result_json`, '$.serverId')) = 0 + OR json_type(`result_json`, '$.toolName') IS NOT 'text' + OR length(json_extract(`result_json`, '$.toolName')) = 0 + OR (json_type(`result_json`, '$.isError') IS NOT NULL AND json_type(`result_json`, '$.isError') NOT IN ('true', 'false')) + OR EXISTS (SELECT 1 FROM json_each(`result_json`) WHERE `key` NOT IN ('isError', 'outputText', 'requestId', 'serverId', 'toolName')) + )); +--> statement-breakpoint +INSERT INTO `__durable_mcp_v3_final_guard` (`violation_count`) +SELECT COUNT(*) +FROM `driver_command` AS `command` +LEFT JOIN `external_tool_effect` AS `effect` ON `effect`.`command_id` = `command`.`id` +WHERE `command`.`kind` = 'mcp.execute' + AND ( + `effect`.`id` IS NULL + OR `effect`.`driver_instance_id` IS NOT `command`.`driver_instance_id` + OR json_extract(`command`.`payload_json`, '$.runId') IS NOT `effect`.`session_run_id` + OR json_extract(`command`.`payload_json`, '$.serverId') IS NOT `effect`.`server_id` + OR json_extract(`command`.`payload_json`, '$.toolName') IS NOT `effect`.`tool_name` + ); +--> statement-breakpoint +INSERT INTO `__durable_mcp_v3_final_guard` (`violation_count`) +SELECT COUNT(*) +FROM `external_tool_effect` AS `effect` +INNER JOIN `driver_command` AS `command` ON `command`.`id` = `effect`.`command_id` +WHERE `command`.`kind` <> 'mcp.execute' + OR length(`effect`.`idempotency_key`) = 0 + OR length(`effect`.`tool_name`) = 0 + OR `effect`.`attempt_count` IS NOT COALESCE((SELECT max(`attempt`) FROM `external_tool_effect_attempt` AS `attempt` WHERE `attempt`.`effect_id` = `effect`.`id`), 0) + OR EXISTS (SELECT 1 FROM `external_tool_effect_attempt` AS `attempt` WHERE `attempt`.`effect_id` = `effect`.`id` AND `attempt`.`attempt` < 1) + OR (`effect`.`status` = 'intent' AND ( + `effect`.`attempt_count` <> 0 + OR `effect`.`claim_token` IS NOT NULL + OR `effect`.`provider_receipt_json` IS NOT NULL + OR `effect`.`result_json` IS NOT NULL + OR EXISTS (SELECT 1 FROM `external_tool_effect_attempt` AS `attempt` WHERE `attempt`.`effect_id` = `effect`.`id`) + )) + OR (`effect`.`status` <> 'intent' AND ( + `effect`.`attempt_count` < 1 + OR `effect`.`claim_token` IS NULL + OR NOT EXISTS ( + SELECT 1 + FROM `external_tool_effect_attempt` AS `attempt` + WHERE `attempt`.`effect_id` = `effect`.`id` + AND `attempt`.`attempt` = `effect`.`attempt_count` + AND `attempt`.`claim_token` = `effect`.`claim_token` + ) + )) + OR EXISTS ( + SELECT 1 + FROM `external_tool_effect_attempt` AS `attempt` + WHERE `attempt`.`effect_id` = `effect`.`id` + AND `attempt`.`attempt` < `effect`.`attempt_count` + AND (`attempt`.`status` <> 'unknown' OR `attempt`.`completed_at` IS NULL OR `attempt`.`provider_receipt_json` IS NOT NULL OR `attempt`.`result_json` IS NOT NULL) + ) + OR (`effect`.`status` = 'claimed' AND ( + `effect`.`provider_receipt_json` IS NOT NULL + OR `effect`.`result_json` IS NOT NULL + OR NOT EXISTS ( + SELECT 1 FROM `external_tool_effect_attempt` AS `attempt` + WHERE `attempt`.`effect_id` = `effect`.`id` + AND `attempt`.`attempt` = `effect`.`attempt_count` + AND `attempt`.`status` = 'claimed' + AND `attempt`.`completed_at` IS NULL + AND `attempt`.`provider_receipt_json` IS NULL + AND `attempt`.`result_json` IS NULL + ) + )) + OR (`effect`.`status` = 'unknown' AND ( + `effect`.`provider_receipt_json` IS NOT NULL + OR `effect`.`result_json` IS NOT NULL + OR `command`.`status` = 'completed' + OR `command`.`result_json` IS NOT NULL + OR NOT EXISTS ( + SELECT 1 FROM `external_tool_effect_attempt` AS `attempt` + WHERE `attempt`.`effect_id` = `effect`.`id` + AND `attempt`.`attempt` = `effect`.`attempt_count` + AND `attempt`.`status` = 'unknown' + AND `attempt`.`completed_at` IS NOT NULL + AND `attempt`.`provider_receipt_json` IS NULL + AND `attempt`.`result_json` IS NULL + ) + )) + OR (`effect`.`status` = 'succeeded' AND ( + `effect`.`result_json` IS NULL + OR `command`.`status` <> 'completed' + OR `command`.`error_json` IS NOT NULL + OR `command`.`result_json` IS NOT `effect`.`result_json` + OR (SELECT COUNT(*) FROM `external_tool_effect_attempt` AS `attempt` WHERE `attempt`.`effect_id` = `effect`.`id` AND `attempt`.`status` = 'succeeded') <> 1 + OR NOT EXISTS ( + SELECT 1 FROM `external_tool_effect_attempt` AS `attempt` + WHERE `attempt`.`effect_id` = `effect`.`id` + AND `attempt`.`attempt` = `effect`.`attempt_count` + AND `attempt`.`status` = 'succeeded' + AND `attempt`.`completed_at` IS NOT NULL + AND `attempt`.`provider_receipt_json` IS `effect`.`provider_receipt_json` + AND `attempt`.`result_json` IS `effect`.`result_json` + ) + OR json_extract(`effect`.`result_json`, '$.requestId') IS NOT json_extract(`command`.`payload_json`, '$.requestId') + OR json_extract(`effect`.`result_json`, '$.serverId') IS NOT json_extract(`command`.`payload_json`, '$.serverId') + OR json_extract(`effect`.`result_json`, '$.toolName') IS NOT json_extract(`command`.`payload_json`, '$.toolName') + OR length(CAST( + CASE WHEN `effect`.`provider_receipt_json` IS NULL + THEN '{"kind":"succeeded","result":' || `effect`.`result_json` || '}' + ELSE '{"kind":"succeeded","providerReceiptJson":' || json_quote(`effect`.`provider_receipt_json`) || ',"result":' || `effect`.`result_json` || '}' + END + AS BLOB)) > 1044480 + )) + OR (`effect`.`status` IN ('intent', 'claimed') AND (`command`.`status` = 'completed' OR `command`.`result_json` IS NOT NULL)); +--> statement-breakpoint +INSERT INTO `__durable_mcp_v3_final_guard` (`violation_count`) +SELECT COUNT(*) +FROM `external_tool_effect_attempt` +WHERE (`status` <> 'succeeded' AND (`provider_receipt_json` IS NOT NULL OR `result_json` IS NOT NULL)) + OR (`status` = 'claimed' AND `completed_at` IS NOT NULL) + OR (`status` IN ('succeeded', 'unknown') AND `completed_at` IS NULL) + OR (`status` = 'succeeded' AND `result_json` IS NULL); +--> statement-breakpoint +INSERT INTO `__durable_mcp_v3_final_guard` (`violation_count`) +SELECT COUNT(*) +FROM `session_run` +WHERE (`error_code` IS NULL AND (`error_message` IS NOT NULL OR `error_details_json` IS NOT NULL)) + OR (`error_message` IS NULL AND (`error_code` IS NOT NULL OR `error_details_json` IS NOT NULL)) + OR (`error_code` IS NOT NULL AND length(`error_code`) = 0) + OR (`error_message` IS NOT NULL AND length(`error_message`) = 0) + OR (`error_details_json` IS NOT NULL AND (NOT json_valid(`error_details_json`) OR json_type(`error_details_json`) IS NOT 'object')) + OR (`error_details_json` IS NOT NULL AND EXISTS (SELECT 1 FROM json_each(`error_details_json`) WHERE `type` NOT IN ('null', 'integer', 'real', 'text', 'true', 'false'))) + OR (`error_code` IS NOT NULL AND length(CAST( + '{"code":' || json_quote(`error_code`) || + ',"details":' || COALESCE(`error_details_json`, '{}') || + ',"message":' || json_quote(`error_message`) || + ',"retryable":false}' + AS BLOB)) > 1044480); +--> statement-breakpoint +INSERT INTO `__durable_mcp_v3_final_guard` (`violation_count`) +SELECT + (SELECT COUNT(*) FROM `external_tool_effect` WHERE `claim_token` IS NOT NULL AND NOT ( + length(`claim_token`) = 36 + AND length(replace(`claim_token`, '-', '')) = 32 + AND `claim_token` = lower(`claim_token`) + AND substr(`claim_token`, 9, 1) = '-' + AND substr(`claim_token`, 14, 1) = '-' + AND substr(`claim_token`, 15, 1) = '4' + AND substr(`claim_token`, 19, 1) = '-' + AND substr(`claim_token`, 20, 1) GLOB '[89ab]' + AND substr(`claim_token`, 24, 1) = '-' + AND replace(`claim_token`, '-', '') NOT GLOB '*[^0-9a-f]*' + )) + + (SELECT COUNT(*) FROM `external_tool_effect_attempt` WHERE NOT ( + length(`claim_token`) = 36 + AND length(replace(`claim_token`, '-', '')) = 32 + AND `claim_token` = lower(`claim_token`) + AND substr(`claim_token`, 9, 1) = '-' + AND substr(`claim_token`, 14, 1) = '-' + AND substr(`claim_token`, 15, 1) = '4' + AND substr(`claim_token`, 19, 1) = '-' + AND substr(`claim_token`, 20, 1) GLOB '[89ab]' + AND substr(`claim_token`, 24, 1) = '-' + AND replace(`claim_token`, '-', '') NOT GLOB '*[^0-9a-f]*' + )); +--> statement-breakpoint +DROP TABLE `__durable_mcp_v3_final_guard`; diff --git a/pkgs/db/drizzle/0014_session-event-stream-identity.sql b/pkgs/db/drizzle/0014_session-event-stream-identity.sql new file mode 100644 index 00000000..8a0684f4 --- /dev/null +++ b/pkgs/db/drizzle/0014_session-event-stream-identity.sql @@ -0,0 +1,755 @@ +CREATE TABLE IF NOT EXISTS "__production_deploy_lease" ( + "id" integer PRIMARY KEY CHECK ("id" = 1), + "owner" text NOT NULL +); +--> statement-breakpoint +CREATE TABLE IF NOT EXISTS "__protocol_v3_legacy_rewrite_authorization" ( + "id" integer PRIMARY KEY CHECK ("id" = 1), + "gate_id" integer NOT NULL CHECK ("gate_id" = 1) REFERENCES "__protocol_v3_cutover" ("id") ON DELETE CASCADE, + "bookmark" text NOT NULL CHECK (length(trim("bookmark")) > 0), + "candidate_count" integer NOT NULL CHECK ("candidate_count" >= 0), + "candidate_manifest_json" text NOT NULL CHECK (json_valid("candidate_manifest_json") AND json_type("candidate_manifest_json") = 'array'), + "deploy_owner" text NOT NULL, + "expires_at" integer NOT NULL, + "release_tree_oid" text NOT NULL CHECK ((length("release_tree_oid") = 40 OR length("release_tree_oid") = 64) AND "release_tree_oid" = lower("release_tree_oid") AND "release_tree_oid" NOT GLOB '*[^0-9a-f]*') +); +--> statement-breakpoint +CREATE TABLE `__session_event_v3_terminal_source_guard` ( + `violation_count` integer NOT NULL, + CONSTRAINT "session_event_v3_terminal_source_guard_check" CHECK(`violation_count` = 0) +); +--> statement-breakpoint +INSERT INTO `__session_event_v3_terminal_source_guard` (`violation_count`) +SELECT count(*) +FROM ( + SELECT 1 + FROM `session_event` + WHERE `event_type` IN ('run.cancelled', 'run.completed', 'run.failed') + AND `run_id` IS NOT NULL + GROUP BY `session_id`, `run_id` + HAVING count(*) > 1 +); +--> statement-breakpoint +INSERT INTO `__session_event_v3_terminal_source_guard` (`violation_count`) +SELECT count(*) +FROM `session_event` AS `event` +LEFT JOIN `session_run` AS `run` ON `run`.`id` = `event`.`run_id` +WHERE `event`.`event_type` IN ('run.cancelled', 'run.completed', 'run.failed') + AND ( + `event`.`run_id` IS NULL + OR `run`.`id` IS NULL + OR `run`.`session_id` <> `event`.`session_id` + OR NOT ( + (`run`.`status` = 'completed' AND `event`.`event_type` = 'run.completed') + OR (`run`.`status` = 'failed' AND `event`.`event_type` = 'run.failed') + OR (`run`.`status` IN ('cancelled', 'expired') AND `event`.`event_type` = 'run.cancelled') + ) + ); +--> statement-breakpoint +INSERT INTO `__session_event_v3_terminal_source_guard` (`violation_count`) +SELECT count(*) +FROM `session_event` AS `terminal` +WHERE `terminal`.`event_type` IN ('run.cancelled', 'run.completed', 'run.failed') + AND `terminal`.`source_event_id` <> + 'session-run-terminal:' || `terminal`.`run_id` || ':' || `terminal`.`event_type` + AND EXISTS ( + SELECT 1 + FROM `session_event` AS `other` + WHERE `other`.`session_id` = `terminal`.`session_id` + AND `other`.`source_event_id` = + 'session-run-terminal:' || `terminal`.`run_id` || ':' || `terminal`.`event_type` + AND `other`.`id` <> `terminal`.`id` + ); +--> statement-breakpoint +INSERT INTO `__session_event_v3_terminal_source_guard` (`violation_count`) +SELECT count(*) +FROM `session_run` AS `run` +WHERE `run`.`status` IN ('cancelled', 'completed', 'expired', 'failed') + AND NOT EXISTS ( + SELECT 1 + FROM `session_event` AS `event` + WHERE `event`.`session_id` = `run`.`session_id` + AND `event`.`run_id` = `run`.`id` + AND `event`.`event_type` IN ('run.cancelled', 'run.completed', 'run.failed') + ); +--> statement-breakpoint +INSERT INTO `__session_event_v3_terminal_source_guard` (`violation_count`) +SELECT count(*) +FROM `session_event` AS `event` +INNER JOIN `session_run` AS `run` + ON `run`.`id` = `event`.`run_id` + AND `run`.`session_id` = `event`.`session_id` +LEFT JOIN `session` ON `session`.`id` = `run`.`session_id` +WHERE `event`.`event_type` IN ('run.cancelled', 'run.completed', 'run.failed') + AND ( + `session`.`id` IS NULL + OR `run`.`completed_at` IS NULL + OR `run`.`status_event` <> CASE `run`.`status` + WHEN 'completed' THEN 'run.complete' + WHEN 'failed' THEN 'run.fail' + WHEN 'cancelled' THEN 'run.cancel' + WHEN 'expired' THEN 'run.expire' + END + OR `event`.`seq` > `session`.`runtime_event_seq_cursor` + OR ( + `session`.`last_run_id` = `run`.`id` + AND ( + `session`.`status` NOT IN ('IDLE', 'TERMINATED') + OR `session`.`status_operation_id` IS NOT NULL + ) + ) + OR EXISTS ( + SELECT 1 + FROM `session_permission_request` AS `permission` + WHERE `permission`.`session_id` = `run`.`session_id` + AND `permission`.`run_id` = `run`.`id` + ) + ); +--> statement-breakpoint +INSERT INTO `__session_event_v3_terminal_source_guard` (`violation_count`) +SELECT count(*) +FROM ( + SELECT `message`.`session_id`, `message`.`session_run_id` + FROM `session_message` AS `message` + INNER JOIN `session_run` AS `run` + ON `run`.`id` = `message`.`session_run_id` + AND `run`.`session_id` = `message`.`session_id` + WHERE `message`.`role` = 'assistant' + AND `message`.`session_run_id` IS NOT NULL + AND `run`.`status` = 'completed' + GROUP BY `message`.`session_id`, `message`.`session_run_id` + HAVING count(*) > 1 +); +--> statement-breakpoint +INSERT INTO `__session_event_v3_terminal_source_guard` (`violation_count`) +SELECT count(*) +FROM `session_message` AS `message` +LEFT JOIN `session_run` AS `run` ON `run`.`id` = `message`.`session_run_id` +LEFT JOIN `session` ON `session`.`id` = `message`.`session_id` +WHERE `message`.`role` = 'assistant' + AND `message`.`session_run_id` IS NOT NULL + AND ( + `run`.`id` IS NULL + OR `run`.`session_id` <> `message`.`session_id` + OR `session`.`id` IS NULL + OR `message`.`seq` > `session`.`message_seq_cursor` + OR `run`.`status` IN ('cancelled', 'expired', 'failed') + OR CASE + WHEN `message`.`plan_json` IS NULL OR `message`.`plan_json` = '' THEN 0 + WHEN json_valid(`message`.`plan_json`) = 0 THEN 1 + WHEN json_type(`message`.`plan_json`) <> 'array' THEN 1 + ELSE 0 + END = 1 + OR CASE + WHEN `message`.`segments_json` IS NULL OR `message`.`segments_json` = '' THEN 0 + WHEN json_valid(`message`.`segments_json`) = 0 THEN 1 + WHEN json_type(`message`.`segments_json`) <> 'array' THEN 1 + ELSE 0 + END = 1 + ); +--> statement-breakpoint +INSERT INTO `__session_event_v3_terminal_source_guard` (`violation_count`) +SELECT count(*) +FROM `session_run` +WHERE `status` = 'failed' + AND ( + `error_code` IS NULL + OR trim(`error_code`) = '' + OR `error_message` IS NULL + OR trim(`error_message`) = '' + OR CASE + WHEN `error_details_json` IS NULL THEN 0 + WHEN json_valid(`error_details_json`) = 0 THEN 1 + WHEN json_type(`error_details_json`) <> 'object' THEN 1 + ELSE 0 + END = 1 + ); +--> statement-breakpoint +INSERT INTO `__session_event_v3_terminal_source_guard` (`violation_count`) +SELECT count(*) +FROM `session_run` +WHERE `status` IN ('cancelled', 'completed', 'expired') + AND ( + `error_code` IS NOT NULL + OR `error_details_json` IS NOT NULL + OR `error_message` IS NOT NULL + ); +--> statement-breakpoint +INSERT INTO `__session_event_v3_terminal_source_guard` (`violation_count`) +SELECT count(*) FROM `session_run` +WHERE `status` IN ('queued', 'booting', 'running', 'waiting_input'); +--> statement-breakpoint +INSERT INTO `__session_event_v3_terminal_source_guard` (`violation_count`) +SELECT count(*) FROM `driver_instance` +WHERE `status` IN ('provisioning', 'connecting', 'ready', 'stopping'); +--> statement-breakpoint +INSERT INTO `__session_event_v3_terminal_source_guard` (`violation_count`) +SELECT count(*) FROM `driver_command` +WHERE `status` IN ('queued', 'delivered', 'accepted'); +--> statement-breakpoint +INSERT INTO `__session_event_v3_terminal_source_guard` (`violation_count`) +SELECT count(*) FROM `external_tool_effect` +WHERE `status` IN ('executing', 'claimed'); +--> statement-breakpoint +INSERT INTO `__session_event_v3_terminal_source_guard` (`violation_count`) +SELECT count(*) FROM `api_command` +WHERE `status` IN ('queued', 'running'); +--> statement-breakpoint +INSERT INTO `__session_event_v3_terminal_source_guard` (`violation_count`) +SELECT count(*) FROM `sandbox` +WHERE `status` <> 'cold' + OR `status_operation_id` IS NOT NULL + OR `claim_owner` IS NOT NULL + OR `claim_expires_at` IS NOT NULL; +--> statement-breakpoint +INSERT INTO `__session_event_v3_terminal_source_guard` (`violation_count`) +SELECT count(*) FROM `sandbox_session` +WHERE `status` NOT IN ('closed', 'error'); +--> statement-breakpoint +INSERT INTO `__session_event_v3_terminal_source_guard` (`violation_count`) +SELECT count(*) FROM `sandbox_backup` +WHERE `status` NOT IN ('ready', 'pruned') OR `error_message` IS NOT NULL; +--> statement-breakpoint +INSERT INTO `__session_event_v3_terminal_source_guard` (`violation_count`) +SELECT count(*) FROM `session` +WHERE `status` NOT IN ('IDLE', 'TERMINATED') OR `status_operation_id` IS NOT NULL; +--> statement-breakpoint +INSERT INTO `__session_event_v3_terminal_source_guard` (`violation_count`) +SELECT CASE + WHEN count(*) = 1 AND coalesce(sum( + `name` = 'session_event_tool_identity_consistency' COLLATE BINARY + AND `tbl_name` = 'session_event' COLLATE BINARY + AND `sql` = 'CREATE TRIGGER `session_event_tool_identity_consistency` +BEFORE INSERT ON `session_event` +WHEN NEW.`tool_call_id` IS NOT NULL AND EXISTS ( + SELECT 1 + FROM `session_event` AS existing + WHERE existing.`session_id` = NEW.`session_id` + AND existing.`tool_call_id` = NEW.`tool_call_id` + AND ( + ( + NEW.`tool_name` IS NOT NULL + AND existing.`tool_name` IS NOT NULL + AND NEW.`tool_name` <> existing.`tool_name` + ) + OR ( + NEW.`tool_input_json` IS NOT NULL + AND existing.`tool_input_json` IS NOT NULL + AND NEW.`tool_input_json` <> existing.`tool_input_json` + ) + ) +) +BEGIN + SELECT RAISE(ABORT, ''session_event tool identity conflict''); +END' COLLATE BINARY + ), 0) = 1 THEN 0 + ELSE 1 +END +FROM `sqlite_master` +WHERE `type` = 'trigger' AND `tbl_name` COLLATE NOCASE = 'session_event'; +--> statement-breakpoint +INSERT INTO `__session_event_v3_terminal_source_guard` (`violation_count`) +WITH `expected_gate` (`type`, `name`, `table_name`, `sql`) AS ( + VALUES ('table', '__protocol_v3_cutover', '__protocol_v3_cutover', 'CREATE TABLE "__protocol_v3_cutover" ( + "id" integer PRIMARY KEY CHECK ("id" = 1), + "command_freeze" integer NOT NULL DEFAULT 0 CHECK ("command_freeze" IN (0, 1)), + "enabled" integer NOT NULL DEFAULT 1 CHECK ("enabled" IN (0, 1)), + "phase" text NOT NULL DEFAULT ''draining'' CHECK ("phase" IN (''draining'', ''queues_resuming'')), + "pre_migration_bookmark" text, + "release_tree_oid" text NOT NULL CHECK ((length("release_tree_oid") = 40 OR length("release_tree_oid") = 64) AND "release_tree_oid" = lower("release_tree_oid") AND "release_tree_oid" NOT GLOB ''*[^0-9a-f]*''), + "smoke_account_id" text, + "smoke_request_key" text, + "smoke_session_id" text, + "target_container_application_version" integer, + "target_container_image_digest" text, + "target_worker_version_id" text, + "started_at" integer NOT NULL DEFAULT (CAST(unixepoch(''subsec'') * 1000 AS INTEGER)), + CONSTRAINT "protocol_v3_cutover_phase_check" CHECK ( + ("phase" = ''draining'' AND "enabled" = 1) + OR ("phase" = ''queues_resuming'' AND "command_freeze" = 1) + ), + CONSTRAINT "protocol_v3_cutover_rollout_check" CHECK ( + ("target_container_application_version" IS NULL AND "target_container_image_digest" IS NULL AND "target_worker_version_id" IS NULL) + OR ("target_container_application_version" >= 0 AND length("target_container_image_digest") = 64 AND "target_container_image_digest" = lower("target_container_image_digest") AND "target_container_image_digest" NOT GLOB ''*[^0-9a-f]*'' AND length(trim("target_worker_version_id")) > 0) + ) +)'), + ('trigger', '__protocol_v3_cutover_session_run_insert', 'session_run', 'CREATE TRIGGER "__protocol_v3_cutover_session_run_insert" +BEFORE INSERT ON "session_run" +WHEN NEW."status" IN (''queued'', ''booting'', ''running'', ''waiting_input'') + AND EXISTS (SELECT 1 FROM "__protocol_v3_cutover" WHERE "enabled" = 1) + AND NOT EXISTS ( + SELECT 1 + FROM "__protocol_v3_cutover" AS "gate" + INNER JOIN "session" AS "smoke_session" + ON "smoke_session"."id" = NEW."session_id" + AND "smoke_session"."creator_account_id" = "gate"."smoke_account_id" + AND "smoke_session"."end_user_id" = "gate"."smoke_request_key" + WHERE "gate"."enabled" = 1 + AND "gate"."smoke_request_key" IS NOT NULL + AND ("gate"."smoke_session_id" IS NULL OR "gate"."smoke_session_id" = "smoke_session"."id") + ) +BEGIN + SELECT RAISE(ABORT, ''protocol v3 cutover blocks new active Session Runs''); +END'), + ('trigger', '__protocol_v3_cutover_session_run_update', 'session_run', 'CREATE TRIGGER "__protocol_v3_cutover_session_run_update" +BEFORE UPDATE OF "status" ON "session_run" +WHEN NEW."status" IN (''queued'', ''booting'', ''running'', ''waiting_input'') + AND OLD."status" NOT IN (''queued'', ''booting'', ''running'', ''waiting_input'') + AND EXISTS (SELECT 1 FROM "__protocol_v3_cutover" WHERE "enabled" = 1) + AND NOT EXISTS ( + SELECT 1 + FROM "__protocol_v3_cutover" AS "gate" + INNER JOIN "session" AS "smoke_session" + ON "smoke_session"."id" = NEW."session_id" + AND "smoke_session"."creator_account_id" = "gate"."smoke_account_id" + AND "smoke_session"."end_user_id" = "gate"."smoke_request_key" + WHERE "gate"."enabled" = 1 + AND "gate"."smoke_request_key" IS NOT NULL + AND ("gate"."smoke_session_id" IS NULL OR "gate"."smoke_session_id" = "smoke_session"."id") + ) +BEGIN + SELECT RAISE(ABORT, ''protocol v3 cutover blocks Session Run reactivation''); +END'), + ('trigger', '__protocol_v3_cutover_app_deployment_run_insert', 'app_deployment_run', 'CREATE TRIGGER "__protocol_v3_cutover_app_deployment_run_insert" +BEFORE INSERT ON "app_deployment_run" +WHEN NEW."status" IN (''queued'', ''preparing'', ''building'', ''submitting'', ''submitted'', ''activating'') + AND EXISTS (SELECT 1 FROM "__protocol_v3_cutover" WHERE "enabled" = 1) +BEGIN + SELECT RAISE(ABORT, ''protocol v3 cutover blocks new active App deployment Runs''); +END'), + ('trigger', '__protocol_v3_cutover_app_deployment_run_update', 'app_deployment_run', 'CREATE TRIGGER "__protocol_v3_cutover_app_deployment_run_update" +BEFORE UPDATE OF "status" ON "app_deployment_run" +WHEN NEW."status" IN (''queued'', ''preparing'', ''building'', ''submitting'', ''submitted'', ''activating'') + AND OLD."status" NOT IN (''queued'', ''preparing'', ''building'', ''submitting'', ''submitted'', ''activating'') + AND EXISTS (SELECT 1 FROM "__protocol_v3_cutover" WHERE "enabled" = 1) +BEGIN + SELECT RAISE(ABORT, ''protocol v3 cutover blocks App deployment Run reactivation''); +END'), + ('trigger', '__protocol_v3_cutover_driver_insert', 'driver_instance', 'CREATE TRIGGER "__protocol_v3_cutover_driver_insert" +BEFORE INSERT ON "driver_instance" +WHEN NEW."status" IN (''provisioning'', ''connecting'', ''ready'', ''stopping'') + AND EXISTS (SELECT 1 FROM "__protocol_v3_cutover" WHERE "enabled" = 1) + AND NOT EXISTS ( + SELECT 1 + FROM "__protocol_v3_cutover" AS "gate" + INNER JOIN "session" AS "smoke_session" + ON "smoke_session"."id" = NEW."sandbox_session_id" + AND "smoke_session"."creator_account_id" = "gate"."smoke_account_id" + AND "smoke_session"."end_user_id" = "gate"."smoke_request_key" + INNER JOIN "sandbox" AS "smoke_sandbox" + ON "smoke_sandbox"."id" = NEW."sandbox_id" + AND "smoke_sandbox"."subject_kind" = ''session'' + AND "smoke_sandbox"."subject_id" = "smoke_session"."id" + WHERE "gate"."enabled" = 1 + AND "gate"."smoke_request_key" IS NOT NULL + AND ("gate"."smoke_session_id" IS NULL OR "gate"."smoke_session_id" = "smoke_session"."id") + ) +BEGIN + SELECT RAISE(ABORT, ''protocol v3 cutover blocks new live Driver instances''); +END'), + ('trigger', '__protocol_v3_cutover_driver_update', 'driver_instance', 'CREATE TRIGGER "__protocol_v3_cutover_driver_update" +BEFORE UPDATE OF "status" ON "driver_instance" +WHEN NEW."status" IN (''provisioning'', ''connecting'', ''ready'', ''stopping'') + AND OLD."status" NOT IN (''provisioning'', ''connecting'', ''ready'', ''stopping'') + AND EXISTS (SELECT 1 FROM "__protocol_v3_cutover" WHERE "enabled" = 1) + AND NOT EXISTS ( + SELECT 1 + FROM "__protocol_v3_cutover" AS "gate" + INNER JOIN "session" AS "smoke_session" + ON "smoke_session"."id" = NEW."sandbox_session_id" + AND "smoke_session"."creator_account_id" = "gate"."smoke_account_id" + AND "smoke_session"."end_user_id" = "gate"."smoke_request_key" + INNER JOIN "sandbox" AS "smoke_sandbox" + ON "smoke_sandbox"."id" = NEW."sandbox_id" + AND "smoke_sandbox"."subject_kind" = ''session'' + AND "smoke_sandbox"."subject_id" = "smoke_session"."id" + WHERE "gate"."enabled" = 1 + AND "gate"."smoke_request_key" IS NOT NULL + AND ("gate"."smoke_session_id" IS NULL OR "gate"."smoke_session_id" = "smoke_session"."id") + ) +BEGIN + SELECT RAISE(ABORT, ''protocol v3 cutover blocks Driver reactivation''); +END'), + ('trigger', '__protocol_v3_cutover_command_insert', 'driver_command', 'CREATE TRIGGER "__protocol_v3_cutover_command_insert" +BEFORE INSERT ON "driver_command" +WHEN EXISTS ( + SELECT 1 FROM "__protocol_v3_cutover" + WHERE "enabled" = 1 + AND ("command_freeze" = 1 OR NEW."kind" IN (''input.start'', ''mcp.execute'')) + ) + AND NOT ( + NEW."kind" = ''session.stop'' + AND EXISTS ( + SELECT 1 + FROM "driver_instance" AS "smoke_driver" + WHERE "smoke_driver"."id" = NEW."driver_instance_id" + AND EXISTS ( + SELECT 1 + FROM "__protocol_v3_cutover" AS "gate" + INNER JOIN "session" AS "smoke_session" + ON "smoke_session"."id" = "smoke_driver"."sandbox_session_id" + AND "smoke_session"."creator_account_id" = "gate"."smoke_account_id" + AND "smoke_session"."end_user_id" = "gate"."smoke_request_key" + INNER JOIN "sandbox" AS "smoke_sandbox" + ON "smoke_sandbox"."id" = "smoke_driver"."sandbox_id" + AND "smoke_sandbox"."subject_kind" = ''session'' + AND "smoke_sandbox"."subject_id" = "smoke_session"."id" + WHERE "gate"."enabled" = 1 + AND "gate"."smoke_request_key" IS NOT NULL + AND ("gate"."smoke_session_id" IS NULL OR "gate"."smoke_session_id" = "smoke_session"."id") + ) + ) + ) +BEGIN + SELECT RAISE(ABORT, ''protocol v3 cutover blocks new Driver commands''); +END'), + ('trigger', '__protocol_v3_cutover_sandbox_insert', 'sandbox', 'CREATE TRIGGER "__protocol_v3_cutover_sandbox_insert" +BEFORE INSERT ON "sandbox" +WHEN (NEW."status" <> ''cold'' + OR NEW."status_operation_id" IS NOT NULL + OR NEW."claim_owner" IS NOT NULL + OR NEW."claim_expires_at" IS NOT NULL) + AND EXISTS (SELECT 1 FROM "__protocol_v3_cutover" WHERE "enabled" = 1) + AND NOT (NEW."subject_kind" = ''session'' AND EXISTS ( + SELECT 1 + FROM "__protocol_v3_cutover" AS "gate" + INNER JOIN "session" AS "smoke_session" + ON "smoke_session"."id" = NEW."subject_id" + AND "smoke_session"."creator_account_id" = "gate"."smoke_account_id" + AND "smoke_session"."end_user_id" = "gate"."smoke_request_key" + WHERE "gate"."enabled" = 1 + AND "gate"."smoke_request_key" IS NOT NULL + AND ("gate"."smoke_session_id" IS NULL OR "gate"."smoke_session_id" = "smoke_session"."id") + )) +BEGIN + SELECT RAISE(ABORT, ''protocol v3 cutover blocks new active sandboxes''); +END'), + ('trigger', '__protocol_v3_cutover_sandbox_update', 'sandbox', 'CREATE TRIGGER "__protocol_v3_cutover_sandbox_update" +BEFORE UPDATE OF "status", "status_operation_id", "claim_owner", "claim_expires_at" ON "sandbox" +WHEN OLD."status" = ''cold'' + AND OLD."status_operation_id" IS NULL + AND OLD."claim_owner" IS NULL + AND OLD."claim_expires_at" IS NULL + AND (NEW."status" <> ''cold'' + OR NEW."status_operation_id" IS NOT NULL + OR NEW."claim_owner" IS NOT NULL + OR NEW."claim_expires_at" IS NOT NULL) + AND EXISTS (SELECT 1 FROM "__protocol_v3_cutover" WHERE "enabled" = 1) + AND NOT (NEW."subject_kind" = ''session'' AND EXISTS ( + SELECT 1 + FROM "__protocol_v3_cutover" AS "gate" + INNER JOIN "session" AS "smoke_session" + ON "smoke_session"."id" = NEW."subject_id" + AND "smoke_session"."creator_account_id" = "gate"."smoke_account_id" + AND "smoke_session"."end_user_id" = "gate"."smoke_request_key" + WHERE "gate"."enabled" = 1 + AND "gate"."smoke_request_key" IS NOT NULL + AND ("gate"."smoke_session_id" IS NULL OR "gate"."smoke_session_id" = "smoke_session"."id") + )) +BEGIN + SELECT RAISE(ABORT, ''protocol v3 cutover blocks sandbox activation''); +END'), + ('trigger', '__protocol_v3_cutover_sandbox_session_insert', 'sandbox_session', 'CREATE TRIGGER "__protocol_v3_cutover_sandbox_session_insert" +BEFORE INSERT ON "sandbox_session" +WHEN NEW."status" NOT IN (''closed'', ''error'') + AND EXISTS (SELECT 1 FROM "__protocol_v3_cutover" WHERE "enabled" = 1) + AND NOT EXISTS ( + SELECT 1 + FROM "__protocol_v3_cutover" AS "gate" + INNER JOIN "session" AS "smoke_session" + ON "smoke_session"."id" = NEW."session_id" + AND "smoke_session"."creator_account_id" = "gate"."smoke_account_id" + AND "smoke_session"."end_user_id" = "gate"."smoke_request_key" + INNER JOIN "sandbox" AS "smoke_sandbox" + ON "smoke_sandbox"."id" = NEW."sandbox_id" + AND "smoke_sandbox"."subject_kind" = ''session'' + AND "smoke_sandbox"."subject_id" = "smoke_session"."id" + WHERE "gate"."enabled" = 1 + AND "gate"."smoke_request_key" IS NOT NULL + AND ("gate"."smoke_session_id" IS NULL OR "gate"."smoke_session_id" = "smoke_session"."id") + ) +BEGIN + SELECT RAISE(ABORT, ''protocol v3 cutover blocks new active sandbox Sessions''); +END'), + ('trigger', '__protocol_v3_cutover_sandbox_session_update', 'sandbox_session', 'CREATE TRIGGER "__protocol_v3_cutover_sandbox_session_update" +BEFORE UPDATE OF "status" ON "sandbox_session" +WHEN OLD."status" IN (''closed'', ''error'') + AND NEW."status" NOT IN (''closed'', ''error'') + AND EXISTS (SELECT 1 FROM "__protocol_v3_cutover" WHERE "enabled" = 1) + AND NOT EXISTS ( + SELECT 1 + FROM "__protocol_v3_cutover" AS "gate" + INNER JOIN "session" AS "smoke_session" + ON "smoke_session"."id" = NEW."session_id" + AND "smoke_session"."creator_account_id" = "gate"."smoke_account_id" + AND "smoke_session"."end_user_id" = "gate"."smoke_request_key" + INNER JOIN "sandbox" AS "smoke_sandbox" + ON "smoke_sandbox"."id" = NEW."sandbox_id" + AND "smoke_sandbox"."subject_kind" = ''session'' + AND "smoke_sandbox"."subject_id" = "smoke_session"."id" + WHERE "gate"."enabled" = 1 + AND "gate"."smoke_request_key" IS NOT NULL + AND ("gate"."smoke_session_id" IS NULL OR "gate"."smoke_session_id" = "smoke_session"."id") + ) +BEGIN + SELECT RAISE(ABORT, ''protocol v3 cutover blocks sandbox Session reactivation''); +END'), + ('trigger', '__protocol_v3_cutover_sandbox_backup_insert', 'sandbox_backup', 'CREATE TRIGGER "__protocol_v3_cutover_sandbox_backup_insert" +BEFORE INSERT ON "sandbox_backup" +WHEN NEW."status" NOT IN (''ready'', ''pruned'') + AND EXISTS (SELECT 1 FROM "__protocol_v3_cutover" WHERE "enabled" = 1) +BEGIN + SELECT RAISE(ABORT, ''protocol v3 cutover blocks new sandbox backup work''); +END'), + ('trigger', '__protocol_v3_cutover_sandbox_backup_update', 'sandbox_backup', 'CREATE TRIGGER "__protocol_v3_cutover_sandbox_backup_update" +BEFORE UPDATE OF "status" ON "sandbox_backup" +WHEN OLD."status" IN (''ready'', ''pruned'') + AND NEW."status" NOT IN (''ready'', ''pruned'') + AND EXISTS (SELECT 1 FROM "__protocol_v3_cutover" WHERE "enabled" = 1) +BEGIN + SELECT RAISE(ABORT, ''protocol v3 cutover blocks sandbox backup reactivation''); +END'), + ('trigger', '__protocol_v3_cutover_session_insert', 'session', 'CREATE TRIGGER "__protocol_v3_cutover_session_insert" +BEFORE INSERT ON "session" +WHEN (NEW."status" NOT IN (''IDLE'', ''TERMINATED'') OR NEW."status_operation_id" IS NOT NULL) + AND EXISTS (SELECT 1 FROM "__protocol_v3_cutover" WHERE "enabled" = 1) + AND NOT EXISTS ( + SELECT 1 + FROM "__protocol_v3_cutover" AS "gate" + WHERE "gate"."enabled" = 1 + AND "gate"."smoke_request_key" IS NOT NULL + AND "gate"."smoke_session_id" IS NULL + AND "gate"."smoke_account_id" = NEW."creator_account_id" + AND "gate"."smoke_request_key" = NEW."end_user_id" + ) +BEGIN + SELECT RAISE(ABORT, ''protocol v3 cutover blocks new Session operations''); +END'), + ('trigger', '__protocol_v3_cutover_session_update', 'session', 'CREATE TRIGGER "__protocol_v3_cutover_session_update" +BEFORE UPDATE OF "status", "status_operation_id" ON "session" +WHEN OLD."status" IN (''IDLE'', ''TERMINATED'') + AND OLD."status_operation_id" IS NULL + AND (NEW."status" NOT IN (''IDLE'', ''TERMINATED'') OR NEW."status_operation_id" IS NOT NULL) + AND EXISTS (SELECT 1 FROM "__protocol_v3_cutover" WHERE "enabled" = 1) + AND NOT EXISTS ( + SELECT 1 + FROM "__protocol_v3_cutover" AS "gate" + INNER JOIN "session" AS "smoke_session" + ON "smoke_session"."id" = NEW."id" + AND "smoke_session"."creator_account_id" = "gate"."smoke_account_id" + AND "smoke_session"."end_user_id" = "gate"."smoke_request_key" + WHERE "gate"."enabled" = 1 + AND "gate"."smoke_request_key" IS NOT NULL + AND ("gate"."smoke_session_id" IS NULL OR "gate"."smoke_session_id" = "smoke_session"."id") + ) +BEGIN + SELECT RAISE(ABORT, ''protocol v3 cutover blocks Session operation acquisition''); +END'), + ('trigger', '__protocol_v3_cutover_api_command_insert', 'api_command', 'CREATE TRIGGER "__protocol_v3_cutover_api_command_insert" +BEFORE INSERT ON "api_command" +WHEN NEW."status" IN (''queued'', ''running'') + AND EXISTS ( + SELECT 1 FROM "__protocol_v3_cutover" + WHERE "enabled" = 1 + AND ("command_freeze" = 1 OR NEW."kind" IN (''session_run_dispatch'', ''app_deployment_run_dispatch'', ''environment_package_artifact_build'')) + ) +BEGIN + SELECT RAISE(ABORT, ''protocol v3 cutover blocks new nonterminal API commands''); +END'), + ('trigger', '__protocol_v3_cutover_api_command_update', 'api_command', 'CREATE TRIGGER "__protocol_v3_cutover_api_command_update" +BEFORE UPDATE OF "kind", "status", "claim_owner", "claim_expires_at" ON "api_command" +WHEN NEW."status" IN (''queued'', ''running'') + AND (OLD."status" NOT IN (''queued'', ''running'') OR NEW."kind" IS NOT OLD."kind") + AND EXISTS ( + SELECT 1 FROM "__protocol_v3_cutover" + WHERE "enabled" = 1 + AND ("command_freeze" = 1 OR NEW."kind" IN (''session_run_dispatch'', ''app_deployment_run_dispatch'', ''environment_package_artifact_build'')) + ) +BEGIN + SELECT RAISE(ABORT, ''protocol v3 cutover blocks API command admission''); +END') +), +`actual_gate` AS ( + SELECT "type", "name", "tbl_name" AS "table_name", "sql" + FROM "sqlite_master" + WHERE "name" COLLATE NOCASE IN ('__protocol_v3_cutover', '__protocol_v3_cutover_session_run_insert', '__protocol_v3_cutover_session_run_update', '__protocol_v3_cutover_app_deployment_run_insert', '__protocol_v3_cutover_app_deployment_run_update', '__protocol_v3_cutover_driver_insert', '__protocol_v3_cutover_driver_update', '__protocol_v3_cutover_command_insert', '__protocol_v3_cutover_sandbox_insert', '__protocol_v3_cutover_sandbox_update', '__protocol_v3_cutover_sandbox_session_insert', '__protocol_v3_cutover_sandbox_session_update', '__protocol_v3_cutover_sandbox_backup_insert', '__protocol_v3_cutover_sandbox_backup_update', '__protocol_v3_cutover_session_insert', '__protocol_v3_cutover_session_update', '__protocol_v3_cutover_api_command_insert', '__protocol_v3_cutover_api_command_update', '__protocol_v3_cutover_sandbox_backup_staging_insert') + OR ( + "type" = 'trigger' + AND "tbl_name" COLLATE NOCASE IN ('__production_deploy_lease', '__protocol_v3_cutover', '__protocol_v3_legacy_rewrite_authorization', 'api_command', 'app_deployment_run', 'driver_command', 'driver_instance', 'sandbox', 'sandbox_backup', 'sandbox_backup_staging', 'sandbox_session', 'session', 'session_run') + AND NOT ( + ( + "type" = 'trigger' + AND "name" = '__protocol_v3_legacy_rewrite_gate_update' COLLATE BINARY + AND "tbl_name" = '__protocol_v3_cutover' COLLATE BINARY + AND "sql" = 'CREATE TRIGGER "__protocol_v3_legacy_rewrite_gate_update" +AFTER UPDATE ON "__protocol_v3_cutover" +BEGIN + DELETE FROM "__protocol_v3_legacy_rewrite_authorization" WHERE "id" = 1; +END' COLLATE BINARY +) + OR ( + "type" = 'trigger' + AND "name" = 'sandbox_identity_immutable' COLLATE BINARY + AND "tbl_name" = 'sandbox' COLLATE BINARY + AND "sql" = 'CREATE TRIGGER `sandbox_identity_immutable` +BEFORE UPDATE OF `id`, `kind`, `subject_kind`, `subject_id`, `agent_id`, `app_id`, `owner_account_id` ON `sandbox` +WHEN NEW.`id` IS NOT OLD.`id` + OR NEW.`kind` IS NOT OLD.`kind` + OR NEW.`subject_kind` IS NOT OLD.`subject_kind` + OR NEW.`subject_id` IS NOT OLD.`subject_id` + OR NEW.`agent_id` IS NOT OLD.`agent_id` + OR NEW.`app_id` IS NOT OLD.`app_id` + OR NEW.`owner_account_id` IS NOT OLD.`owner_account_id` +BEGIN + SELECT RAISE(ABORT, ''sandbox identity is immutable''); +END' COLLATE BINARY +) + ) + ) +), +`manifest` AS ( + SELECT + count(*) AS `candidate_count`, + json_group_array(json_array( + `id`, `session_id`, `run_id`, `event_type`, `source_event_id`, `seq` + )) AS `candidate_manifest_json` + FROM ( + SELECT `id`, `session_id`, `run_id`, `event_type`, `source_event_id`, `seq` + FROM `session_event` + WHERE `event_type` IN ('run.cancelled', 'run.completed', 'run.failed') + AND `source_event_id` <> + 'session-run-terminal:' || `run_id` || ':' || `event_type` + ORDER BY `id` COLLATE BINARY + ) +) +SELECT CASE + WHEN `manifest`.`candidate_count` = 0 THEN 0 + WHEN EXISTS ( + SELECT 1 + FROM `__protocol_v3_legacy_rewrite_authorization` AS `authorization` + INNER JOIN `__production_deploy_lease` AS `lease` + ON `lease`.`id` = 1 + AND `lease`.`owner` = `authorization`.`deploy_owner` + WHERE `authorization`.`id` = 1 + AND `authorization`.`expires_at` > unixepoch() + AND `authorization`.`candidate_count` = `manifest`.`candidate_count` + AND `authorization`.`candidate_manifest_json` = `manifest`.`candidate_manifest_json` COLLATE BINARY + AND ( + SELECT `sql` FROM `sqlite_master` + WHERE `type` = 'table' + AND `name` = '__protocol_v3_legacy_rewrite_authorization' + ) = 'CREATE TABLE "__protocol_v3_legacy_rewrite_authorization" ( + "id" integer PRIMARY KEY CHECK ("id" = 1), + "gate_id" integer NOT NULL CHECK ("gate_id" = 1) REFERENCES "__protocol_v3_cutover" ("id") ON DELETE CASCADE, + "bookmark" text NOT NULL CHECK (length(trim("bookmark")) > 0), + "candidate_count" integer NOT NULL CHECK ("candidate_count" >= 0), + "candidate_manifest_json" text NOT NULL CHECK (json_valid("candidate_manifest_json") AND json_type("candidate_manifest_json") = ''array''), + "deploy_owner" text NOT NULL, + "expires_at" integer NOT NULL, + "release_tree_oid" text NOT NULL CHECK ((length("release_tree_oid") = 40 OR length("release_tree_oid") = 64) AND "release_tree_oid" = lower("release_tree_oid") AND "release_tree_oid" NOT GLOB ''*[^0-9a-f]*'') +)' COLLATE BINARY + AND ( + SELECT `sql` FROM `sqlite_master` + WHERE `type` = 'table' AND `name` = '__production_deploy_lease' + ) = 'CREATE TABLE "__production_deploy_lease" ( + "id" integer PRIMARY KEY CHECK ("id" = 1), + "owner" text NOT NULL +)' COLLATE BINARY + AND NOT EXISTS ( + SELECT 1 FROM `sqlite_master` + WHERE `type` = 'trigger' AND `tbl_name` = '__production_deploy_lease' COLLATE BINARY + ) + AND ( + SELECT `sql` FROM `sqlite_master` + WHERE `type` = 'trigger' + AND `name` = '__protocol_v3_legacy_rewrite_gate_update' + ) = 'CREATE TRIGGER "__protocol_v3_legacy_rewrite_gate_update" +AFTER UPDATE ON "__protocol_v3_cutover" +BEGIN + DELETE FROM "__protocol_v3_legacy_rewrite_authorization" WHERE "id" = 1; +END' COLLATE BINARY + AND (SELECT count(*) FROM `actual_gate`) = 18 + AND ( + SELECT count(*) + FROM `actual_gate` AS `actual` + INNER JOIN `expected_gate` AS `expected` + ON `expected`.`type` = `actual`.`type` COLLATE BINARY + AND `expected`.`name` = `actual`.`name` COLLATE BINARY + AND `expected`.`table_name` = `actual`.`table_name` COLLATE BINARY + AND `expected`.`sql` = `actual`.`sql` COLLATE BINARY + ) = 18 + ) THEN 0 + ELSE 1 +END +FROM `manifest`; +--> statement-breakpoint +UPDATE `session_event` +SET `source_event_id` = 'session-run-terminal:' || `run_id` || ':' || `event_type` +WHERE `event_type` IN ('run.cancelled', 'run.completed', 'run.failed') + AND `source_event_id` <> 'session-run-terminal:' || `run_id` || ':' || `event_type`; +--> statement-breakpoint +INSERT INTO `__session_event_v3_terminal_source_guard` (`violation_count`) +SELECT count(*) +FROM `session_event` +WHERE `event_type` IN ('run.cancelled', 'run.completed', 'run.failed') + AND `source_event_id` <> 'session-run-terminal:' || `run_id` || ':' || `event_type`; +--> statement-breakpoint +DROP TABLE `__session_event_v3_terminal_source_guard`; +--> statement-breakpoint +ALTER TABLE `session_event` ADD `stream_id` text; +--> statement-breakpoint +ALTER TABLE `session_message` ADD `projection_format` text NOT NULL DEFAULT 'materialized' +CHECK (`projection_format` IN ('materialized', 'event_stream_v3')) +CHECK (`projection_format` <> 'event_stream_v3' OR (`role` = 'assistant' AND `session_run_id` IS NOT NULL AND `content_text` = '' AND `plan_json` IS NULL AND `segments_json` IS NULL)); +--> statement-breakpoint +ALTER TABLE `session_event` ADD `semantic_hash` text CHECK (`semantic_hash` IS NULL OR (length(`semantic_hash`) = 64 AND `semantic_hash` = lower(`semantic_hash`) AND `semantic_hash` NOT GLOB '*[^0-9a-f]*')); +--> statement-breakpoint +ALTER TABLE `session_event` ADD `tool_input_delta_json` text CHECK (`tool_input_delta_json` IS NULL OR `tool_input_json` IS NULL); +--> statement-breakpoint +ALTER TABLE `session_event` ADD `tool_output_delta_text` text; +--> statement-breakpoint +ALTER TABLE `session_event` ADD `tool_output_text` text CHECK (`tool_output_delta_text` IS NULL OR `tool_output_text` IS NULL); +--> statement-breakpoint +ALTER TABLE `session_event` ADD `tool_parent_message_id` text; +--> statement-breakpoint +ALTER TABLE `session_event` ADD `tool_result_message_id` text; +--> statement-breakpoint +ALTER TABLE `session_event` ADD `tool_status` text CHECK (`tool_status` IS NULL OR `tool_status` IN ('running', 'completed', 'failed', 'cancelled')); +--> statement-breakpoint +ALTER TABLE `session_event` ADD `mcp_command_id` text +CHECK (`mcp_command_id` = upper(`mcp_command_id`) AND length(`mcp_command_id`) = 26 AND substr(`mcp_command_id`, 1, 1) GLOB '[0-7]' AND `mcp_command_id` NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*') +CHECK (`mcp_command_id` IS NULL OR (`event_type` = 'tool.call.updated' AND `tool_status` IS NOT NULL AND `tool_status` IN ('completed', 'failed', 'cancelled'))); +--> statement-breakpoint +UPDATE `session_event` +SET `stream_id` = `id` +WHERE `stream_id` IS NULL + AND (`event_type` LIKE 'message.%' OR `event_type` LIKE 'thought.%'); +--> statement-breakpoint +CREATE UNIQUE INDEX `session_event_mcp_terminal_winner_idx` +ON `session_event` (`session_id`, `mcp_command_id`) +WHERE `mcp_command_id` IS NOT NULL; +--> statement-breakpoint +CREATE UNIQUE INDEX `session_event_run_terminal_winner_idx` +ON `session_event` (`session_id`, `run_id`) +WHERE `semantic_hash` IS NOT NULL + AND `run_id` IS NOT NULL + AND `event_type` IN ('run.cancelled', 'run.completed', 'run.failed'); +--> statement-breakpoint +CREATE INDEX `session_event_run_stream_process_seq_idx` +ON `session_event` (`run_id`, `stream_id`, `process_type`, `seq`); +--> statement-breakpoint +CREATE INDEX `session_event_run_tool_call_seq_idx` +ON `session_event` (`run_id`, `tool_call_id`, `seq`); +--> statement-breakpoint +ALTER TABLE `session_run` ADD `error_retryable` integer CHECK (`error_retryable` IS NULL OR (`error_retryable` IN (0, 1) AND `error_code` IS NOT NULL AND `error_details_json` IS NOT NULL AND `error_message` IS NOT NULL)); +--> statement-breakpoint +UPDATE `session_run` +SET `error_details_json` = COALESCE(`error_details_json`, '{}'), + `error_retryable` = 0 +WHERE `error_code` IS NOT NULL + AND `error_message` IS NOT NULL + AND (`error_details_json` IS NULL OR `error_retryable` IS NULL); +--> statement-breakpoint +DROP TRIGGER IF EXISTS `__protocol_v3_legacy_rewrite_gate_update`; +--> statement-breakpoint +DROP TABLE `__protocol_v3_legacy_rewrite_authorization`; diff --git a/pkgs/db/drizzle/0015_session-cleanup-operation.sql b/pkgs/db/drizzle/0015_session-cleanup-operation.sql new file mode 100644 index 00000000..0d8a0c7e --- /dev/null +++ b/pkgs/db/drizzle/0015_session-cleanup-operation.sql @@ -0,0 +1,8 @@ +ALTER TABLE `session` ADD `cleanup_operation_kind` text CONSTRAINT `session_cleanup_operation_kind_check` CHECK (`cleanup_operation_kind` IS NULL OR (`cleanup_operation_kind` IN ('archive', 'delete') AND `archived_at` IS NOT NULL AND `status` IN ('IDLE', 'RESCHEDULING') AND (`status_operation_id` IS NOT NULL OR (`cleanup_operation_kind` = 'archive' AND `status` = 'IDLE'))));--> statement-breakpoint +ALTER TABLE `session` ADD `runtime_provisioning_operation_id` text CHECK (`runtime_provisioning_operation_id` = upper(`runtime_provisioning_operation_id`) AND length(`runtime_provisioning_operation_id`) = 26 AND substr(`runtime_provisioning_operation_id`, 1, 1) GLOB '[0-7]' AND `runtime_provisioning_operation_id` NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*');--> statement-breakpoint +ALTER TABLE `session` ADD `runtime_provisioning_run_id` text CHECK (`runtime_provisioning_run_id` = upper(`runtime_provisioning_run_id`) AND length(`runtime_provisioning_run_id`) = 26 AND substr(`runtime_provisioning_run_id`, 1, 1) GLOB '[0-7]' AND `runtime_provisioning_run_id` NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*');--> statement-breakpoint +ALTER TABLE `session` ADD `runtime_provisioning_sandbox_id` text CHECK (`runtime_provisioning_sandbox_id` = upper(`runtime_provisioning_sandbox_id`) AND length(`runtime_provisioning_sandbox_id`) = 26 AND substr(`runtime_provisioning_sandbox_id`, 1, 1) GLOB '[0-7]' AND `runtime_provisioning_sandbox_id` NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*');--> statement-breakpoint +ALTER TABLE `session` ADD `runtime_provisioning_heartbeat_at` integer CONSTRAINT `session_runtime_provisioning_lease_check` CHECK ((`runtime_provisioning_operation_id` IS NULL AND `runtime_provisioning_run_id` IS NULL AND `runtime_provisioning_sandbox_id` IS NULL AND `runtime_provisioning_heartbeat_at` IS NULL) OR (`runtime_provisioning_operation_id` IS NOT NULL AND `runtime_provisioning_sandbox_id` IS NOT NULL AND `runtime_provisioning_heartbeat_at` IS NOT NULL AND typeof(`runtime_provisioning_heartbeat_at`) = 'integer' AND `runtime_provisioning_heartbeat_at` >= 0 AND `archived_at` IS NULL AND `cleanup_operation_kind` IS NULL AND `status_operation_id` IS NULL));--> statement-breakpoint +CREATE INDEX `session_cleanup_operation_updated_idx` ON `session` (`cleanup_operation_kind`,`status`,`updated_at`,`id`); +--> statement-breakpoint +CREATE INDEX `session_runtime_provisioning_heartbeat_idx` ON `session` (`runtime_provisioning_heartbeat_at`,`id`); diff --git a/pkgs/db/drizzle/0016_durable-event-side-effects.sql b/pkgs/db/drizzle/0016_durable-event-side-effects.sql new file mode 100644 index 00000000..b1760886 --- /dev/null +++ b/pkgs/db/drizzle/0016_durable-event-side-effects.sql @@ -0,0 +1,161 @@ +ALTER TABLE `file_record` +ADD `runtime_event_seq` integer +CHECK (`runtime_event_seq` IS NULL OR `runtime_event_seq` >= 0); +--> statement-breakpoint +CREATE INDEX `file_record_runtime_event_seq_idx` +ON `file_record` (`scope_id`, `runtime_event_seq`); +--> statement-breakpoint +CREATE TABLE `runtime_artifact_attempt` ( + `accepted_event_id` text, + `created_at` integer NOT NULL, + `created_by_account_id` text CHECK (`created_by_account_id` = upper(`created_by_account_id`) AND length(`created_by_account_id`) = 26 AND substr(`created_by_account_id`, 1, 1) GLOB '[0-7]' AND `created_by_account_id` NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*') NOT NULL, + `delete_after` integer, + `driver_connection_id` text NOT NULL, + `driver_generation` integer NOT NULL, + `driver_instance_id` text CHECK (`driver_instance_id` = upper(`driver_instance_id`) AND length(`driver_instance_id`) = 26 AND substr(`driver_instance_id`, 1, 1) GLOB '[0-7]' AND `driver_instance_id` NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*') NOT NULL, + `event_type` text NOT NULL, + `expires_at` integer, + `id` text PRIMARY KEY NOT NULL, + `manifest_json` text, + `manifest_sha256` text, + `owned_object_keys_json` text DEFAULT '[]' NOT NULL, + `run_id` text CHECK (`run_id` = upper(`run_id`) AND length(`run_id`) = 26 AND substr(`run_id`, 1, 1) GLOB '[0-7]' AND `run_id` NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*') NOT NULL, + `semantic_hash` text NOT NULL, + `session_id` text CHECK (`session_id` = upper(`session_id`) AND length(`session_id`) = 26 AND substr(`session_id`, 1, 1) GLOB '[0-7]' AND `session_id` NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*') NOT NULL, + `source_event_id` text NOT NULL, + `status` text NOT NULL, + `updated_at` integer NOT NULL, + CONSTRAINT "runtime_artifact_attempt_manifest_check" CHECK((`manifest_json` IS NULL AND `manifest_sha256` IS NULL) OR (`manifest_json` IS NOT NULL AND json_valid(`manifest_json`) = 1 AND json_extract(`manifest_json`, '$.version') IS 1 AND json_type(`manifest_json`, '$.captureStatus') IS 'text' AND json_extract(`manifest_json`, '$.captureStatus') IN ('complete', 'omitted_file_limit', 'omitted_runtime_unavailable', 'omitted_size_limit', 'omitted_source_changed', 'omitted_source_missing') AND json_type(`manifest_json`, '$.mode') IS 'text' AND json_extract(`manifest_json`, '$.mode') IN ('delta', 'snapshot') AND (json_extract(`manifest_json`, '$.captureStatus') = 'complete' OR json_array_length(`manifest_json`, '$.files') = 0) AND json_extract(`manifest_json`, '$.sourceEventId') IS `source_event_id` AND json_extract(`manifest_json`, '$.semanticHash') IS `semantic_hash` AND json_type(`manifest_json`, '$.files') IS 'array' AND `manifest_sha256` IS NOT NULL AND length(`manifest_sha256`) = 64 AND `manifest_sha256` = lower(`manifest_sha256`) AND `manifest_sha256` NOT GLOB '*[^0-9a-f]*')), + CONSTRAINT "runtime_artifact_attempt_owned_keys_check" CHECK(json_valid(`owned_object_keys_json`) = 1 AND json_type(`owned_object_keys_json`) IS 'array'), + CONSTRAINT "runtime_artifact_attempt_semantic_hash_check" CHECK(length(`semantic_hash`) = 64 AND `semantic_hash` = lower(`semantic_hash`) AND `semantic_hash` NOT GLOB '*[^0-9a-f]*'), + CONSTRAINT "runtime_artifact_attempt_status_check" CHECK((`status` = 'staging' AND `manifest_json` IS NULL AND `accepted_event_id` IS NULL AND `expires_at` IS NOT NULL AND `delete_after` IS NULL) OR (`status` = 'staged' AND `manifest_json` IS NOT NULL AND `accepted_event_id` IS NULL AND `expires_at` IS NOT NULL AND `delete_after` IS NULL) OR (`status` = 'accepted' AND `manifest_json` IS NOT NULL AND `accepted_event_id` IS NOT NULL AND `expires_at` IS NULL AND `delete_after` IS NULL AND json_array_length(`owned_object_keys_json`) = 0) OR (`status` = 'deleting' AND `accepted_event_id` IS NULL AND `delete_after` IS NOT NULL)), + CONSTRAINT "runtime_artifact_attempt_time_check" CHECK(`driver_generation` >= 0 AND (`expires_at` IS NULL OR `expires_at` >= `created_at`) AND (`delete_after` IS NULL OR `delete_after` >= `created_at`) AND `updated_at` >= `created_at`) +); +--> statement-breakpoint +CREATE INDEX `runtime_artifact_attempt_cleanup_idx` +ON `runtime_artifact_attempt` (`status`, `expires_at`, `updated_at`, `id`); +--> statement-breakpoint +CREATE INDEX `runtime_artifact_attempt_session_status_idx` +ON `runtime_artifact_attempt` (`session_id`, `status`, `id`); +--> statement-breakpoint +CREATE UNIQUE INDEX `runtime_artifact_attempt_accepted_event_idx` +ON `runtime_artifact_attempt` (`accepted_event_id`) +WHERE `accepted_event_id` IS NOT NULL; +--> statement-breakpoint +CREATE TABLE `session_artifact_head` ( + `file_id` text CHECK (`file_id` = upper(`file_id`) AND length(`file_id`) = 26 AND substr(`file_id`, 1, 1) GLOB '[0-7]' AND `file_id` NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*'), + `runtime_event_seq` integer NOT NULL, + `session_id` text CHECK (`session_id` = upper(`session_id`) AND length(`session_id`) = 26 AND substr(`session_id`, 1, 1) GLOB '[0-7]' AND `session_id` NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*') NOT NULL, + `source_event_id` text NOT NULL, + `source_path` text NOT NULL, + `updated_at` integer NOT NULL, + CONSTRAINT "session_artifact_head_path_check" CHECK(length(`source_path`) > 8 AND substr(`source_path`, 1, 8) = 'outputs/' AND instr(`source_path`, char(0)) = 0 AND instr(`source_path`, '\') = 0 AND `source_path` NOT LIKE '%//%' AND `source_path` NOT LIKE '%/./%' AND `source_path` NOT LIKE '%/.' AND `source_path` NOT LIKE '%/../%' AND `source_path` NOT LIKE '%/..'), + CONSTRAINT "session_artifact_head_seq_check" CHECK(`runtime_event_seq` >= 0 AND `updated_at` >= 0), + FOREIGN KEY (`session_id`) REFERENCES `session`(`id`) ON UPDATE no action ON DELETE cascade +); +--> statement-breakpoint +CREATE UNIQUE INDEX `session_artifact_head_session_path_idx` +ON `session_artifact_head` (`session_id`, `source_path`); +--> statement-breakpoint +CREATE INDEX `session_artifact_head_session_seq_idx` +ON `session_artifact_head` (`session_id`, `runtime_event_seq`, `source_path`); +--> statement-breakpoint +WITH legacy_artifact AS ( + SELECT + legacy_file.`id` AS `file_id`, + legacy_file.`scope_id` AS `session_id`, + substr(legacy_file.`parent_path`, 16, length(legacy_file.`parent_path`) - 80) AS `source_path`, + MAX(legacy_file.`created_at`, 0) AS `updated_at`, + row_number() OVER ( + PARTITION BY legacy_file.`scope_id`, substr(legacy_file.`parent_path`, 16, length(legacy_file.`parent_path`) - 80) + ORDER BY legacy_file.`created_at` DESC, legacy_file.`id` DESC + ) AS `rank` + FROM `file_record` AS legacy_file + INNER JOIN `session` AS legacy_session ON legacy_session.`id` = legacy_file.`scope_id` + WHERE legacy_file.`scope_kind` = 'session' + AND legacy_file.`scope_id` IS NOT NULL + AND legacy_file.`session_kind` = 'artifact' + AND legacy_file.`status` = 'ready' + AND legacy_file.`runtime_event_seq` IS NULL + AND substr(legacy_file.`parent_path`, 1, 15) = 'runtime-output/' + AND length(legacy_file.`parent_path`) > 80 + AND substr(legacy_file.`parent_path`, -65, 1) = '/' + AND length(substr(legacy_file.`parent_path`, -64)) = 64 + AND substr(legacy_file.`parent_path`, -64) = lower(substr(legacy_file.`parent_path`, -64)) + AND substr(legacy_file.`parent_path`, -64) NOT GLOB '*[^0-9a-f]*' +) +INSERT INTO `session_artifact_head` ( + `file_id`, `runtime_event_seq`, `session_id`, `source_event_id`, `source_path`, `updated_at` +) +SELECT + `file_id`, 0, `session_id`, 'legacy-file:' || `file_id`, `source_path`, `updated_at` +FROM legacy_artifact +WHERE `rank` = 1 + AND length(`source_path`) > 8 + AND substr(`source_path`, 1, 8) = 'outputs/' + AND instr(`source_path`, char(0)) = 0 + AND instr(`source_path`, '\') = 0 + AND `source_path` NOT LIKE '%//%' + AND `source_path` NOT LIKE '%/./%' + AND `source_path` NOT LIKE '%/.' + AND `source_path` NOT LIKE '%/../%' + AND `source_path` NOT LIKE '%/..'; +--> statement-breakpoint +ALTER TABLE `native_resume_ref` +ADD `observed_event_seq` integer NOT NULL DEFAULT 0 +CHECK (`observed_event_seq` >= 0); +--> statement-breakpoint +ALTER TABLE `session` +ADD `auto_title_event_seq` integer +CHECK (`auto_title_event_seq` IS NULL OR `auto_title_event_seq` >= 0); +--> statement-breakpoint +ALTER TABLE `session_event` +ADD `artifact_attempt_id` text; +--> statement-breakpoint +ALTER TABLE `session_event` +ADD `artifact_manifest_json` text; +--> statement-breakpoint +ALTER TABLE `session_event` +ADD `artifact_manifest_sha256` text +CHECK ( + (`artifact_attempt_id` IS NULL AND `artifact_manifest_json` IS NULL AND `artifact_manifest_sha256` IS NULL) + OR ( + `artifact_attempt_id` IS NOT NULL + AND `artifact_manifest_json` IS NOT NULL + AND json_valid(`artifact_manifest_json`) = 1 + AND json_extract(`artifact_manifest_json`, '$.version') IS 1 + AND json_type(`artifact_manifest_json`, '$.captureStatus') IS 'text' + AND json_extract(`artifact_manifest_json`, '$.captureStatus') IN ('complete', 'omitted_file_limit', 'omitted_runtime_unavailable', 'omitted_size_limit', 'omitted_source_changed', 'omitted_source_missing') + AND json_type(`artifact_manifest_json`, '$.mode') IS 'text' + AND json_extract(`artifact_manifest_json`, '$.mode') IN ('delta', 'snapshot') + AND (json_extract(`artifact_manifest_json`, '$.captureStatus') = 'complete' OR json_array_length(`artifact_manifest_json`, '$.files') = 0) + AND json_extract(`artifact_manifest_json`, '$.sourceEventId') IS `source_event_id` + AND json_extract(`artifact_manifest_json`, '$.semanticHash') IS `semantic_hash` + AND json_type(`artifact_manifest_json`, '$.files') IS 'array' + AND `artifact_manifest_sha256` IS NOT NULL + AND length(`artifact_manifest_sha256`) = 64 + AND `artifact_manifest_sha256` = lower(`artifact_manifest_sha256`) + AND `artifact_manifest_sha256` NOT GLOB '*[^0-9a-f]*' + AND `semantic_hash` IS NOT NULL + AND `event_type` IN ('file.change.updated', 'file.changed', 'run.completed') + ) +); +--> statement-breakpoint +CREATE UNIQUE INDEX `session_event_artifact_attempt_idx` +ON `session_event` (`artifact_attempt_id`) +WHERE `artifact_attempt_id` IS NOT NULL; +--> statement-breakpoint +ALTER TABLE `session_event` +ADD `terminal_event_json` text +CHECK ( + (`terminal_event_json` IS NULL AND NOT (`semantic_hash` IS NOT NULL AND `event_type` IN ('run.cancelled', 'run.completed', 'run.failed'))) + OR (`terminal_event_json` IS NOT NULL AND json_valid(`terminal_event_json`) = 1 AND `semantic_hash` IS NOT NULL AND `event_type` IN ('run.cancelled', 'run.completed', 'run.failed')) +); +--> statement-breakpoint +ALTER TABLE `session_model_call` +ADD `source_event_seq` integer NOT NULL DEFAULT 0 +CHECK (`source_event_seq` >= 0); +--> statement-breakpoint +ALTER TABLE `usage_event` +ADD `source_event_seq` integer NOT NULL DEFAULT 0 +CHECK (`source_event_seq` >= 0); diff --git a/pkgs/db/drizzle/0017_terminal-reconciliation-scheduling.sql b/pkgs/db/drizzle/0017_terminal-reconciliation-scheduling.sql new file mode 100644 index 00000000..409b4d4a --- /dev/null +++ b/pkgs/db/drizzle/0017_terminal-reconciliation-scheduling.sql @@ -0,0 +1,12 @@ +ALTER TABLE `session_run` +ADD `terminal_reconciliation_attempted_at` integer +CHECK ( + `terminal_reconciliation_attempted_at` IS NULL + OR `terminal_reconciliation_attempted_at` >= 0 +); +--> statement-breakpoint +CREATE INDEX `session_run_terminal_reconciliation_attempt_idx` +ON `session_run` ( + COALESCE(`terminal_reconciliation_attempted_at`, `updated_at`), + `id` +); diff --git a/pkgs/db/drizzle/0018_runtime-operation-ready-authority.sql b/pkgs/db/drizzle/0018_runtime-operation-ready-authority.sql new file mode 100644 index 00000000..8f73d49d --- /dev/null +++ b/pkgs/db/drizzle/0018_runtime-operation-ready-authority.sql @@ -0,0 +1,14 @@ +ALTER TABLE `session_event` +ADD `runtime_operation_event_json` text +CONSTRAINT `session_event_runtime_operation_event_json_check` +CHECK ( + `runtime_operation_event_json` IS NULL + OR ( + json_valid(`runtime_operation_event_json`) = 1 + AND json_extract(`runtime_operation_event_json`, '$.kind') IS 'agent.task.updated' + AND json_type(`runtime_operation_event_json`, '$.payload') IS 'object' + AND json_extract(`runtime_operation_event_json`, '$.payload.status') IN ('updating', 'ready') + AND `semantic_hash` IS NOT NULL + AND `event_type` = 'agent.task.updated' + ) +); diff --git a/pkgs/db/drizzle/0019_runtime-subject-operation-authority.sql b/pkgs/db/drizzle/0019_runtime-subject-operation-authority.sql new file mode 100644 index 00000000..90ef5fc9 --- /dev/null +++ b/pkgs/db/drizzle/0019_runtime-subject-operation-authority.sql @@ -0,0 +1,1391 @@ +CREATE TABLE `__runtime_subject_authority_guard` ( + `assertion` text PRIMARY KEY NOT NULL, + `violation_count` integer NOT NULL, + CONSTRAINT "runtime_subject_authority_guard_check" CHECK (`violation_count` = 0) +); +--> statement-breakpoint +CREATE TABLE `__runtime_subject_identity` ( + `sandbox_id` text PRIMARY KEY NOT NULL, + `kind` text NOT NULL, + `subject_kind` text NOT NULL, + `subject_id` text NOT NULL, + `agent_id` text NOT NULL, + `app_id` text NOT NULL, + `owner_account_id` text NOT NULL +); +--> statement-breakpoint +INSERT INTO `__runtime_subject_identity` (`sandbox_id`, `kind`, `subject_kind`, `subject_id`, `agent_id`, `app_id`, `owner_account_id`) +SELECT `sandbox`.`id`, 'pet', 'agent', `sandbox`.`subject_id`, `agent`.`id`, `agent`.`app_id`, `agent`.`owner_account_id` +FROM `sandbox` +INNER JOIN `agent` + ON `sandbox`.`kind` = 'pet' + AND `sandbox`.`subject_kind` = 'agent' + AND `agent`.`id` = `sandbox`.`subject_id` + AND `agent`.`kind` = 'pet' +INNER JOIN `app` ON `app`.`id` = `agent`.`app_id` +UNION ALL +SELECT `sandbox`.`id`, 'cattle', 'session', `sandbox`.`subject_id`, `agent`.`id`, `session`.`app_id`, `agent`.`owner_account_id` +FROM `sandbox` +INNER JOIN `session` + ON `sandbox`.`kind` = 'cattle' + AND `sandbox`.`subject_kind` = 'session' + AND `session`.`id` = `sandbox`.`subject_id` + AND `session`.`kind` = 'cattle' +INNER JOIN `agent` + ON `agent`.`id` = `session`.`agent_id` + AND `agent`.`app_id` = `session`.`app_id` + AND `agent`.`kind` = 'cattle' +INNER JOIN `app` ON `app`.`id` = `session`.`app_id`; +--> statement-breakpoint +INSERT INTO `__runtime_subject_authority_guard` (`assertion`, `violation_count`) +SELECT 'active session runs', COUNT(*) +FROM `session_run` +WHERE `status` IN ('queued', 'booting', 'running', 'waiting_input'); +--> statement-breakpoint +INSERT INTO `__runtime_subject_authority_guard` (`assertion`, `violation_count`) +SELECT 'live driver instances', COUNT(*) +FROM `driver_instance` +WHERE `status` IN ('provisioning', 'connecting', 'ready', 'stopping'); +--> statement-breakpoint +INSERT INTO `__runtime_subject_authority_guard` (`assertion`, `violation_count`) +SELECT 'nonterminal driver commands', COUNT(*) +FROM `driver_command` +WHERE `status` IN ('queued', 'delivered', 'accepted'); +--> statement-breakpoint +INSERT INTO `__runtime_subject_authority_guard` (`assertion`, `violation_count`) +SELECT 'unsettled external tool effects', COUNT(*) +FROM `external_tool_effect` +WHERE `status` IN ('executing', 'claimed'); +--> statement-breakpoint +INSERT INTO `__runtime_subject_authority_guard` (`assertion`, `violation_count`) +SELECT 'nonterminal api commands', COUNT(*) +FROM `api_command` +WHERE `status` IN ('queued', 'running'); +--> statement-breakpoint +INSERT INTO `__runtime_subject_authority_guard` (`assertion`, `violation_count`) +SELECT 'legacy app deployment traffic without script authority', COUNT(*) +FROM `app_deployment` +WHERE `last_successful_url` IS NOT NULL; +--> statement-breakpoint +INSERT INTO `__runtime_subject_authority_guard` (`assertion`, `violation_count`) +SELECT 'active app deployment runs', COUNT(*) +FROM `app_deployment_run` +WHERE `status` IN ('queued', 'preparing', 'building', 'submitting', 'submitted', 'activating'); +--> statement-breakpoint +INSERT INTO `__runtime_subject_authority_guard` (`assertion`, `violation_count`) +SELECT 'nonstatic sandboxes', COUNT(*) +FROM `sandbox` +WHERE `status` <> 'cold' + OR `status_operation_id` IS NOT NULL + OR `claim_owner` IS NOT NULL + OR `claim_expires_at` IS NOT NULL; +--> statement-breakpoint +INSERT INTO `__runtime_subject_authority_guard` (`assertion`, `violation_count`) +SELECT 'nonstatic sessions', COUNT(*) +FROM `session` +WHERE `status` NOT IN ('IDLE', 'TERMINATED') + OR `status_operation_id` IS NOT NULL + OR NOT ( + `cleanup_operation_kind` IS NULL + OR ( + `cleanup_operation_kind` = 'archive' + AND `status` = 'IDLE' + AND `archived_at` IS NOT NULL + ) + ) + OR `runtime_provisioning_operation_id` IS NOT NULL + OR `runtime_provisioning_run_id` IS NOT NULL + OR `runtime_provisioning_sandbox_id` IS NOT NULL + OR `runtime_provisioning_heartbeat_at` IS NOT NULL; +--> statement-breakpoint +INSERT INTO `__runtime_subject_authority_guard` (`assertion`, `violation_count`) +SELECT 'nonstatic sandbox sessions', COUNT(*) +FROM `sandbox_session` +WHERE `status` NOT IN ('closed', 'error'); +--> statement-breakpoint +INSERT INTO `__runtime_subject_authority_guard` (`assertion`, `violation_count`) +SELECT 'invalid driver generations', COUNT(*) +FROM `driver_instance` +WHERE typeof(`generation`) <> 'integer' + OR `generation` NOT BETWEEN 0 AND 9007199254740991; +--> statement-breakpoint +INSERT INTO `__runtime_subject_authority_guard` (`assertion`, `violation_count`) +SELECT 'missing sandbox identity authority', COUNT(*) +FROM `sandbox` +LEFT JOIN `__runtime_subject_identity` AS `identity` ON `identity`.`sandbox_id` = `sandbox`.`id` +WHERE `identity`.`sandbox_id` IS NULL; +--> statement-breakpoint +INSERT INTO `__runtime_subject_authority_guard` (`assertion`, `violation_count`) +SELECT 'partial or mismatched sandbox identity', COUNT(*) +FROM `sandbox` +INNER JOIN `__runtime_subject_identity` AS `identity` ON `identity`.`sandbox_id` = `sandbox`.`id` +WHERE NOT ( + (`sandbox`.`agent_id` IS NULL AND `sandbox`.`app_id` IS NULL AND `sandbox`.`owner_account_id` IS NULL) + OR ( + `sandbox`.`agent_id` IS `identity`.`agent_id` + AND `sandbox`.`app_id` IS `identity`.`app_id` + AND `sandbox`.`owner_account_id` IS `identity`.`owner_account_id` + ) +); +--> statement-breakpoint +INSERT INTO `__runtime_subject_authority_guard` (`assertion`, `violation_count`) +SELECT 'invalid sandbox session authority', COUNT(*) +FROM `sandbox_session` +LEFT JOIN `__runtime_subject_identity` AS `identity` ON `identity`.`sandbox_id` = `sandbox_session`.`sandbox_id` +LEFT JOIN `session` ON `session`.`id` = `sandbox_session`.`session_id` +WHERE `identity`.`sandbox_id` IS NULL + OR `session`.`id` IS NULL + OR `session`.`kind` IS NOT `identity`.`kind` + OR `session`.`agent_id` IS NOT `identity`.`agent_id` + OR `session`.`app_id` IS NOT `identity`.`app_id` + OR (`identity`.`subject_kind` = 'session' AND `sandbox_session`.`session_id` IS NOT `identity`.`subject_id`) + OR (`identity`.`subject_kind` = 'agent' AND `session`.`agent_id` IS NOT `identity`.`subject_id`); +--> statement-breakpoint +INSERT INTO `__runtime_subject_authority_guard` (`assertion`, `violation_count`) +SELECT 'invalid sandbox backups', COUNT(*) +FROM `sandbox_backup` +WHERE `status` NOT IN ('ready', 'pruned') + OR `error_message` IS NOT NULL + OR typeof(`dir`) <> 'text' + OR length(`dir`) = 0 + OR typeof(`keep`) <> 'integer' + OR `keep` NOT IN (0, 1) + OR typeof(`ttl_seconds`) <> 'integer' + OR `ttl_seconds` NOT BETWEEN 1 AND 9007199254740991 + OR typeof(`created_at`) <> 'integer' + OR `created_at` NOT BETWEEN 0 AND 9007199254740991 + OR typeof(`updated_at`) <> 'integer' + OR `updated_at` NOT BETWEEN `created_at` AND 9007199254740991; +--> statement-breakpoint +INSERT INTO `__runtime_subject_authority_guard` (`assertion`, `violation_count`) +SELECT 'invalid terminal backup authority', COUNT(*) +FROM `sandbox_backup` +LEFT JOIN `__runtime_subject_identity` AS `identity` + ON `identity`.`sandbox_id` = `sandbox_backup`.`sandbox_id` +LEFT JOIN `session_run` ON `session_run`.`id` = `sandbox_backup`.`session_run_id` +LEFT JOIN `session` ON `session`.`id` = `session_run`.`session_id` +LEFT JOIN `sandbox_session` AS `workspace` + ON `workspace`.`session_id` = `session_run`.`session_id` + AND `workspace`.`sandbox_id` = `sandbox_backup`.`sandbox_id` + AND `workspace`.`cwd` = `sandbox_backup`.`dir` +WHERE `sandbox_backup`.`session_run_id` IS NOT NULL + AND ( + `identity`.`sandbox_id` IS NULL + OR `session_run`.`id` IS NULL + OR `session_run`.`status` IS NOT 'completed' + OR `session_run`.`agent_id` IS NOT `identity`.`agent_id` + OR `session`.`id` IS NULL + OR `session`.`kind` IS NOT `identity`.`kind` + OR `workspace`.`session_id` IS NULL + OR ( + `identity`.`subject_kind` = 'session' + AND `session`.`id` IS NOT `identity`.`subject_id` + ) + OR ( + `identity`.`subject_kind` = 'agent' + AND ( + `session`.`agent_id` IS NOT `identity`.`agent_id` + OR `session`.`app_id` IS NOT `identity`.`app_id` + ) + ) + ); +--> statement-breakpoint +INSERT INTO `__runtime_subject_authority_guard` (`assertion`, `violation_count`) +SELECT 'duplicate terminal backup authority', COUNT(*) +FROM ( + SELECT 1 + FROM `sandbox_backup` + WHERE `session_run_id` IS NOT NULL + GROUP BY `sandbox_id`, `dir`, `session_run_id` + HAVING COUNT(*) > 1 +); +--> statement-breakpoint +INSERT INTO `__runtime_subject_authority_guard` (`assertion`, `violation_count`) +SELECT 'invalid sandbox backup pointers', + (SELECT COUNT(*) + FROM `sandbox` + WHERE `last_backup_id` IS NOT NULL + AND NOT EXISTS ( + SELECT 1 + FROM `sandbox_backup` + WHERE `sandbox_backup`.`id` = `sandbox`.`last_backup_id` + AND `sandbox_backup`.`sandbox_id` = `sandbox`.`id` + AND `sandbox_backup`.`status` = 'ready' + )) + + + (SELECT COUNT(*) + FROM `sandbox` + WHERE `last_restore_backup_id` IS NOT NULL + AND NOT EXISTS ( + SELECT 1 + FROM `sandbox_backup` + WHERE `sandbox_backup`.`id` = `sandbox`.`last_restore_backup_id` + AND `sandbox_backup`.`sandbox_id` = `sandbox`.`id` + AND `sandbox_backup`.`status` IN ('ready', 'pruned') + )); +--> statement-breakpoint +INSERT INTO `__runtime_subject_authority_guard` (`assertion`, `violation_count`) +SELECT 'pre-migration foreign key violations', COUNT(*) +FROM pragma_foreign_key_check; +--> statement-breakpoint +CREATE TABLE `__new_app_deployment_run` ( + `app_id` text CHECK ("app_id" = upper("app_id") AND length("app_id") = 26 AND substr("app_id", 1, 1) GLOB '[0-7]' AND "app_id" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*') NOT NULL, + `created_at` integer NOT NULL, + `deployment_id` text CHECK ("deployment_id" = upper("deployment_id") AND length("deployment_id") = 26 AND substr("deployment_id", 1, 1) GLOB '[0-7]' AND "deployment_id" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*') NOT NULL, + `error_code` text, + `error_message` text, + `external_deployment_id` text, + `external_project_id` text, + `external_version_id` text, + `generated_wrangler_config_json` text, + `id` text CHECK ("id" = upper("id") AND length("id") = 26 AND substr("id", 1, 1) GLOB '[0-7]' AND "id" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*') PRIMARY KEY NOT NULL, + `mosoo_config_json` text, + `plan_json` text, + `source_branch` text NOT NULL, + `source_commit_sha` text NOT NULL, + `status` text NOT NULL, + `target_kind` text, + `target_project_name` text, + `target_script_name` text, + `updated_at` integer NOT NULL, + `url` text, + CONSTRAINT "app_deployment_run_status_check" CHECK("__new_app_deployment_run"."status" IN ('queued', 'preparing', 'building', 'submitting', 'submitted', 'activating', 'success', 'failed')), + CONSTRAINT "app_deployment_run_target_kind_check" CHECK("__new_app_deployment_run"."target_kind" IS NULL OR "__new_app_deployment_run"."target_kind" IN ('cloudflare_static_assets', 'cloudflare_worker')) +); +--> statement-breakpoint +INSERT INTO `__new_app_deployment_run` ( + `app_id`, `created_at`, `deployment_id`, `error_code`, `error_message`, + `external_deployment_id`, `external_project_id`, `external_version_id`, + `generated_wrangler_config_json`, `id`, `mosoo_config_json`, `plan_json`, + `source_branch`, `source_commit_sha`, `status`, `target_kind`, + `target_project_name`, `target_script_name`, `updated_at`, `url` +) +SELECT + `app_id`, `created_at`, `deployment_id`, `error_code`, `error_message`, + `external_deployment_id`, `external_project_id`, `external_version_id`, + `generated_wrangler_config_json`, `id`, `mosoo_config_json`, `plan_json`, + `source_branch`, `source_commit_sha`, `status`, + CASE `target_kind` + WHEN 'cloudflare_pages' THEN 'cloudflare_static_assets' + ELSE `target_kind` + END, + `target_project_name`, `target_script_name`, `updated_at`, `url` +FROM `app_deployment_run`; +--> statement-breakpoint +DROP TABLE `app_deployment_run`; +--> statement-breakpoint +ALTER TABLE `__new_app_deployment_run` RENAME TO `app_deployment_run`; +--> statement-breakpoint +CREATE INDEX `app_deployment_run_app_id_idx` ON `app_deployment_run` (`app_id`,`id`); +--> statement-breakpoint +CREATE INDEX `app_deployment_run_deployment_id_idx` ON `app_deployment_run` (`deployment_id`,`id`); +--> statement-breakpoint +CREATE UNIQUE INDEX `app_deployment_run_active_app_idx` ON `app_deployment_run` (`app_id`) WHERE "app_deployment_run"."status" IN ('queued', 'preparing', 'building', 'submitting', 'submitted', 'activating'); +--> statement-breakpoint +ALTER TABLE `app_deployment` ADD `active_script_name` text + CONSTRAINT "app_deployment_traffic_authority_check" + CHECK ( + (`active_script_name` IS NULL AND `last_successful_url` IS NULL) + OR ( + `deleted_at` IS NULL + AND typeof(`active_script_name`) = 'text' + AND length(`active_script_name`) > 0 + AND typeof(`last_successful_url`) = 'text' + AND length(`last_successful_url`) > 0 + ) + );--> statement-breakpoint +ALTER TABLE `api_command` ADD `delivery_generation` integer DEFAULT 1 NOT NULL + CONSTRAINT "api_command_delivery_generation_check" + CHECK ( + typeof(`delivery_generation`) = 'integer' + AND `delivery_generation` BETWEEN 1 AND 9007199254740991 + );--> statement-breakpoint +CREATE TABLE `app_deployment_script` ( + `attempt_count` integer NOT NULL, + `command_id` text CHECK ("command_id" = upper("command_id") AND length("command_id") = 26 AND substr("command_id", 1, 1) GLOB '[0-7]' AND "command_id" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*') NOT NULL, + `delivery_generation` integer NOT NULL, + `deployment_id` text CHECK ("deployment_id" = upper("deployment_id") AND length("deployment_id") = 26 AND substr("deployment_id", 1, 1) GLOB '[0-7]' AND "deployment_id" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*') NOT NULL, + `external_deleted_at` integer, + `last_reconciled_at` integer, + `next_reconcile_at` integer, + `reconcile_count` integer DEFAULT 0 NOT NULL, + `reconcile_expires_at` integer, + `reconcile_owner` text, + `registered_at` integer NOT NULL, + `registered_claim_owner` text NOT NULL, + `retire_after` integer, + `run_id` text CHECK ("run_id" = upper("run_id") AND length("run_id") = 26 AND substr("run_id", 1, 1) GLOB '[0-7]' AND "run_id" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*') NOT NULL, + `script_name` text PRIMARY KEY NOT NULL, + `upload_started_at` integer, + FOREIGN KEY (`command_id`) REFERENCES `api_command`(`id`) ON UPDATE no action ON DELETE restrict, + FOREIGN KEY (`deployment_id`) REFERENCES `app_deployment`(`id`) ON UPDATE no action ON DELETE restrict, + FOREIGN KEY (`run_id`) REFERENCES `app_deployment_run`(`id`) ON UPDATE no action ON DELETE restrict, + CONSTRAINT "app_deployment_script_attempt_check" CHECK(typeof("app_deployment_script"."attempt_count") = 'integer' AND "app_deployment_script"."attempt_count" BETWEEN 1 AND 9007199254740991), + CONSTRAINT "app_deployment_script_delivery_check" CHECK(typeof("app_deployment_script"."delivery_generation") = 'integer' AND "app_deployment_script"."delivery_generation" BETWEEN 1 AND 9007199254740991), + CONSTRAINT "app_deployment_script_name_check" CHECK(typeof("app_deployment_script"."script_name") = 'text' AND length("app_deployment_script"."script_name") BETWEEN 36 AND 63 AND substr("app_deployment_script"."script_name", 1, 4) = 'app-' AND "app_deployment_script"."script_name" NOT GLOB '*[^0-9a-z-]*' AND substr("app_deployment_script"."script_name", -1, 1) GLOB '[0-9a-z]'), + CONSTRAINT "app_deployment_script_registered_owner_check" CHECK(typeof("app_deployment_script"."registered_claim_owner") = 'text' AND length("app_deployment_script"."registered_claim_owner") > 0), + CONSTRAINT "app_deployment_script_reconcile_count_check" CHECK(typeof("app_deployment_script"."reconcile_count") = 'integer' AND "app_deployment_script"."reconcile_count" BETWEEN 0 AND 9007199254740991), + CONSTRAINT "app_deployment_script_reconcile_lease_check" CHECK(("app_deployment_script"."reconcile_owner" IS NULL AND "app_deployment_script"."reconcile_expires_at" IS NULL) OR (typeof("app_deployment_script"."reconcile_owner") = 'text' AND length("app_deployment_script"."reconcile_owner") > 0 AND typeof("app_deployment_script"."reconcile_expires_at") = 'integer' AND "app_deployment_script"."reconcile_expires_at" BETWEEN 0 AND 9007199254740991)), + CONSTRAINT "app_deployment_script_time_check" CHECK(typeof("app_deployment_script"."registered_at") = 'integer' AND "app_deployment_script"."registered_at" BETWEEN 0 AND 9007199254740991 AND ("app_deployment_script"."upload_started_at" IS NULL OR (typeof("app_deployment_script"."upload_started_at") = 'integer' AND "app_deployment_script"."upload_started_at" BETWEEN "app_deployment_script"."registered_at" AND 9007199254740991)) AND ("app_deployment_script"."retire_after" IS NULL OR (typeof("app_deployment_script"."retire_after") = 'integer' AND "app_deployment_script"."retire_after" BETWEEN "app_deployment_script"."registered_at" AND 9007199254740991)) AND ("app_deployment_script"."next_reconcile_at" IS NULL OR (typeof("app_deployment_script"."next_reconcile_at") = 'integer' AND "app_deployment_script"."next_reconcile_at" BETWEEN "app_deployment_script"."registered_at" AND 9007199254740991)) AND ("app_deployment_script"."last_reconciled_at" IS NULL OR (typeof("app_deployment_script"."last_reconciled_at") = 'integer' AND "app_deployment_script"."last_reconciled_at" BETWEEN "app_deployment_script"."registered_at" AND 9007199254740991)) AND ("app_deployment_script"."external_deleted_at" IS NULL OR ("app_deployment_script"."retire_after" IS NOT NULL AND typeof("app_deployment_script"."external_deleted_at") = 'integer' AND "app_deployment_script"."external_deleted_at" BETWEEN "app_deployment_script"."registered_at" AND 9007199254740991))) +); +--> statement-breakpoint +CREATE INDEX `app_deployment_script_reconcile_idx` ON `app_deployment_script` (`next_reconcile_at`,`script_name`);--> statement-breakpoint +CREATE TRIGGER `app_deployment_script_registration_authority` +BEFORE INSERT ON `app_deployment_script` +WHEN EXISTS ( + SELECT 1 FROM `app_deployment_script` + WHERE `script_name` = NEW.`script_name` + ) + OR NEW.`retire_after` IS NOT NULL + OR NEW.`upload_started_at` IS NOT NULL + OR NEW.`next_reconcile_at` IS NOT NEW.`registered_at` + 86400000 + OR NEW.`reconcile_count` <> 0 + OR NEW.`last_reconciled_at` IS NOT NULL + OR NEW.`external_deleted_at` IS NOT NULL + OR NEW.`reconcile_owner` IS NOT NULL + OR NEW.`reconcile_expires_at` IS NOT NULL + OR NOT EXISTS ( + SELECT 1 + FROM `api_command` AS `command` + INNER JOIN `app_deployment_run` AS `run` + ON `run`.`id` = NEW.`run_id` + AND `run`.`deployment_id` = NEW.`deployment_id` + AND `run`.`status` IN ('queued', 'preparing', 'building', 'submitting', 'submitted', 'activating') + INNER JOIN `app_deployment` AS `deployment` + ON `deployment`.`id` = NEW.`deployment_id` + AND `deployment`.`latest_run_id` = NEW.`run_id` + AND `deployment`.`deleted_at` IS NULL + WHERE `command`.`id` = NEW.`command_id` + AND `command`.`kind` = 'app_deployment_run_dispatch' + AND `command`.`status` = 'running' + AND `command`.`delivery_generation` = NEW.`delivery_generation` + AND `command`.`attempt_count` = NEW.`attempt_count` + AND `command`.`claim_owner` = NEW.`registered_claim_owner` + AND typeof(`command`.`claim_expires_at`) = 'integer' + AND `command`.`claim_expires_at` > unixepoch('subsec') * 1000 + AND json_valid(`command`.`payload_json`) = 1 + AND json_extract(`command`.`payload_json`, '$.appDeploymentRunId') = NEW.`run_id` + ) +BEGIN + SELECT RAISE(ABORT, 'app deployment script registration lacks command authority'); +END;--> statement-breakpoint +CREATE TRIGGER `app_deployment_script_register_target` +AFTER INSERT ON `app_deployment_script` +BEGIN + UPDATE `app_deployment_run` + SET `status` = 'preparing', + `target_script_name` = NEW.`script_name`, + `updated_at` = NEW.`registered_at` + WHERE `id` = NEW.`run_id` + AND `deployment_id` = NEW.`deployment_id` + AND `status` IN ('queued', 'preparing', 'building', 'submitting', 'submitted', 'activating') + AND EXISTS ( + SELECT 1 + FROM `app_deployment` + WHERE `id` = NEW.`deployment_id` + AND `latest_run_id` = NEW.`run_id` + AND `deleted_at` IS NULL + ); + SELECT CASE + WHEN changes() <> 1 + THEN RAISE(ABORT, 'app deployment script registration lost run authority') + END; +END;--> statement-breakpoint +CREATE TRIGGER `app_deployment_script_identity_monotonic` +BEFORE UPDATE OF `attempt_count`, `command_id`, `delivery_generation`, `deployment_id`, `external_deleted_at`, `registered_at`, `registered_claim_owner`, `retire_after`, `run_id`, `script_name`, `upload_started_at` ON `app_deployment_script` +WHEN NEW.`attempt_count` IS NOT OLD.`attempt_count` + OR NEW.`command_id` IS NOT OLD.`command_id` + OR NEW.`delivery_generation` IS NOT OLD.`delivery_generation` + OR NEW.`deployment_id` IS NOT OLD.`deployment_id` + OR NEW.`registered_at` IS NOT OLD.`registered_at` + OR NEW.`registered_claim_owner` IS NOT OLD.`registered_claim_owner` + OR NEW.`run_id` IS NOT OLD.`run_id` + OR NEW.`script_name` IS NOT OLD.`script_name` + OR (OLD.`upload_started_at` IS NOT NULL AND NEW.`upload_started_at` IS NOT OLD.`upload_started_at`) + OR (OLD.`retire_after` IS NOT NULL AND NEW.`retire_after` IS NOT OLD.`retire_after`) + OR (OLD.`external_deleted_at` IS NOT NULL AND NEW.`external_deleted_at` IS NOT OLD.`external_deleted_at`) +BEGIN + SELECT RAISE(ABORT, 'app deployment script identity or monotonic state changed'); +END;--> statement-breakpoint +CREATE TRIGGER `app_deployment_script_retire_authority` +BEFORE UPDATE OF `retire_after` ON `app_deployment_script` +WHEN OLD.`retire_after` IS NULL + AND NEW.`retire_after` IS NOT NULL + AND ( + NEW.`retire_after` IS NOT CAST(unixepoch('subsec') * 1000 AS INTEGER) + 86400000 + OR EXISTS ( + SELECT 1 + FROM `app_deployment` + WHERE `id` = OLD.`deployment_id` + AND `active_script_name` = OLD.`script_name` + ) + OR EXISTS ( + SELECT 1 + FROM `app_deployment_run` AS `run` + INNER JOIN `app_deployment` AS `deployment` + ON `deployment`.`id` = OLD.`deployment_id` + AND `deployment`.`latest_run_id` = OLD.`run_id` + AND `deployment`.`deleted_at` IS NULL + INNER JOIN `api_command` AS `command` + ON `command`.`id` = OLD.`command_id` + AND `command`.`kind` = 'app_deployment_run_dispatch' + AND `command`.`status` = 'running' + AND `command`.`delivery_generation` = OLD.`delivery_generation` + AND `command`.`attempt_count` = OLD.`attempt_count` + AND `command`.`claim_owner` = OLD.`registered_claim_owner` + AND typeof(`command`.`claim_expires_at`) = 'integer' + AND `command`.`claim_expires_at` > unixepoch('subsec') * 1000 + AND json_valid(`command`.`payload_json`) = 1 + AND json_extract(`command`.`payload_json`, '$.appDeploymentRunId') = OLD.`run_id` + WHERE `run`.`id` = OLD.`run_id` + AND `run`.`deployment_id` = OLD.`deployment_id` + AND `run`.`target_script_name` = OLD.`script_name` + AND `run`.`status` IN ('queued', 'preparing', 'building', 'submitting', 'submitted', 'activating') + ) + ) +BEGIN + SELECT RAISE(ABORT, 'app deployment script retirement lacks orphan authority'); +END;--> statement-breakpoint +CREATE TRIGGER `app_deployment_script_upload_authority` +BEFORE UPDATE OF `upload_started_at` ON `app_deployment_script` +WHEN OLD.`upload_started_at` IS NULL + AND NEW.`upload_started_at` IS NOT NULL + AND NOT EXISTS ( + SELECT 1 + FROM `api_command` AS `command` + INNER JOIN `app_deployment_run` AS `run` + ON `run`.`id` = OLD.`run_id` + AND `run`.`deployment_id` = OLD.`deployment_id` + AND `run`.`target_script_name` = OLD.`script_name` + AND `run`.`status` IN ('queued', 'preparing', 'building', 'submitting', 'submitted', 'activating') + INNER JOIN `app_deployment` AS `deployment` + ON `deployment`.`id` = OLD.`deployment_id` + AND `deployment`.`latest_run_id` = OLD.`run_id` + AND `deployment`.`deleted_at` IS NULL + WHERE OLD.`retire_after` IS NULL + AND `command`.`id` = OLD.`command_id` + AND `command`.`kind` = 'app_deployment_run_dispatch' + AND `command`.`status` = 'running' + AND `command`.`delivery_generation` = OLD.`delivery_generation` + AND `command`.`attempt_count` = OLD.`attempt_count` + AND `command`.`claim_owner` = OLD.`registered_claim_owner` + AND typeof(`command`.`claim_expires_at`) = 'integer' + AND `command`.`claim_expires_at` > unixepoch('subsec') * 1000 + AND json_valid(`command`.`payload_json`) = 1 + AND json_extract(`command`.`payload_json`, '$.appDeploymentRunId') = OLD.`run_id` + ) +BEGIN + SELECT RAISE(ABORT, 'app deployment script upload lacks command authority'); +END;--> statement-breakpoint +CREATE TRIGGER `app_deployment_script_delete_forbidden` +BEFORE DELETE ON `app_deployment_script` +BEGIN + SELECT RAISE(ABORT, 'app deployment script ledger is permanent'); +END;--> statement-breakpoint +CREATE TRIGGER `app_deployment_run_target_script_insert_authority` +BEFORE INSERT ON `app_deployment_run` +WHEN NEW.`target_script_name` IS NOT NULL + AND NOT EXISTS ( + SELECT 1 + FROM `app_deployment_script` AS `script` + INNER JOIN `api_command` AS `command` + ON `command`.`id` = `script`.`command_id` + AND `command`.`kind` = 'app_deployment_run_dispatch' + AND `command`.`status` = 'running' + AND `command`.`delivery_generation` = `script`.`delivery_generation` + AND `command`.`attempt_count` = `script`.`attempt_count` + AND `command`.`claim_owner` = `script`.`registered_claim_owner` + AND typeof(`command`.`claim_expires_at`) = 'integer' + AND `command`.`claim_expires_at` > unixepoch('subsec') * 1000 + AND json_valid(`command`.`payload_json`) = 1 + AND json_extract(`command`.`payload_json`, '$.appDeploymentRunId') = `script`.`run_id` + WHERE `script`.`script_name` = NEW.`target_script_name` + AND `script`.`deployment_id` = NEW.`deployment_id` + AND `script`.`run_id` = NEW.`id` + AND `script`.`retire_after` IS NULL + AND NEW.`status` IN ('queued', 'preparing', 'building', 'submitting', 'submitted', 'activating') + AND EXISTS ( + SELECT 1 + FROM `app_deployment` AS `deployment` + WHERE `deployment`.`id` = NEW.`deployment_id` + AND `deployment`.`latest_run_id` = NEW.`id` + AND `deployment`.`deleted_at` IS NULL + ) + ) +BEGIN + SELECT RAISE(ABORT, 'app deployment run target lacks live script authority'); +END;--> statement-breakpoint +CREATE TRIGGER `app_deployment_run_target_script_update_authority` +BEFORE UPDATE OF `deployment_id`, `target_script_name` ON `app_deployment_run` +WHEN NEW.`target_script_name` IS NOT NULL + AND NOT EXISTS ( + SELECT 1 + FROM `app_deployment_script` AS `script` + INNER JOIN `api_command` AS `command` + ON `command`.`id` = `script`.`command_id` + AND `command`.`kind` = 'app_deployment_run_dispatch' + AND `command`.`status` = 'running' + AND `command`.`delivery_generation` = `script`.`delivery_generation` + AND `command`.`attempt_count` = `script`.`attempt_count` + AND `command`.`claim_owner` = `script`.`registered_claim_owner` + AND typeof(`command`.`claim_expires_at`) = 'integer' + AND `command`.`claim_expires_at` > unixepoch('subsec') * 1000 + AND json_valid(`command`.`payload_json`) = 1 + AND json_extract(`command`.`payload_json`, '$.appDeploymentRunId') = `script`.`run_id` + WHERE `script`.`script_name` = NEW.`target_script_name` + AND `script`.`deployment_id` = NEW.`deployment_id` + AND `script`.`run_id` = NEW.`id` + AND `script`.`retire_after` IS NULL + AND NEW.`status` IN ('queued', 'preparing', 'building', 'submitting', 'submitted', 'activating') + AND EXISTS ( + SELECT 1 + FROM `app_deployment` AS `deployment` + WHERE `deployment`.`id` = NEW.`deployment_id` + AND `deployment`.`latest_run_id` = NEW.`id` + AND `deployment`.`deleted_at` IS NULL + ) + ) +BEGIN + SELECT RAISE(ABORT, 'app deployment run target lacks live script authority'); +END;--> statement-breakpoint +CREATE TRIGGER `app_deployment_run_target_script_retire` +AFTER UPDATE OF `target_script_name` ON `app_deployment_run` +WHEN OLD.`target_script_name` IS NOT NULL + AND OLD.`target_script_name` IS NOT NEW.`target_script_name` +BEGIN + UPDATE `app_deployment_script` + SET `retire_after` = CAST(unixepoch('subsec') * 1000 AS INTEGER) + 86400000, + `next_reconcile_at` = CAST(unixepoch('subsec') * 1000 AS INTEGER) + 86400000, + `reconcile_owner` = NULL, + `reconcile_expires_at` = NULL + WHERE `script_name` = OLD.`target_script_name` + AND `deployment_id` = OLD.`deployment_id` + AND `run_id` = OLD.`id` + AND `retire_after` IS NULL + AND NOT EXISTS ( + SELECT 1 + FROM `app_deployment` + WHERE `active_script_name` = OLD.`target_script_name` + ); +END;--> statement-breakpoint +CREATE TRIGGER `app_deployment_run_terminal_script_retire` +AFTER UPDATE OF `status` ON `app_deployment_run` +WHEN OLD.`status` IN ('queued', 'preparing', 'building', 'submitting', 'submitted', 'activating') + AND NEW.`status` IN ('success', 'failed') + AND NEW.`target_script_name` IS NOT NULL +BEGIN + UPDATE `app_deployment_script` + SET `retire_after` = CAST(unixepoch('subsec') * 1000 AS INTEGER) + 86400000, + `next_reconcile_at` = CAST(unixepoch('subsec') * 1000 AS INTEGER) + 86400000, + `reconcile_owner` = NULL, + `reconcile_expires_at` = NULL + WHERE `script_name` = NEW.`target_script_name` + AND `deployment_id` = NEW.`deployment_id` + AND `run_id` = NEW.`id` + AND `retire_after` IS NULL + AND NOT EXISTS ( + SELECT 1 + FROM `app_deployment` + WHERE `active_script_name` = NEW.`target_script_name` + ); +END;--> statement-breakpoint +CREATE TRIGGER `app_deployment_active_script_insert_authority` +BEFORE INSERT ON `app_deployment` +WHEN NEW.`active_script_name` IS NOT NULL + AND NOT EXISTS ( + SELECT 1 + FROM `app_deployment_script` AS `script` + INNER JOIN `app_deployment_run` AS `run` + ON `run`.`id` = `script`.`run_id` + AND `run`.`deployment_id` = `script`.`deployment_id` + AND `run`.`target_script_name` = `script`.`script_name` + AND `run`.`status` = 'activating' + INNER JOIN `api_command` AS `command` + ON `command`.`id` = `script`.`command_id` + AND `command`.`kind` = 'app_deployment_run_dispatch' + AND `command`.`status` = 'running' + AND `command`.`delivery_generation` = `script`.`delivery_generation` + AND `command`.`attempt_count` = `script`.`attempt_count` + AND `command`.`claim_owner` = `script`.`registered_claim_owner` + AND typeof(`command`.`claim_expires_at`) = 'integer' + AND `command`.`claim_expires_at` > unixepoch('subsec') * 1000 + AND json_valid(`command`.`payload_json`) = 1 + AND json_extract(`command`.`payload_json`, '$.appDeploymentRunId') = `script`.`run_id` + WHERE `script`.`script_name` = NEW.`active_script_name` + AND `script`.`deployment_id` = NEW.`id` + AND `script`.`run_id` = NEW.`latest_run_id` + AND `script`.`retire_after` IS NULL + AND `script`.`upload_started_at` IS NOT NULL + ) +BEGIN + SELECT RAISE(ABORT, 'app deployment active pointer lacks promotable script authority'); +END;--> statement-breakpoint +CREATE TRIGGER `app_deployment_active_script_update_authority` +BEFORE UPDATE OF `active_script_name`, `latest_run_id` ON `app_deployment` +WHEN NEW.`active_script_name` IS NOT NULL + AND OLD.`active_script_name` IS NOT NEW.`active_script_name` + AND NOT EXISTS ( + SELECT 1 + FROM `app_deployment_script` AS `script` + INNER JOIN `app_deployment_run` AS `run` + ON `run`.`id` = `script`.`run_id` + AND `run`.`deployment_id` = `script`.`deployment_id` + AND `run`.`target_script_name` = `script`.`script_name` + AND `run`.`status` = 'activating' + INNER JOIN `api_command` AS `command` + ON `command`.`id` = `script`.`command_id` + AND `command`.`kind` = 'app_deployment_run_dispatch' + AND `command`.`status` = 'running' + AND `command`.`delivery_generation` = `script`.`delivery_generation` + AND `command`.`attempt_count` = `script`.`attempt_count` + AND `command`.`claim_owner` = `script`.`registered_claim_owner` + AND typeof(`command`.`claim_expires_at`) = 'integer' + AND `command`.`claim_expires_at` > unixepoch('subsec') * 1000 + AND json_valid(`command`.`payload_json`) = 1 + AND json_extract(`command`.`payload_json`, '$.appDeploymentRunId') = `script`.`run_id` + WHERE `script`.`script_name` = NEW.`active_script_name` + AND `script`.`deployment_id` = NEW.`id` + AND `script`.`run_id` = NEW.`latest_run_id` + AND `script`.`retire_after` IS NULL + AND `script`.`upload_started_at` IS NOT NULL + ) +BEGIN + SELECT RAISE(ABORT, 'app deployment active pointer lacks promotable script authority'); +END;--> statement-breakpoint +CREATE TRIGGER `app_deployment_active_script_old_authority` +BEFORE UPDATE OF `active_script_name` ON `app_deployment` +WHEN OLD.`active_script_name` IS NOT NULL + AND NEW.`active_script_name` IS NOT NULL + AND OLD.`active_script_name` IS NOT NEW.`active_script_name` + AND NOT EXISTS ( + SELECT 1 + FROM `app_deployment_script` AS `script` + WHERE `script`.`script_name` = OLD.`active_script_name` + AND `script`.`deployment_id` = OLD.`id` + AND `script`.`retire_after` IS NULL + ) +BEGIN + SELECT RAISE(ABORT, 'app deployment active pointer references retired script authority'); +END;--> statement-breakpoint +CREATE TRIGGER `app_deployment_active_script_promote` +AFTER UPDATE OF `active_script_name` ON `app_deployment` +WHEN NEW.`active_script_name` IS NOT NULL + AND OLD.`active_script_name` IS NOT NEW.`active_script_name` +BEGIN + UPDATE `app_deployment_script` + SET `next_reconcile_at` = NULL, + `reconcile_owner` = NULL, + `reconcile_expires_at` = NULL + WHERE `script_name` = NEW.`active_script_name` + AND `deployment_id` = NEW.`id` + AND `retire_after` IS NULL; +END;--> statement-breakpoint +CREATE TRIGGER `app_deployment_active_script_retire` +AFTER UPDATE OF `active_script_name` ON `app_deployment` +WHEN OLD.`active_script_name` IS NOT NULL + AND OLD.`active_script_name` IS NOT NEW.`active_script_name` +BEGIN + UPDATE `app_deployment_script` + SET `retire_after` = CAST(unixepoch('subsec') * 1000 AS INTEGER) + 86400000, + `next_reconcile_at` = CAST(unixepoch('subsec') * 1000 AS INTEGER) + 86400000, + `reconcile_owner` = NULL, + `reconcile_expires_at` = NULL + WHERE `script_name` = OLD.`active_script_name` + AND `deployment_id` = OLD.`id` + AND `retire_after` IS NULL + AND NOT EXISTS ( + SELECT 1 + FROM `app_deployment` + WHERE `active_script_name` = OLD.`active_script_name` + ); +END;--> statement-breakpoint +CREATE TABLE `environment_package_artifact_backup_staging` ( + `actual_backup_id` text CHECK ("actual_backup_id" = upper("actual_backup_id") AND length("actual_backup_id") = 26 AND substr("actual_backup_id", 1, 1) GLOB '[0-7]' AND "actual_backup_id" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*'), + `app_id` text CHECK ("app_id" = upper("app_id") AND length("app_id") = 26 AND substr("app_id", 1, 1) GLOB '[0-7]' AND "app_id" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*') NOT NULL, + `attempt_count` integer NOT NULL, + `claim_owner` text NOT NULL, + `command_id` text CHECK ("command_id" = upper("command_id") AND length("command_id") = 26 AND substr("command_id", 1, 1) GLOB '[0-7]' AND "command_id" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*') PRIMARY KEY NOT NULL, + `created_at` integer NOT NULL, + `delivery_generation` integer NOT NULL, + `dir` text NOT NULL, + `input_digest` text NOT NULL, + `paths_json` text NOT NULL, + `updated_at` integer NOT NULL, + FOREIGN KEY (`command_id`) REFERENCES `api_command`(`id`) ON UPDATE no action ON DELETE restrict, + CONSTRAINT "environment_package_artifact_backup_staging_attempt_check" CHECK(typeof("environment_package_artifact_backup_staging"."attempt_count") = 'integer' AND "environment_package_artifact_backup_staging"."attempt_count" BETWEEN 1 AND 9007199254740991), + CONSTRAINT "environment_package_artifact_backup_staging_claim_owner_check" CHECK(typeof("environment_package_artifact_backup_staging"."claim_owner") = 'text' AND length("environment_package_artifact_backup_staging"."claim_owner") > 0), + CONSTRAINT "environment_package_artifact_backup_staging_delivery_check" CHECK(typeof("environment_package_artifact_backup_staging"."delivery_generation") = 'integer' AND "environment_package_artifact_backup_staging"."delivery_generation" BETWEEN 1 AND 9007199254740991), + CONSTRAINT "environment_package_artifact_backup_staging_digest_check" CHECK(length("environment_package_artifact_backup_staging"."input_digest") = 64 AND "environment_package_artifact_backup_staging"."input_digest" = lower("environment_package_artifact_backup_staging"."input_digest") AND "environment_package_artifact_backup_staging"."input_digest" NOT GLOB '*[^0-9a-f]*'), + CONSTRAINT "environment_package_artifact_backup_staging_dir_check" CHECK(typeof("environment_package_artifact_backup_staging"."dir") = 'text' AND length("environment_package_artifact_backup_staging"."dir") > 0), + CONSTRAINT "environment_package_artifact_backup_staging_paths_check" CHECK(json_valid("environment_package_artifact_backup_staging"."paths_json") = 1 AND json_type("environment_package_artifact_backup_staging"."paths_json") = 'object' AND json_type("environment_package_artifact_backup_staging"."paths_json", '$.executable') = 'array' AND json_type("environment_package_artifact_backup_staging"."paths_json", '$.node') = 'array' AND json_type("environment_package_artifact_backup_staging"."paths_json", '$.python') = 'array'), + CONSTRAINT "environment_package_artifact_backup_staging_time_check" CHECK(typeof("environment_package_artifact_backup_staging"."created_at") = 'integer' AND "environment_package_artifact_backup_staging"."created_at" BETWEEN 0 AND 9007199254740991 AND typeof("environment_package_artifact_backup_staging"."updated_at") = 'integer' AND "environment_package_artifact_backup_staging"."updated_at" BETWEEN "environment_package_artifact_backup_staging"."created_at" AND 9007199254740991) +); +--> statement-breakpoint +CREATE UNIQUE INDEX `environment_package_artifact_backup_staging_actual_idx` ON `environment_package_artifact_backup_staging` (`actual_backup_id`) WHERE "environment_package_artifact_backup_staging"."actual_backup_id" IS NOT NULL;--> statement-breakpoint +CREATE UNIQUE INDEX `environment_package_artifact_backup_staging_intent_idx` ON `environment_package_artifact_backup_staging` (`app_id`,`input_digest`);--> statement-breakpoint +CREATE INDEX `environment_package_artifact_backup_staging_updated_idx` ON `environment_package_artifact_backup_staging` (`updated_at`,`command_id`);--> statement-breakpoint +CREATE TRIGGER `environment_package_artifact_backup_staging_authority` +BEFORE INSERT ON `environment_package_artifact_backup_staging` +WHEN NOT EXISTS ( + SELECT 1 + FROM `api_command` AS `command` + WHERE `command`.`id` = NEW.`command_id` + AND `command`.`kind` = 'environment_package_artifact_build' + AND `command`.`status` = 'running' + AND `command`.`delivery_generation` = NEW.`delivery_generation` + AND `command`.`attempt_count` = NEW.`attempt_count` + AND `command`.`claim_owner` = NEW.`claim_owner` + AND typeof(`command`.`claim_expires_at`) = 'integer' + AND `command`.`claim_expires_at` > unixepoch('subsec') * 1000 + AND json_valid(`command`.`payload_json`) = 1 + AND json_extract(`command`.`payload_json`, '$.appId') = NEW.`app_id` + AND json_extract(`command`.`payload_json`, '$.inputDigest') = NEW.`input_digest` +) +BEGIN + SELECT RAISE(ABORT, 'environment artifact backup stage lacks command authority'); +END;--> statement-breakpoint +CREATE TRIGGER `environment_package_artifact_backup_staging_immutable` +BEFORE UPDATE OF `app_id`, `attempt_count`, `claim_owner`, `command_id`, `created_at`, `delivery_generation`, `dir`, `input_digest`, `paths_json` ON `environment_package_artifact_backup_staging` +BEGIN + SELECT RAISE(ABORT, 'environment artifact backup stage is immutable'); +END;--> statement-breakpoint +CREATE TABLE `sandbox_backup_staging` ( + `actual_backup_id` text CHECK ("actual_backup_id" = upper("actual_backup_id") AND length("actual_backup_id") = 26 AND substr("actual_backup_id", 1, 1) GLOB '[0-7]' AND "actual_backup_id" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*'), + `created_at` integer NOT NULL, + `dir` text NOT NULL, + `driver_generation` integer, + `driver_instance_id` text CHECK ("driver_instance_id" = upper("driver_instance_id") AND length("driver_instance_id") = 26 AND substr("driver_instance_id", 1, 1) GLOB '[0-7]' AND "driver_instance_id" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*'), + `id` text CHECK ("id" = upper("id") AND length("id") = 26 AND substr("id", 1, 1) GLOB '[0-7]' AND "id" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*') PRIMARY KEY NOT NULL, + `operation_id` text CHECK ("operation_id" = upper("operation_id") AND length("operation_id") = 26 AND substr("operation_id", 1, 1) GLOB '[0-7]' AND "operation_id" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*'), + `sandbox_id` text CHECK ("sandbox_id" = upper("sandbox_id") AND length("sandbox_id") = 26 AND substr("sandbox_id", 1, 1) GLOB '[0-7]' AND "sandbox_id" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*') NOT NULL, + `sandbox_incarnation` integer NOT NULL, + `session_run_id` text CHECK ("session_run_id" = upper("session_run_id") AND length("session_run_id") = 26 AND substr("session_run_id", 1, 1) GLOB '[0-7]' AND "session_run_id" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*'), + `ttl_seconds` integer NOT NULL, + `updated_at` integer NOT NULL, + `updates_subject_backup` integer DEFAULT false NOT NULL, + `workspace_session_id` text CHECK ("workspace_session_id" = upper("workspace_session_id") AND length("workspace_session_id") = 26 AND substr("workspace_session_id", 1, 1) GLOB '[0-7]' AND "workspace_session_id" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*'), + CONSTRAINT "sandbox_backup_staging_dir_check" CHECK(typeof("sandbox_backup_staging"."dir") = 'text' AND length("sandbox_backup_staging"."dir") > 0), + CONSTRAINT "sandbox_backup_staging_incarnation_check" CHECK(typeof("sandbox_backup_staging"."sandbox_incarnation") = 'integer' AND "sandbox_backup_staging"."sandbox_incarnation" BETWEEN 1 AND 9007199254740991), + CONSTRAINT "sandbox_backup_staging_ttl_check" CHECK(typeof("sandbox_backup_staging"."ttl_seconds") = 'integer' AND "sandbox_backup_staging"."ttl_seconds" BETWEEN 1 AND 9007199254740991), + CONSTRAINT "sandbox_backup_staging_timestamps_check" CHECK(typeof("sandbox_backup_staging"."created_at") = 'integer' AND "sandbox_backup_staging"."created_at" BETWEEN 0 AND 9007199254740991 AND typeof("sandbox_backup_staging"."updated_at") = 'integer' AND "sandbox_backup_staging"."updated_at" BETWEEN "sandbox_backup_staging"."created_at" AND 9007199254740991), + CONSTRAINT "sandbox_backup_staging_scope_check" CHECK((("sandbox_backup_staging"."operation_id" IS NOT NULL AND "sandbox_backup_staging"."session_run_id" IS NULL AND "sandbox_backup_staging"."driver_instance_id" IS NULL AND "sandbox_backup_staging"."driver_generation" IS NULL) OR ("sandbox_backup_staging"."operation_id" IS NULL AND "sandbox_backup_staging"."session_run_id" IS NOT NULL AND "sandbox_backup_staging"."workspace_session_id" IS NOT NULL AND "sandbox_backup_staging"."driver_instance_id" IS NOT NULL AND typeof("sandbox_backup_staging"."driver_generation") = 'integer' AND "sandbox_backup_staging"."driver_generation" BETWEEN 0 AND 9007199254740991)) AND ("sandbox_backup_staging"."updates_subject_backup" = false OR ("sandbox_backup_staging"."operation_id" IS NOT NULL AND "sandbox_backup_staging"."workspace_session_id" IS NULL))), + CONSTRAINT "sandbox_backup_staging_updates_subject_check" CHECK(typeof("sandbox_backup_staging"."updates_subject_backup") = 'integer' AND "sandbox_backup_staging"."updates_subject_backup" IN (false, true)) +); +--> statement-breakpoint +CREATE INDEX `sandbox_backup_staging_updated_idx` ON `sandbox_backup_staging` (`updated_at`,`id`);--> statement-breakpoint +CREATE UNIQUE INDEX `sandbox_backup_staging_actual_idx` ON `sandbox_backup_staging` (`actual_backup_id`) WHERE "sandbox_backup_staging"."actual_backup_id" IS NOT NULL;--> statement-breakpoint +CREATE UNIQUE INDEX `sandbox_backup_staging_terminal_checkpoint_idx` ON `sandbox_backup_staging` (`sandbox_id`,`sandbox_incarnation`,`dir`,`session_run_id`) WHERE "sandbox_backup_staging"."session_run_id" IS NOT NULL;--> statement-breakpoint +CREATE UNIQUE INDEX `sandbox_backup_staging_operation_checkpoint_idx` ON `sandbox_backup_staging` (`sandbox_id`,`sandbox_incarnation`,`operation_id`,`dir`) WHERE "sandbox_backup_staging"."operation_id" IS NOT NULL;--> statement-breakpoint +-- SQLite validates triggers on other tables while a referenced table is rebuilt. +-- The surrounding D1 migration transaction keeps admissions closed until the +-- canonical post-migration triggers are recreated at the end of this file. +DROP TRIGGER IF EXISTS "__protocol_v3_cutover_environment_artifact_backup_staging_insert"; +DROP TRIGGER IF EXISTS "__protocol_v3_cutover_sandbox_backup_staging_insert"; +DROP TRIGGER IF EXISTS "__protocol_v3_cutover_api_command_update"; +DROP TRIGGER IF EXISTS "__protocol_v3_cutover_api_command_insert"; +DROP TRIGGER IF EXISTS "__protocol_v3_cutover_session_update"; +DROP TRIGGER IF EXISTS "__protocol_v3_cutover_session_insert"; +DROP TRIGGER IF EXISTS "__protocol_v3_cutover_sandbox_backup_update"; +DROP TRIGGER IF EXISTS "__protocol_v3_cutover_sandbox_backup_insert"; +DROP TRIGGER IF EXISTS "__protocol_v3_cutover_sandbox_session_update"; +DROP TRIGGER IF EXISTS "__protocol_v3_cutover_sandbox_session_insert"; +DROP TRIGGER IF EXISTS "__protocol_v3_cutover_sandbox_update"; +DROP TRIGGER IF EXISTS "__protocol_v3_cutover_sandbox_insert"; +DROP TRIGGER IF EXISTS "__protocol_v3_cutover_command_insert"; +DROP TRIGGER IF EXISTS "__protocol_v3_cutover_driver_update"; +DROP TRIGGER IF EXISTS "__protocol_v3_cutover_driver_insert"; +DROP TRIGGER IF EXISTS "__protocol_v3_cutover_app_deployment_run_update"; +DROP TRIGGER IF EXISTS "__protocol_v3_cutover_app_deployment_run_insert"; +DROP TRIGGER IF EXISTS "__protocol_v3_cutover_session_run_update"; +DROP TRIGGER IF EXISTS "__protocol_v3_cutover_session_run_insert"; +--> statement-breakpoint +CREATE TABLE `__new_sandbox` ( + `agent_id` text CHECK ("agent_id" = upper("agent_id") AND length("agent_id") = 26 AND substr("agent_id", 1, 1) GLOB '[0-7]' AND "agent_id" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*') NOT NULL, + `app_id` text CHECK ("app_id" = upper("app_id") AND length("app_id") = 26 AND substr("app_id", 1, 1) GLOB '[0-7]' AND "app_id" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*') NOT NULL, + `bind_mount_ready` integer DEFAULT false NOT NULL, + `claim_expires_at` integer, + `claim_owner` text, + `created_at` integer NOT NULL, + `global_mounts_json` text DEFAULT '[]' NOT NULL, + `id` text CHECK ("id" = upper("id") AND length("id") = 26 AND substr("id", 1, 1) GLOB '[0-7]' AND "id" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*') PRIMARY KEY NOT NULL, + `inactive_deadline_at` integer, + `incarnation` integer DEFAULT 0 NOT NULL, + `kind` text NOT NULL, + `last_backup_id` text CHECK ("last_backup_id" = upper("last_backup_id") AND length("last_backup_id") = 26 AND substr("last_backup_id", 1, 1) GLOB '[0-7]' AND "last_backup_id" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*'), + `last_error` text, + `last_error_code` text, + `last_restore_backup_id` text CHECK ("last_restore_backup_id" = upper("last_restore_backup_id") AND length("last_restore_backup_id") = 26 AND substr("last_restore_backup_id", 1, 1) GLOB '[0-7]' AND "last_restore_backup_id" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*'), + `network_constraints_hash` text, + `owner_account_id` text CHECK ("owner_account_id" = upper("owner_account_id") AND length("owner_account_id") = 26 AND substr("owner_account_id", 1, 1) GLOB '[0-7]' AND "owner_account_id" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*') NOT NULL, + `operation_kind` text, + `status` text NOT NULL, + `status_changed_at` integer DEFAULT 0 NOT NULL, + `status_event` text DEFAULT 'runtime_subject.cold' NOT NULL, + `status_operation_id` text CHECK ("status_operation_id" = upper("status_operation_id") AND length("status_operation_id") = 26 AND substr("status_operation_id", 1, 1) GLOB '[0-7]' AND "status_operation_id" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*'), + `status_seq` integer DEFAULT 0 NOT NULL, + `status_source` text DEFAULT 'system' NOT NULL, + `subject_id` text CHECK ("subject_id" = upper("subject_id") AND length("subject_id") = 26 AND substr("subject_id", 1, 1) GLOB '[0-7]' AND "subject_id" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*') NOT NULL, + `subject_kind` text NOT NULL, + `updated_at` integer NOT NULL, + CONSTRAINT "sandbox_status_check" CHECK("__new_sandbox"."status" IN ('cold', 'restoring', 'active', 'backing_up', 'destroying')), + CONSTRAINT "sandbox_status_seq_check" CHECK("__new_sandbox"."status_seq" >= 0), + CONSTRAINT "sandbox_incarnation_check" CHECK(typeof("__new_sandbox"."incarnation") = 'integer' AND "__new_sandbox"."incarnation" BETWEEN 0 AND 9007199254740991 AND ("__new_sandbox"."status" = 'cold' OR "__new_sandbox"."incarnation" > 0)), + CONSTRAINT "sandbox_identity_check" CHECK(("__new_sandbox"."kind" = 'pet' AND "__new_sandbox"."subject_kind" = 'agent' AND "__new_sandbox"."subject_id" = "__new_sandbox"."agent_id") OR ("__new_sandbox"."kind" = 'cattle' AND "__new_sandbox"."subject_kind" = 'session')), + CONSTRAINT "sandbox_network_constraints_hash_check" CHECK(("__new_sandbox"."network_constraints_hash" IS NULL AND "__new_sandbox"."status" = 'cold') OR ("__new_sandbox"."network_constraints_hash" IS NOT NULL AND length("__new_sandbox"."network_constraints_hash") = 64 AND "__new_sandbox"."network_constraints_hash" = lower("__new_sandbox"."network_constraints_hash") AND "__new_sandbox"."network_constraints_hash" NOT GLOB '*[^0-9a-f]*')), + CONSTRAINT "sandbox_operation_state_check" CHECK(("__new_sandbox"."status" IN ('cold', 'active') AND "__new_sandbox"."operation_kind" IS NULL AND "__new_sandbox"."status_operation_id" IS NULL) OR ("__new_sandbox"."status" = 'restoring' AND "__new_sandbox"."operation_kind" = 'activate' AND "__new_sandbox"."status_operation_id" IS NOT NULL) OR ("__new_sandbox"."status" = 'backing_up' AND "__new_sandbox"."operation_kind" IN ('hibernate', 'recreate', 'reset') AND "__new_sandbox"."status_operation_id" IS NOT NULL) OR ("__new_sandbox"."status" = 'destroying' AND "__new_sandbox"."operation_kind" IN ('activate', 'hibernate', 'recreate', 'reset') AND "__new_sandbox"."status_operation_id" IS NOT NULL)), + CONSTRAINT "sandbox_claim_check" CHECK(("__new_sandbox"."claim_owner" IS NULL AND "__new_sandbox"."claim_expires_at" IS NULL) OR ("__new_sandbox"."claim_owner" IS NOT NULL AND typeof("__new_sandbox"."claim_expires_at") = 'integer' AND "__new_sandbox"."claim_expires_at" BETWEEN 0 AND 9007199254740991)), + CONSTRAINT "sandbox_operation_claim_check" CHECK("__new_sandbox"."status" IN ('cold', 'active') OR "__new_sandbox"."claim_owner" IS NOT NULL) +); +--> statement-breakpoint +INSERT INTO `__new_sandbox`( + "agent_id", "app_id", "bind_mount_ready", "claim_expires_at", "claim_owner", "created_at", "global_mounts_json", "id", "inactive_deadline_at", "incarnation", "kind", "last_backup_id", "last_error", "last_error_code", "last_restore_backup_id", "network_constraints_hash", "owner_account_id", "operation_kind", "status", "status_changed_at", "status_event", "status_operation_id", "status_seq", "status_source", "subject_id", "subject_kind", "updated_at" +) +SELECT + `identity`.`agent_id`, + `identity`.`app_id`, + `sandbox`.`bind_mount_ready`, + NULL, + NULL, + `sandbox`.`created_at`, + `sandbox`.`global_mounts_json`, + `sandbox`.`id`, + `sandbox`.`inactive_deadline_at`, + 0, + `identity`.`kind`, + `sandbox`.`last_backup_id`, + `sandbox`.`last_error`, + `sandbox`.`last_error_code`, + `sandbox`.`last_restore_backup_id`, + NULL, + `identity`.`owner_account_id`, + NULL, + 'cold', + `sandbox`.`status_changed_at`, + `sandbox`.`status_event`, + NULL, + `sandbox`.`status_seq`, + `sandbox`.`status_source`, + `identity`.`subject_id`, + `identity`.`subject_kind`, + `sandbox`.`updated_at` +FROM `sandbox` +INNER JOIN `__runtime_subject_identity` AS `identity` ON `identity`.`sandbox_id` = `sandbox`.`id`;--> statement-breakpoint +DROP TABLE `sandbox`;--> statement-breakpoint +ALTER TABLE `__new_sandbox` RENAME TO `sandbox`;--> statement-breakpoint +CREATE UNIQUE INDEX `sandbox_subject_idx` ON `sandbox` (`kind`,`subject_kind`,`subject_id`);--> statement-breakpoint +CREATE INDEX `sandbox_status_deadline_idx` ON `sandbox` (`status`,`inactive_deadline_at`,`updated_at`);--> statement-breakpoint +CREATE INDEX `sandbox_claim_idx` ON `sandbox` (`claim_expires_at`,`claim_owner`);--> statement-breakpoint +CREATE TABLE `__new_sandbox_backup` ( + `created_at` integer NOT NULL, + `dir` text NOT NULL, + `id` text CHECK ("id" = upper("id") AND length("id") = 26 AND substr("id", 1, 1) GLOB '[0-7]' AND "id" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*') PRIMARY KEY NOT NULL, + `keep` integer DEFAULT false NOT NULL, + `operation_id` text CHECK ("operation_id" = upper("operation_id") AND length("operation_id") = 26 AND substr("operation_id", 1, 1) GLOB '[0-7]' AND "operation_id" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*'), + `sandbox_id` text CHECK ("sandbox_id" = upper("sandbox_id") AND length("sandbox_id") = 26 AND substr("sandbox_id", 1, 1) GLOB '[0-7]' AND "sandbox_id" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*') NOT NULL, + `sandbox_incarnation` integer NOT NULL, + `session_run_id` text CHECK ("session_run_id" = upper("session_run_id") AND length("session_run_id") = 26 AND substr("session_run_id", 1, 1) GLOB '[0-7]' AND "session_run_id" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*'), + `staging_id` text CHECK ("staging_id" = upper("staging_id") AND length("staging_id") = 26 AND substr("staging_id", 1, 1) GLOB '[0-7]' AND "staging_id" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*') NOT NULL, + `status` text NOT NULL, + `ttl_seconds` integer NOT NULL, + `updated_at` integer NOT NULL, + `workspace_session_id` text CHECK ("workspace_session_id" = upper("workspace_session_id") AND length("workspace_session_id") = 26 AND substr("workspace_session_id", 1, 1) GLOB '[0-7]' AND "workspace_session_id" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*'), + CONSTRAINT "sandbox_backup_status_check" CHECK("__new_sandbox_backup"."status" IN ('ready', 'pruned')), + CONSTRAINT "sandbox_backup_dir_check" CHECK(typeof("__new_sandbox_backup"."dir") = 'text' AND length("__new_sandbox_backup"."dir") > 0), + CONSTRAINT "sandbox_backup_keep_check" CHECK(typeof("__new_sandbox_backup"."keep") = 'integer' AND "__new_sandbox_backup"."keep" IN (false, true)), + CONSTRAINT "sandbox_backup_incarnation_check" CHECK(typeof("__new_sandbox_backup"."sandbox_incarnation") = 'integer' AND "__new_sandbox_backup"."sandbox_incarnation" BETWEEN 0 AND 9007199254740991 AND ("__new_sandbox_backup"."sandbox_incarnation" > 0 OR "__new_sandbox_backup"."staging_id" = "__new_sandbox_backup"."id")), + CONSTRAINT "sandbox_backup_ttl_check" CHECK(typeof("__new_sandbox_backup"."ttl_seconds") = 'integer' AND "__new_sandbox_backup"."ttl_seconds" BETWEEN 1 AND 9007199254740991), + CONSTRAINT "sandbox_backup_timestamps_check" CHECK(typeof("__new_sandbox_backup"."created_at") = 'integer' AND "__new_sandbox_backup"."created_at" BETWEEN 0 AND 9007199254740991 AND typeof("__new_sandbox_backup"."updated_at") = 'integer' AND "__new_sandbox_backup"."updated_at" BETWEEN "__new_sandbox_backup"."created_at" AND 9007199254740991), + CONSTRAINT "sandbox_backup_scope_check" CHECK(("__new_sandbox_backup"."session_run_id" IS NULL OR "__new_sandbox_backup"."workspace_session_id" IS NOT NULL) AND (("__new_sandbox_backup"."operation_id" IS NOT NULL) <> ("__new_sandbox_backup"."session_run_id" IS NOT NULL) OR ("__new_sandbox_backup"."operation_id" IS NULL AND "__new_sandbox_backup"."session_run_id" IS NULL AND "__new_sandbox_backup"."workspace_session_id" IS NULL AND "__new_sandbox_backup"."staging_id" = "__new_sandbox_backup"."id" AND "__new_sandbox_backup"."sandbox_incarnation" = 0))) +); +--> statement-breakpoint +INSERT INTO `__new_sandbox_backup`( + "created_at", "dir", "id", "keep", "operation_id", "sandbox_id", "sandbox_incarnation", "session_run_id", "staging_id", "status", "ttl_seconds", "updated_at", "workspace_session_id" +) +SELECT + `sandbox_backup`.`created_at`, + `sandbox_backup`.`dir`, + `sandbox_backup`.`id`, + `sandbox_backup`.`keep`, + NULL, + `sandbox_backup`.`sandbox_id`, + 0, + `sandbox_backup`.`session_run_id`, + `sandbox_backup`.`id`, + `sandbox_backup`.`status`, + `sandbox_backup`.`ttl_seconds`, + `sandbox_backup`.`updated_at`, + `session_run`.`session_id` +FROM `sandbox_backup` +LEFT JOIN `session_run` ON `session_run`.`id` = `sandbox_backup`.`session_run_id`;--> statement-breakpoint +DROP TABLE `sandbox_backup`;--> statement-breakpoint +ALTER TABLE `__new_sandbox_backup` RENAME TO `sandbox_backup`;--> statement-breakpoint +CREATE INDEX `sandbox_backup_sandbox_status_dir_created_idx` ON `sandbox_backup` (`sandbox_id`,`status`,`dir`,`created_at`,`id`);--> statement-breakpoint +CREATE INDEX `sandbox_backup_workspace_status_updated_idx` ON `sandbox_backup` (`workspace_session_id`,`status`,`updated_at`,`id`);--> statement-breakpoint +CREATE UNIQUE INDEX `sandbox_backup_staging_idx` ON `sandbox_backup` (`staging_id`);--> statement-breakpoint +CREATE UNIQUE INDEX `sandbox_backup_terminal_checkpoint_idx` ON `sandbox_backup` (`sandbox_id`,`sandbox_incarnation`,`dir`,`session_run_id`) WHERE "sandbox_backup"."session_run_id" IS NOT NULL;--> statement-breakpoint +CREATE UNIQUE INDEX `sandbox_backup_operation_checkpoint_idx` ON `sandbox_backup` (`sandbox_id`,`sandbox_incarnation`,`operation_id`,`dir`) WHERE "sandbox_backup"."operation_id" IS NOT NULL;--> statement-breakpoint +ALTER TABLE `driver_instance` ADD `sandbox_incarnation` integer DEFAULT 0 NOT NULL + CONSTRAINT "driver_instance_generation_incarnation_check" + CHECK ( + typeof(`generation`) = 'integer' + AND `generation` BETWEEN 0 AND 9007199254740991 + AND typeof(`sandbox_incarnation`) = 'integer' + AND `sandbox_incarnation` BETWEEN 0 AND 9007199254740991 + AND (`status` IN ('stopped', 'failed') OR `sandbox_incarnation` > 0) + );--> statement-breakpoint +DROP INDEX `driver_instance_sandbox_session_idx`;--> statement-breakpoint +DROP INDEX `driver_instance_live_sandbox_session_idx`;--> statement-breakpoint +CREATE INDEX `driver_instance_sandbox_session_idx` ON `driver_instance` (`sandbox_id`,`sandbox_incarnation`,`sandbox_session_id`,`status`,`updated_at`);--> statement-breakpoint +CREATE UNIQUE INDEX `driver_instance_live_sandbox_session_idx` ON `driver_instance` (`sandbox_id`,`sandbox_incarnation`,`sandbox_session_id`) WHERE "driver_instance"."status" IN ('provisioning', 'connecting', 'ready', 'stopping');--> statement-breakpoint +ALTER TABLE `sandbox_session` ADD `cleanup_operation_id` text + CHECK ("cleanup_operation_id" = upper("cleanup_operation_id") AND length("cleanup_operation_id") = 26 AND substr("cleanup_operation_id", 1, 1) GLOB '[0-7]' AND "cleanup_operation_id" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*') + CONSTRAINT "sandbox_session_cleanup_check" + CHECK ((`status` = 'cleanup_pending' AND `cleanup_operation_id` IS NOT NULL) OR (`status` <> 'cleanup_pending' AND `cleanup_operation_id` IS NULL));--> statement-breakpoint +ALTER TABLE `sandbox_session` ADD `sandbox_incarnation` integer DEFAULT 0 NOT NULL + CONSTRAINT "sandbox_session_status_incarnation_check" + CHECK ( + `status` IN ('active', 'cleanup_pending', 'closed', 'error') + AND typeof(`sandbox_incarnation`) = 'integer' + AND `sandbox_incarnation` BETWEEN 0 AND 9007199254740991 + AND (`status` IN ('closed', 'error') OR `sandbox_incarnation` > 0) + );--> statement-breakpoint +CREATE INDEX `sandbox_session_status_updated_idx` ON `sandbox_session` (`status`,`updated_at`,`session_id`);--> statement-breakpoint +ALTER TABLE `session` ADD `runtime_provisioning_sandbox_session_id` text + CHECK ("runtime_provisioning_sandbox_session_id" = upper("runtime_provisioning_sandbox_session_id") AND length("runtime_provisioning_sandbox_session_id") = 26 AND substr("runtime_provisioning_sandbox_session_id", 1, 1) GLOB '[0-7]' AND "runtime_provisioning_sandbox_session_id" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*');--> statement-breakpoint +ALTER TABLE `session` ADD `runtime_provisioning_sandbox_incarnation` integer + CONSTRAINT "session_runtime_provisioning_sandbox_pair_check" + CHECK ( + (`runtime_provisioning_sandbox_session_id` IS NULL AND `runtime_provisioning_sandbox_incarnation` IS NULL) + OR ( + `runtime_provisioning_operation_id` IS NOT NULL + AND typeof(`runtime_provisioning_sandbox_incarnation`) = 'integer' + AND `runtime_provisioning_sandbox_incarnation` BETWEEN 0 AND 9007199254740991 + ) + );--> statement-breakpoint +CREATE UNIQUE INDEX `session_runtime_provisioning_sandbox_idx` ON `session` (`runtime_provisioning_sandbox_id`) WHERE "session"."runtime_provisioning_operation_id" IS NOT NULL;--> statement-breakpoint +CREATE TRIGGER `sandbox_identity_immutable` +BEFORE UPDATE OF `id`, `kind`, `subject_kind`, `subject_id`, `agent_id`, `app_id`, `owner_account_id` ON `sandbox` +WHEN NEW.`id` IS NOT OLD.`id` + OR NEW.`kind` IS NOT OLD.`kind` + OR NEW.`subject_kind` IS NOT OLD.`subject_kind` + OR NEW.`subject_id` IS NOT OLD.`subject_id` + OR NEW.`agent_id` IS NOT OLD.`agent_id` + OR NEW.`app_id` IS NOT OLD.`app_id` + OR NEW.`owner_account_id` IS NOT OLD.`owner_account_id` +BEGIN + SELECT RAISE(ABORT, 'sandbox identity is immutable'); +END;--> statement-breakpoint +INSERT INTO `__runtime_subject_authority_guard` (`assertion`, `violation_count`) +SELECT 'post-migration foreign key violations', COUNT(*) +FROM pragma_foreign_key_check;--> statement-breakpoint +DROP TABLE `__runtime_subject_identity`;--> statement-breakpoint +DROP TABLE `__runtime_subject_authority_guard`; +--> statement-breakpoint +CREATE TABLE IF NOT EXISTS "__protocol_v3_cutover" ( + "id" integer PRIMARY KEY CHECK ("id" = 1), + "command_freeze" integer NOT NULL DEFAULT 0 CHECK ("command_freeze" IN (0, 1)), + "enabled" integer NOT NULL DEFAULT 1 CHECK ("enabled" IN (0, 1)), + "phase" text NOT NULL DEFAULT 'draining' CHECK ("phase" IN ('draining', 'queues_resuming')), + "pre_migration_bookmark" text, + "release_tree_oid" text NOT NULL CHECK ((length("release_tree_oid") = 40 OR length("release_tree_oid") = 64) AND "release_tree_oid" = lower("release_tree_oid") AND "release_tree_oid" NOT GLOB '*[^0-9a-f]*'), + "smoke_account_id" text, + "smoke_request_key" text, + "smoke_session_id" text, + "target_container_application_version" integer, + "target_container_image_digest" text, + "target_worker_version_id" text, + "started_at" integer NOT NULL DEFAULT (CAST(unixepoch('subsec') * 1000 AS INTEGER)), + CONSTRAINT "protocol_v3_cutover_phase_check" CHECK ( + ("phase" = 'draining' AND "enabled" = 1) + OR ("phase" = 'queues_resuming' AND "command_freeze" = 1) + ), + CONSTRAINT "protocol_v3_cutover_rollout_check" CHECK ( + ("target_container_application_version" IS NULL AND "target_container_image_digest" IS NULL AND "target_worker_version_id" IS NULL) + OR ("target_container_application_version" >= 0 AND length("target_container_image_digest") = 64 AND "target_container_image_digest" = lower("target_container_image_digest") AND "target_container_image_digest" NOT GLOB '*[^0-9a-f]*' AND length(trim("target_worker_version_id")) > 0) + ) +); +CREATE TRIGGER IF NOT EXISTS "__protocol_v3_cutover_session_run_insert" +BEFORE INSERT ON "session_run" +WHEN NEW."status" IN ('queued', 'booting', 'running', 'waiting_input') + AND EXISTS (SELECT 1 FROM "__protocol_v3_cutover" WHERE "enabled" = 1) + AND NOT EXISTS ( + SELECT 1 + FROM "__protocol_v3_cutover" AS "gate" + INNER JOIN "session" AS "smoke_session" + ON "smoke_session"."id" = NEW."session_id" + AND "smoke_session"."creator_account_id" = "gate"."smoke_account_id" + AND "smoke_session"."end_user_id" = "gate"."smoke_request_key" + WHERE "gate"."enabled" = 1 + AND "gate"."smoke_request_key" IS NOT NULL + AND ("gate"."smoke_session_id" IS NULL OR "gate"."smoke_session_id" = "smoke_session"."id") + ) +BEGIN + SELECT RAISE(ABORT, 'protocol v3 cutover blocks new active Session Runs'); +END; +CREATE TRIGGER IF NOT EXISTS "__protocol_v3_cutover_session_run_update" +BEFORE UPDATE OF "status" ON "session_run" +WHEN NEW."status" IN ('queued', 'booting', 'running', 'waiting_input') + AND OLD."status" NOT IN ('queued', 'booting', 'running', 'waiting_input') + AND EXISTS (SELECT 1 FROM "__protocol_v3_cutover" WHERE "enabled" = 1) + AND NOT EXISTS ( + SELECT 1 + FROM "__protocol_v3_cutover" AS "gate" + INNER JOIN "session" AS "smoke_session" + ON "smoke_session"."id" = NEW."session_id" + AND "smoke_session"."creator_account_id" = "gate"."smoke_account_id" + AND "smoke_session"."end_user_id" = "gate"."smoke_request_key" + WHERE "gate"."enabled" = 1 + AND "gate"."smoke_request_key" IS NOT NULL + AND ("gate"."smoke_session_id" IS NULL OR "gate"."smoke_session_id" = "smoke_session"."id") + ) +BEGIN + SELECT RAISE(ABORT, 'protocol v3 cutover blocks Session Run reactivation'); +END; +CREATE TRIGGER IF NOT EXISTS "__protocol_v3_cutover_app_deployment_run_insert" +BEFORE INSERT ON "app_deployment_run" +WHEN NEW."status" IN ('queued', 'preparing', 'building', 'submitting', 'submitted', 'activating') + AND EXISTS (SELECT 1 FROM "__protocol_v3_cutover" WHERE "enabled" = 1) +BEGIN + SELECT RAISE(ABORT, 'protocol v3 cutover blocks new active App deployment Runs'); +END; +CREATE TRIGGER IF NOT EXISTS "__protocol_v3_cutover_app_deployment_run_update" +BEFORE UPDATE OF "status" ON "app_deployment_run" +WHEN NEW."status" IN ('queued', 'preparing', 'building', 'submitting', 'submitted', 'activating') + AND OLD."status" NOT IN ('queued', 'preparing', 'building', 'submitting', 'submitted', 'activating') + AND EXISTS (SELECT 1 FROM "__protocol_v3_cutover" WHERE "enabled" = 1) +BEGIN + SELECT RAISE(ABORT, 'protocol v3 cutover blocks App deployment Run reactivation'); +END; +CREATE TRIGGER IF NOT EXISTS "__protocol_v3_cutover_driver_insert" +BEFORE INSERT ON "driver_instance" +WHEN NEW."status" IN ('provisioning', 'connecting', 'ready', 'stopping') + AND EXISTS (SELECT 1 FROM "__protocol_v3_cutover" WHERE "enabled" = 1) + AND NOT EXISTS ( + SELECT 1 + FROM "__protocol_v3_cutover" AS "gate" + INNER JOIN "session" AS "smoke_session" + ON "smoke_session"."id" = NEW."sandbox_session_id" + AND "smoke_session"."creator_account_id" = "gate"."smoke_account_id" + AND "smoke_session"."end_user_id" = "gate"."smoke_request_key" + INNER JOIN "sandbox" AS "smoke_sandbox" + ON "smoke_sandbox"."id" = NEW."sandbox_id" + AND "smoke_sandbox"."subject_kind" = 'session' + AND "smoke_sandbox"."subject_id" = "smoke_session"."id" + WHERE "gate"."enabled" = 1 + AND "gate"."smoke_request_key" IS NOT NULL + AND ("gate"."smoke_session_id" IS NULL OR "gate"."smoke_session_id" = "smoke_session"."id") + ) +BEGIN + SELECT RAISE(ABORT, 'protocol v3 cutover blocks new live Driver instances'); +END; +CREATE TRIGGER IF NOT EXISTS "__protocol_v3_cutover_driver_update" +BEFORE UPDATE OF "status" ON "driver_instance" +WHEN NEW."status" IN ('provisioning', 'connecting', 'ready', 'stopping') + AND OLD."status" NOT IN ('provisioning', 'connecting', 'ready', 'stopping') + AND EXISTS (SELECT 1 FROM "__protocol_v3_cutover" WHERE "enabled" = 1) + AND NOT EXISTS ( + SELECT 1 + FROM "__protocol_v3_cutover" AS "gate" + INNER JOIN "session" AS "smoke_session" + ON "smoke_session"."id" = NEW."sandbox_session_id" + AND "smoke_session"."creator_account_id" = "gate"."smoke_account_id" + AND "smoke_session"."end_user_id" = "gate"."smoke_request_key" + INNER JOIN "sandbox" AS "smoke_sandbox" + ON "smoke_sandbox"."id" = NEW."sandbox_id" + AND "smoke_sandbox"."subject_kind" = 'session' + AND "smoke_sandbox"."subject_id" = "smoke_session"."id" + WHERE "gate"."enabled" = 1 + AND "gate"."smoke_request_key" IS NOT NULL + AND ("gate"."smoke_session_id" IS NULL OR "gate"."smoke_session_id" = "smoke_session"."id") + ) +BEGIN + SELECT RAISE(ABORT, 'protocol v3 cutover blocks Driver reactivation'); +END; +CREATE TRIGGER IF NOT EXISTS "__protocol_v3_cutover_command_insert" +BEFORE INSERT ON "driver_command" +WHEN EXISTS ( + SELECT 1 FROM "__protocol_v3_cutover" + WHERE "enabled" = 1 + AND ("command_freeze" = 1 OR NEW."kind" IN ('input.start', 'mcp.execute')) + ) + AND NOT ( + NEW."kind" = 'session.stop' + AND EXISTS ( + SELECT 1 + FROM "driver_instance" AS "smoke_driver" + WHERE "smoke_driver"."id" = NEW."driver_instance_id" + AND EXISTS ( + SELECT 1 + FROM "__protocol_v3_cutover" AS "gate" + INNER JOIN "session" AS "smoke_session" + ON "smoke_session"."id" = "smoke_driver"."sandbox_session_id" + AND "smoke_session"."creator_account_id" = "gate"."smoke_account_id" + AND "smoke_session"."end_user_id" = "gate"."smoke_request_key" + INNER JOIN "sandbox" AS "smoke_sandbox" + ON "smoke_sandbox"."id" = "smoke_driver"."sandbox_id" + AND "smoke_sandbox"."subject_kind" = 'session' + AND "smoke_sandbox"."subject_id" = "smoke_session"."id" + WHERE "gate"."enabled" = 1 + AND "gate"."smoke_request_key" IS NOT NULL + AND ("gate"."smoke_session_id" IS NULL OR "gate"."smoke_session_id" = "smoke_session"."id") + ) + ) + ) +BEGIN + SELECT RAISE(ABORT, 'protocol v3 cutover blocks new Driver commands'); +END; +CREATE TRIGGER IF NOT EXISTS "__protocol_v3_cutover_sandbox_insert" +BEFORE INSERT ON "sandbox" +WHEN (NEW."status" <> 'cold' + OR NEW."operation_kind" IS NOT NULL + OR NEW."status_operation_id" IS NOT NULL + OR NEW."claim_owner" IS NOT NULL + OR NEW."claim_expires_at" IS NOT NULL) + AND EXISTS (SELECT 1 FROM "__protocol_v3_cutover" WHERE "enabled" = 1) + AND NOT (NEW."subject_kind" = 'session' AND EXISTS ( + SELECT 1 + FROM "__protocol_v3_cutover" AS "gate" + INNER JOIN "session" AS "smoke_session" + ON "smoke_session"."id" = NEW."subject_id" + AND "smoke_session"."creator_account_id" = "gate"."smoke_account_id" + AND "smoke_session"."end_user_id" = "gate"."smoke_request_key" + WHERE "gate"."enabled" = 1 + AND "gate"."smoke_request_key" IS NOT NULL + AND ("gate"."smoke_session_id" IS NULL OR "gate"."smoke_session_id" = "smoke_session"."id") + )) +BEGIN + SELECT RAISE(ABORT, 'protocol v3 cutover blocks new active sandboxes'); +END; +CREATE TRIGGER IF NOT EXISTS "__protocol_v3_cutover_sandbox_update" +BEFORE UPDATE OF "status", "operation_kind", "status_operation_id", "claim_owner", "claim_expires_at" ON "sandbox" +WHEN OLD."status" = 'cold' + AND OLD."operation_kind" IS NULL + AND OLD."status_operation_id" IS NULL + AND OLD."claim_owner" IS NULL + AND OLD."claim_expires_at" IS NULL + AND (NEW."status" <> 'cold' + OR NEW."operation_kind" IS NOT NULL + OR NEW."status_operation_id" IS NOT NULL + OR NEW."claim_owner" IS NOT NULL + OR NEW."claim_expires_at" IS NOT NULL) + AND EXISTS (SELECT 1 FROM "__protocol_v3_cutover" WHERE "enabled" = 1) + AND NOT (NEW."subject_kind" = 'session' AND EXISTS ( + SELECT 1 + FROM "__protocol_v3_cutover" AS "gate" + INNER JOIN "session" AS "smoke_session" + ON "smoke_session"."id" = NEW."subject_id" + AND "smoke_session"."creator_account_id" = "gate"."smoke_account_id" + AND "smoke_session"."end_user_id" = "gate"."smoke_request_key" + WHERE "gate"."enabled" = 1 + AND "gate"."smoke_request_key" IS NOT NULL + AND ("gate"."smoke_session_id" IS NULL OR "gate"."smoke_session_id" = "smoke_session"."id") + )) +BEGIN + SELECT RAISE(ABORT, 'protocol v3 cutover blocks sandbox activation'); +END; +CREATE TRIGGER IF NOT EXISTS "__protocol_v3_cutover_sandbox_session_insert" +BEFORE INSERT ON "sandbox_session" +WHEN NEW."status" NOT IN ('closed', 'error') + AND EXISTS (SELECT 1 FROM "__protocol_v3_cutover" WHERE "enabled" = 1) + AND NOT EXISTS ( + SELECT 1 + FROM "__protocol_v3_cutover" AS "gate" + INNER JOIN "session" AS "smoke_session" + ON "smoke_session"."id" = NEW."session_id" + AND "smoke_session"."creator_account_id" = "gate"."smoke_account_id" + AND "smoke_session"."end_user_id" = "gate"."smoke_request_key" + INNER JOIN "sandbox" AS "smoke_sandbox" + ON "smoke_sandbox"."id" = NEW."sandbox_id" + AND "smoke_sandbox"."subject_kind" = 'session' + AND "smoke_sandbox"."subject_id" = "smoke_session"."id" + WHERE "gate"."enabled" = 1 + AND "gate"."smoke_request_key" IS NOT NULL + AND ("gate"."smoke_session_id" IS NULL OR "gate"."smoke_session_id" = "smoke_session"."id") + ) +BEGIN + SELECT RAISE(ABORT, 'protocol v3 cutover blocks new active sandbox Sessions'); +END; +CREATE TRIGGER IF NOT EXISTS "__protocol_v3_cutover_sandbox_session_update" +BEFORE UPDATE OF "status" ON "sandbox_session" +WHEN OLD."status" IN ('closed', 'error') + AND NEW."status" NOT IN ('closed', 'error') + AND EXISTS (SELECT 1 FROM "__protocol_v3_cutover" WHERE "enabled" = 1) + AND NOT EXISTS ( + SELECT 1 + FROM "__protocol_v3_cutover" AS "gate" + INNER JOIN "session" AS "smoke_session" + ON "smoke_session"."id" = NEW."session_id" + AND "smoke_session"."creator_account_id" = "gate"."smoke_account_id" + AND "smoke_session"."end_user_id" = "gate"."smoke_request_key" + INNER JOIN "sandbox" AS "smoke_sandbox" + ON "smoke_sandbox"."id" = NEW."sandbox_id" + AND "smoke_sandbox"."subject_kind" = 'session' + AND "smoke_sandbox"."subject_id" = "smoke_session"."id" + WHERE "gate"."enabled" = 1 + AND "gate"."smoke_request_key" IS NOT NULL + AND ("gate"."smoke_session_id" IS NULL OR "gate"."smoke_session_id" = "smoke_session"."id") + ) +BEGIN + SELECT RAISE(ABORT, 'protocol v3 cutover blocks sandbox Session reactivation'); +END; +CREATE TRIGGER IF NOT EXISTS "__protocol_v3_cutover_sandbox_backup_insert" +BEFORE INSERT ON "sandbox_backup" +WHEN NEW."status" NOT IN ('ready', 'pruned') + AND EXISTS (SELECT 1 FROM "__protocol_v3_cutover" WHERE "enabled" = 1) +BEGIN + SELECT RAISE(ABORT, 'protocol v3 cutover blocks new sandbox backup work'); +END; +CREATE TRIGGER IF NOT EXISTS "__protocol_v3_cutover_sandbox_backup_update" +BEFORE UPDATE OF "status" ON "sandbox_backup" +WHEN OLD."status" IN ('ready', 'pruned') + AND NEW."status" NOT IN ('ready', 'pruned') + AND EXISTS (SELECT 1 FROM "__protocol_v3_cutover" WHERE "enabled" = 1) +BEGIN + SELECT RAISE(ABORT, 'protocol v3 cutover blocks sandbox backup reactivation'); +END; +CREATE TRIGGER IF NOT EXISTS "__protocol_v3_cutover_session_insert" +BEFORE INSERT ON "session" +WHEN NOT (NEW."status" IN ('IDLE', 'TERMINATED') + AND NEW."status_operation_id" IS NULL + AND (NEW."cleanup_operation_kind" IS NULL + OR (NEW."cleanup_operation_kind" = 'archive' + AND NEW."status" = 'IDLE' + AND NEW."archived_at" IS NOT NULL)) + AND NEW."runtime_provisioning_operation_id" IS NULL + AND NEW."runtime_provisioning_run_id" IS NULL + AND NEW."runtime_provisioning_sandbox_id" IS NULL + AND NEW."runtime_provisioning_sandbox_session_id" IS NULL + AND NEW."runtime_provisioning_sandbox_incarnation" IS NULL + AND NEW."runtime_provisioning_heartbeat_at" IS NULL) + AND EXISTS (SELECT 1 FROM "__protocol_v3_cutover" WHERE "enabled" = 1) + AND NOT EXISTS ( + SELECT 1 + FROM "__protocol_v3_cutover" AS "gate" + WHERE "gate"."enabled" = 1 + AND "gate"."smoke_request_key" IS NOT NULL + AND "gate"."smoke_session_id" IS NULL + AND "gate"."smoke_account_id" = NEW."creator_account_id" + AND "gate"."smoke_request_key" = NEW."end_user_id" + ) +BEGIN + SELECT RAISE(ABORT, 'protocol v3 cutover blocks new Session operations'); +END; +CREATE TRIGGER IF NOT EXISTS "__protocol_v3_cutover_session_update" +BEFORE UPDATE OF "status", "status_operation_id", "archived_at", "cleanup_operation_kind", "runtime_provisioning_operation_id", "runtime_provisioning_run_id", "runtime_provisioning_sandbox_id", "runtime_provisioning_sandbox_session_id", "runtime_provisioning_sandbox_incarnation", "runtime_provisioning_heartbeat_at" ON "session" +WHEN (OLD."status" IN ('IDLE', 'TERMINATED') + AND OLD."status_operation_id" IS NULL + AND (OLD."cleanup_operation_kind" IS NULL + OR (OLD."cleanup_operation_kind" = 'archive' + AND OLD."status" = 'IDLE' + AND OLD."archived_at" IS NOT NULL)) + AND OLD."runtime_provisioning_operation_id" IS NULL + AND OLD."runtime_provisioning_run_id" IS NULL + AND OLD."runtime_provisioning_sandbox_id" IS NULL + AND OLD."runtime_provisioning_sandbox_session_id" IS NULL + AND OLD."runtime_provisioning_sandbox_incarnation" IS NULL + AND OLD."runtime_provisioning_heartbeat_at" IS NULL) + AND NOT (NEW."status" IN ('IDLE', 'TERMINATED') + AND NEW."status_operation_id" IS NULL + AND (NEW."cleanup_operation_kind" IS NULL + OR (NEW."cleanup_operation_kind" = 'archive' + AND NEW."status" = 'IDLE' + AND NEW."archived_at" IS NOT NULL)) + AND NEW."runtime_provisioning_operation_id" IS NULL + AND NEW."runtime_provisioning_run_id" IS NULL + AND NEW."runtime_provisioning_sandbox_id" IS NULL + AND NEW."runtime_provisioning_sandbox_session_id" IS NULL + AND NEW."runtime_provisioning_sandbox_incarnation" IS NULL + AND NEW."runtime_provisioning_heartbeat_at" IS NULL) + AND EXISTS (SELECT 1 FROM "__protocol_v3_cutover" WHERE "enabled" = 1) + AND NOT EXISTS ( + SELECT 1 + FROM "__protocol_v3_cutover" AS "gate" + INNER JOIN "session" AS "smoke_session" + ON "smoke_session"."id" = NEW."id" + AND "smoke_session"."creator_account_id" = "gate"."smoke_account_id" + AND "smoke_session"."end_user_id" = "gate"."smoke_request_key" + WHERE "gate"."enabled" = 1 + AND "gate"."smoke_request_key" IS NOT NULL + AND ("gate"."smoke_session_id" IS NULL OR "gate"."smoke_session_id" = "smoke_session"."id") + ) +BEGIN + SELECT RAISE(ABORT, 'protocol v3 cutover blocks Session operation acquisition'); +END; +CREATE TRIGGER IF NOT EXISTS "__protocol_v3_cutover_api_command_insert" +BEFORE INSERT ON "api_command" +WHEN NEW."status" IN ('queued', 'running') + AND EXISTS ( + SELECT 1 FROM "__protocol_v3_cutover" + WHERE "enabled" = 1 + AND ("command_freeze" = 1 OR NEW."kind" IN ('session_run_dispatch', 'app_deployment_run_dispatch', 'environment_package_artifact_build')) + ) +BEGIN + SELECT RAISE(ABORT, 'protocol v3 cutover blocks new nonterminal API commands'); +END; +CREATE TRIGGER IF NOT EXISTS "__protocol_v3_cutover_api_command_update" +BEFORE UPDATE OF "kind", "status", "claim_owner", "claim_expires_at", "delivery_generation" ON "api_command" +WHEN EXISTS ( + SELECT 1 FROM "__protocol_v3_cutover" + WHERE "enabled" = 1 + AND ( + ("command_freeze" = 1 + AND ( + NEW."delivery_generation" IS NOT OLD."delivery_generation" + OR (NEW."status" IN ('queued', 'running') + AND (OLD."status" NOT IN ('queued', 'running') OR NEW."kind" IS NOT OLD."kind")) + )) + OR ("command_freeze" = 0 + AND NEW."kind" IN ('session_run_dispatch', 'app_deployment_run_dispatch', 'environment_package_artifact_build') + AND NEW."status" IN ('queued', 'running') + AND (OLD."status" NOT IN ('queued', 'running') OR NEW."kind" IS NOT OLD."kind")) + ) + ) +BEGIN + SELECT RAISE(ABORT, 'protocol v3 cutover blocks API command admission'); +END; +CREATE TRIGGER IF NOT EXISTS "__protocol_v3_cutover_sandbox_backup_staging_insert" +BEFORE INSERT ON "sandbox_backup_staging" +WHEN EXISTS (SELECT 1 FROM "__protocol_v3_cutover" WHERE "enabled" = 1) + AND NOT EXISTS ( + SELECT 1 + FROM "__protocol_v3_cutover" AS "gate" + INNER JOIN "session" AS "smoke_session" + ON "smoke_session"."id" = NEW."workspace_session_id" + AND "smoke_session"."creator_account_id" = "gate"."smoke_account_id" + AND "smoke_session"."end_user_id" = "gate"."smoke_request_key" + INNER JOIN "sandbox" AS "smoke_sandbox" + ON "smoke_sandbox"."id" = NEW."sandbox_id" + AND "smoke_sandbox"."subject_kind" = 'session' + AND "smoke_sandbox"."subject_id" = "smoke_session"."id" + WHERE "gate"."enabled" = 1 + AND "gate"."smoke_request_key" IS NOT NULL + AND ("gate"."smoke_session_id" IS NULL OR "gate"."smoke_session_id" = "smoke_session"."id") + ) +BEGIN + SELECT RAISE(ABORT, 'protocol v3 cutover blocks new sandbox backup staging'); +END; +CREATE TRIGGER IF NOT EXISTS "__protocol_v3_cutover_environment_artifact_backup_staging_insert" +BEFORE INSERT ON "environment_package_artifact_backup_staging" +WHEN EXISTS (SELECT 1 FROM "__protocol_v3_cutover" WHERE "enabled" = 1) + AND NOT EXISTS ( + SELECT 1 + FROM "__protocol_v3_cutover" AS "gate" + INNER JOIN "api_command" AS "command" + ON "command"."id" = NEW."command_id" + AND "command"."created_at" <= "gate"."started_at" + AND "command"."kind" = 'environment_package_artifact_build' + AND "command"."status" = 'running' + AND "command"."delivery_generation" = NEW."delivery_generation" + AND "command"."attempt_count" = NEW."attempt_count" + AND "command"."claim_owner" = NEW."claim_owner" + AND typeof("command"."claim_expires_at") = 'integer' + AND "command"."claim_expires_at" > unixepoch('subsec') * 1000 + AND json_valid("command"."payload_json") = 1 + AND json_extract("command"."payload_json", '$.appId') = NEW."app_id" + AND json_extract("command"."payload_json", '$.inputDigest') = NEW."input_digest" + WHERE "gate"."enabled" = 1 + AND "gate"."command_freeze" = 0 + ) +BEGIN + SELECT RAISE(ABORT, 'protocol v3 cutover blocks new environment artifact backup staging'); +END; diff --git a/pkgs/db/drizzle/0020_sandbox-backup-object-authority.sql b/pkgs/db/drizzle/0020_sandbox-backup-object-authority.sql new file mode 100644 index 00000000..691feb5e --- /dev/null +++ b/pkgs/db/drizzle/0020_sandbox-backup-object-authority.sql @@ -0,0 +1,738 @@ +CREATE TABLE `environment_package_artifact_backup` ( + `app_id` text CHECK ("app_id" = upper("app_id") AND length("app_id") = 26 AND substr("app_id", 1, 1) GLOB '[0-7]' AND "app_id" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*') NOT NULL, + `attempt_count` integer NOT NULL, + `backup_id` text CHECK ("backup_id" = upper("backup_id") AND length("backup_id") = 26 AND substr("backup_id", 1, 1) GLOB '[0-7]' AND "backup_id" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*') PRIMARY KEY NOT NULL, + `command_id` text CHECK ("command_id" = upper("command_id") AND length("command_id") = 26 AND substr("command_id", 1, 1) GLOB '[0-7]' AND "command_id" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*') NOT NULL, + `committed_at` integer NOT NULL, + `delivery_generation` integer NOT NULL, + `expires_at` integer NOT NULL, + `input_digest` text NOT NULL, + `manifest_generation` integer NOT NULL, + `paths_json` text NOT NULL, + FOREIGN KEY (`command_id`) REFERENCES `api_command`(`id`) ON UPDATE no action ON DELETE restrict, + CONSTRAINT "environment_package_artifact_backup_attempt_check" CHECK(typeof("environment_package_artifact_backup"."attempt_count") = 'integer' AND "environment_package_artifact_backup"."attempt_count" BETWEEN 1 AND 9007199254740991), + CONSTRAINT "environment_package_artifact_backup_delivery_check" CHECK(typeof("environment_package_artifact_backup"."delivery_generation") = 'integer' AND "environment_package_artifact_backup"."delivery_generation" BETWEEN 1 AND 9007199254740991), + CONSTRAINT "environment_package_artifact_backup_generation_check" CHECK(typeof("environment_package_artifact_backup"."manifest_generation") = 'integer' AND "environment_package_artifact_backup"."manifest_generation" BETWEEN 1 AND 9007199254740991), + CONSTRAINT "environment_package_artifact_backup_digest_check" CHECK(length("environment_package_artifact_backup"."input_digest") = 64 AND "environment_package_artifact_backup"."input_digest" = lower("environment_package_artifact_backup"."input_digest") AND "environment_package_artifact_backup"."input_digest" NOT GLOB '*[^0-9a-f]*'), + CONSTRAINT "environment_package_artifact_backup_paths_check" CHECK(json_valid("environment_package_artifact_backup"."paths_json") = 1 AND json_type("environment_package_artifact_backup"."paths_json") IS 'object' AND json_type("environment_package_artifact_backup"."paths_json", '$.executable') IS 'array' AND json_type("environment_package_artifact_backup"."paths_json", '$.node') IS 'array' AND json_type("environment_package_artifact_backup"."paths_json", '$.python') IS 'array'), + CONSTRAINT "environment_package_artifact_backup_time_check" CHECK(typeof("environment_package_artifact_backup"."committed_at") = 'integer' AND "environment_package_artifact_backup"."committed_at" BETWEEN 0 AND 9007199254740991 AND typeof("environment_package_artifact_backup"."expires_at") = 'integer' AND "environment_package_artifact_backup"."expires_at" BETWEEN "environment_package_artifact_backup"."committed_at" + 86400001 AND 9007199254740991) +) WITHOUT ROWID; +--> statement-breakpoint +CREATE UNIQUE INDEX `environment_package_artifact_backup_key_idx` ON `environment_package_artifact_backup` (`app_id`,`input_digest`);--> statement-breakpoint +CREATE INDEX `environment_package_artifact_backup_expiry_idx` ON `environment_package_artifact_backup` (`expires_at`,`backup_id`);--> statement-breakpoint +CREATE TABLE `sandbox_backup_delete_intent` ( + `attempted_at` integer, + `backup_id` text CHECK ("backup_id" = upper("backup_id") AND length("backup_id") = 26 AND substr("backup_id", 1, 1) GLOB '[0-7]' AND "backup_id" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*') PRIMARY KEY NOT NULL, + `created_at` integer NOT NULL, + `delete_after` integer NOT NULL, + `deleted_at` integer, + CONSTRAINT "sandbox_backup_delete_intent_time_check" CHECK(typeof("sandbox_backup_delete_intent"."created_at") = 'integer' AND "sandbox_backup_delete_intent"."created_at" BETWEEN 0 AND 9007199254740991 AND typeof("sandbox_backup_delete_intent"."delete_after") = 'integer' AND "sandbox_backup_delete_intent"."delete_after" BETWEEN "sandbox_backup_delete_intent"."created_at" AND 9007199254740991 AND ("sandbox_backup_delete_intent"."attempted_at" IS NULL OR (typeof("sandbox_backup_delete_intent"."attempted_at") = 'integer' AND "sandbox_backup_delete_intent"."attempted_at" BETWEEN "sandbox_backup_delete_intent"."delete_after" AND 9007199254740991)) AND ("sandbox_backup_delete_intent"."deleted_at" IS NULL OR (typeof("sandbox_backup_delete_intent"."deleted_at") = 'integer' AND "sandbox_backup_delete_intent"."deleted_at" BETWEEN coalesce("sandbox_backup_delete_intent"."attempted_at", "sandbox_backup_delete_intent"."delete_after") AND 9007199254740991))) +) WITHOUT ROWID; +--> statement-breakpoint +CREATE INDEX `sandbox_backup_delete_intent_pending_idx` ON `sandbox_backup_delete_intent` (`delete_after`,`attempted_at`,`created_at`,`backup_id`) WHERE "sandbox_backup_delete_intent"."deleted_at" IS NULL;--> statement-breakpoint +PRAGMA foreign_keys=OFF;--> statement-breakpoint +DROP TRIGGER IF EXISTS `__protocol_v3_cutover_sandbox_backup_insert`;--> statement-breakpoint +DROP TRIGGER IF EXISTS `__protocol_v3_cutover_sandbox_backup_update`;--> statement-breakpoint +CREATE TABLE `__new_sandbox_backup` ( + `created_at` integer NOT NULL, + `dir` text NOT NULL, + `id` text CHECK ("id" = upper("id") AND length("id") = 26 AND substr("id", 1, 1) GLOB '[0-7]' AND "id" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*') PRIMARY KEY NOT NULL, + `keep` integer DEFAULT false NOT NULL, + `operation_id` text CHECK ("operation_id" = upper("operation_id") AND length("operation_id") = 26 AND substr("operation_id", 1, 1) GLOB '[0-7]' AND "operation_id" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*'), + `sandbox_id` text CHECK ("sandbox_id" = upper("sandbox_id") AND length("sandbox_id") = 26 AND substr("sandbox_id", 1, 1) GLOB '[0-7]' AND "sandbox_id" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*') NOT NULL, + `sandbox_incarnation` integer NOT NULL, + `session_run_id` text CHECK ("session_run_id" = upper("session_run_id") AND length("session_run_id") = 26 AND substr("session_run_id", 1, 1) GLOB '[0-7]' AND "session_run_id" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*'), + `staging_id` text CHECK ("staging_id" = upper("staging_id") AND length("staging_id") = 26 AND substr("staging_id", 1, 1) GLOB '[0-7]' AND "staging_id" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*') NOT NULL, + `status` text NOT NULL, + `ttl_seconds` integer NOT NULL, + `updated_at` integer NOT NULL, + `workspace_session_id` text CHECK ("workspace_session_id" = upper("workspace_session_id") AND length("workspace_session_id") = 26 AND substr("workspace_session_id", 1, 1) GLOB '[0-7]' AND "workspace_session_id" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*'), + CONSTRAINT "sandbox_backup_status_check" CHECK("__new_sandbox_backup"."status" IN ('ready', 'pruned')), + CONSTRAINT "sandbox_backup_dir_check" CHECK(typeof("__new_sandbox_backup"."dir") = 'text' AND length("__new_sandbox_backup"."dir") > 0), + CONSTRAINT "sandbox_backup_keep_check" CHECK(typeof("__new_sandbox_backup"."keep") = 'integer' AND "__new_sandbox_backup"."keep" IN (false, true)), + CONSTRAINT "sandbox_backup_incarnation_check" CHECK(typeof("__new_sandbox_backup"."sandbox_incarnation") = 'integer' AND "__new_sandbox_backup"."sandbox_incarnation" BETWEEN 0 AND 9007199254740991 AND ("__new_sandbox_backup"."sandbox_incarnation" > 0 OR "__new_sandbox_backup"."staging_id" = "__new_sandbox_backup"."id")), + CONSTRAINT "sandbox_backup_ttl_check" CHECK(typeof("__new_sandbox_backup"."ttl_seconds") = 'integer' AND "__new_sandbox_backup"."ttl_seconds" BETWEEN 1 AND 9007199254740991), + CONSTRAINT "sandbox_backup_timestamps_check" CHECK(typeof("__new_sandbox_backup"."created_at") = 'integer' AND "__new_sandbox_backup"."created_at" BETWEEN 0 AND 9007199254740991 AND typeof("__new_sandbox_backup"."updated_at") = 'integer' AND "__new_sandbox_backup"."updated_at" BETWEEN "__new_sandbox_backup"."created_at" AND 9007199254740991), + CONSTRAINT "sandbox_backup_scope_check" CHECK(("__new_sandbox_backup"."session_run_id" IS NULL OR "__new_sandbox_backup"."workspace_session_id" IS NOT NULL) AND (("__new_sandbox_backup"."operation_id" IS NOT NULL) <> ("__new_sandbox_backup"."session_run_id" IS NOT NULL) OR ("__new_sandbox_backup"."operation_id" IS NULL AND "__new_sandbox_backup"."session_run_id" IS NULL AND "__new_sandbox_backup"."workspace_session_id" IS NULL AND "__new_sandbox_backup"."staging_id" = "__new_sandbox_backup"."id" AND "__new_sandbox_backup"."sandbox_incarnation" = 0))) +) WITHOUT ROWID; +--> statement-breakpoint +INSERT INTO `__new_sandbox_backup` ( + `created_at`, `dir`, `id`, `keep`, `operation_id`, `sandbox_id`, `sandbox_incarnation`, + `session_run_id`, `staging_id`, `status`, `ttl_seconds`, `updated_at`, `workspace_session_id` +) +SELECT + `created_at`, `dir`, `id`, `keep`, `operation_id`, `sandbox_id`, `sandbox_incarnation`, + `session_run_id`, `staging_id`, `status`, `ttl_seconds`, `updated_at`, `workspace_session_id` +FROM `sandbox_backup`;--> statement-breakpoint +DROP TABLE `sandbox_backup`;--> statement-breakpoint +ALTER TABLE `__new_sandbox_backup` RENAME TO `sandbox_backup`;--> statement-breakpoint +CREATE INDEX `sandbox_backup_sandbox_status_dir_created_idx` ON `sandbox_backup` (`sandbox_id`,`status`,`dir`,`created_at`,`id`);--> statement-breakpoint +CREATE INDEX `sandbox_backup_workspace_status_updated_idx` ON `sandbox_backup` (`workspace_session_id`,`status`,`updated_at`,`id`);--> statement-breakpoint +CREATE UNIQUE INDEX `sandbox_backup_staging_idx` ON `sandbox_backup` (`staging_id`);--> statement-breakpoint +CREATE UNIQUE INDEX `sandbox_backup_terminal_checkpoint_idx` ON `sandbox_backup` (`sandbox_id`,`sandbox_incarnation`,`dir`,`session_run_id`) WHERE "sandbox_backup"."session_run_id" IS NOT NULL;--> statement-breakpoint +CREATE UNIQUE INDEX `sandbox_backup_operation_checkpoint_idx` ON `sandbox_backup` (`sandbox_id`,`sandbox_incarnation`,`operation_id`,`dir`) WHERE "sandbox_backup"."operation_id" IS NOT NULL;--> statement-breakpoint +DROP TRIGGER IF EXISTS `environment_package_artifact_backup_staging_authority`;--> statement-breakpoint +DROP TRIGGER IF EXISTS `environment_package_artifact_backup_staging_immutable`;--> statement-breakpoint +DROP TRIGGER IF EXISTS `__protocol_v3_cutover_environment_artifact_backup_staging_insert`;--> statement-breakpoint +CREATE TABLE `__new_environment_package_artifact_backup_staging` ( + `actual_backup_id` text CHECK ("actual_backup_id" = upper("actual_backup_id") AND length("actual_backup_id") = 26 AND substr("actual_backup_id", 1, 1) GLOB '[0-7]' AND "actual_backup_id" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*'), + `app_id` text CHECK ("app_id" = upper("app_id") AND length("app_id") = 26 AND substr("app_id", 1, 1) GLOB '[0-7]' AND "app_id" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*') NOT NULL, + `attempt_count` integer NOT NULL, + `claim_owner` text NOT NULL, + `command_id` text CHECK ("command_id" = upper("command_id") AND length("command_id") = 26 AND substr("command_id", 1, 1) GLOB '[0-7]' AND "command_id" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*') PRIMARY KEY NOT NULL, + `created_at` integer NOT NULL, + `delivery_generation` integer NOT NULL, + `dir` text NOT NULL, + `input_digest` text NOT NULL, + `paths_json` text NOT NULL, + `updated_at` integer NOT NULL, + FOREIGN KEY (`command_id`) REFERENCES `api_command`(`id`) ON UPDATE no action ON DELETE restrict, + CONSTRAINT "environment_package_artifact_backup_staging_attempt_check" CHECK(typeof("__new_environment_package_artifact_backup_staging"."attempt_count") = 'integer' AND "__new_environment_package_artifact_backup_staging"."attempt_count" BETWEEN 1 AND 9007199254740991), + CONSTRAINT "environment_package_artifact_backup_staging_claim_owner_check" CHECK(typeof("__new_environment_package_artifact_backup_staging"."claim_owner") = 'text' AND length("__new_environment_package_artifact_backup_staging"."claim_owner") > 0), + CONSTRAINT "environment_package_artifact_backup_staging_delivery_check" CHECK(typeof("__new_environment_package_artifact_backup_staging"."delivery_generation") = 'integer' AND "__new_environment_package_artifact_backup_staging"."delivery_generation" BETWEEN 1 AND 9007199254740991), + CONSTRAINT "environment_package_artifact_backup_staging_digest_check" CHECK(length("__new_environment_package_artifact_backup_staging"."input_digest") = 64 AND "__new_environment_package_artifact_backup_staging"."input_digest" = lower("__new_environment_package_artifact_backup_staging"."input_digest") AND "__new_environment_package_artifact_backup_staging"."input_digest" NOT GLOB '*[^0-9a-f]*'), + CONSTRAINT "environment_package_artifact_backup_staging_dir_check" CHECK(typeof("__new_environment_package_artifact_backup_staging"."dir") = 'text' AND length("__new_environment_package_artifact_backup_staging"."dir") > 0), + CONSTRAINT "environment_package_artifact_backup_staging_paths_check" CHECK(json_valid("__new_environment_package_artifact_backup_staging"."paths_json") = 1 AND json_type("__new_environment_package_artifact_backup_staging"."paths_json") = 'object' AND json_type("__new_environment_package_artifact_backup_staging"."paths_json", '$.executable') = 'array' AND json_type("__new_environment_package_artifact_backup_staging"."paths_json", '$.node') = 'array' AND json_type("__new_environment_package_artifact_backup_staging"."paths_json", '$.python') = 'array'), + CONSTRAINT "environment_package_artifact_backup_staging_time_check" CHECK(typeof("__new_environment_package_artifact_backup_staging"."created_at") = 'integer' AND "__new_environment_package_artifact_backup_staging"."created_at" BETWEEN 0 AND 9007199254740991 AND typeof("__new_environment_package_artifact_backup_staging"."updated_at") = 'integer' AND "__new_environment_package_artifact_backup_staging"."updated_at" BETWEEN "__new_environment_package_artifact_backup_staging"."created_at" AND 9007199254740991) +) WITHOUT ROWID; +--> statement-breakpoint +INSERT INTO `__new_environment_package_artifact_backup_staging` ( + `actual_backup_id`, `app_id`, `attempt_count`, `claim_owner`, `command_id`, `created_at`, + `delivery_generation`, `dir`, `input_digest`, `paths_json`, `updated_at` +) +SELECT + `actual_backup_id`, `app_id`, `attempt_count`, `claim_owner`, `command_id`, `created_at`, + `delivery_generation`, `dir`, `input_digest`, `paths_json`, `updated_at` +FROM `environment_package_artifact_backup_staging`;--> statement-breakpoint +DROP TABLE `environment_package_artifact_backup_staging`;--> statement-breakpoint +ALTER TABLE `__new_environment_package_artifact_backup_staging` RENAME TO `environment_package_artifact_backup_staging`;--> statement-breakpoint +CREATE UNIQUE INDEX `environment_package_artifact_backup_staging_actual_idx` ON `environment_package_artifact_backup_staging` (`actual_backup_id`) WHERE "environment_package_artifact_backup_staging"."actual_backup_id" IS NOT NULL;--> statement-breakpoint +CREATE UNIQUE INDEX `environment_package_artifact_backup_staging_intent_idx` ON `environment_package_artifact_backup_staging` (`app_id`,`input_digest`);--> statement-breakpoint +CREATE INDEX `environment_package_artifact_backup_staging_updated_idx` ON `environment_package_artifact_backup_staging` (`updated_at`,`command_id`);--> statement-breakpoint +CREATE TABLE `__new_sandbox_backup_staging` ( + `actual_backup_id` text CHECK ("actual_backup_id" = upper("actual_backup_id") AND length("actual_backup_id") = 26 AND substr("actual_backup_id", 1, 1) GLOB '[0-7]' AND "actual_backup_id" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*'), + `claim_owner` text, + `created_at` integer NOT NULL, + `dir` text NOT NULL, + `driver_generation` integer, + `driver_instance_id` text CHECK ("driver_instance_id" = upper("driver_instance_id") AND length("driver_instance_id") = 26 AND substr("driver_instance_id", 1, 1) GLOB '[0-7]' AND "driver_instance_id" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*'), + `id` text CHECK ("id" = upper("id") AND length("id") = 26 AND substr("id", 1, 1) GLOB '[0-7]' AND "id" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*') PRIMARY KEY NOT NULL, + `operation_id` text CHECK ("operation_id" = upper("operation_id") AND length("operation_id") = 26 AND substr("operation_id", 1, 1) GLOB '[0-7]' AND "operation_id" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*'), + `sandbox_id` text CHECK ("sandbox_id" = upper("sandbox_id") AND length("sandbox_id") = 26 AND substr("sandbox_id", 1, 1) GLOB '[0-7]' AND "sandbox_id" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*') NOT NULL, + `sandbox_incarnation` integer NOT NULL, + `session_run_id` text CHECK ("session_run_id" = upper("session_run_id") AND length("session_run_id") = 26 AND substr("session_run_id", 1, 1) GLOB '[0-7]' AND "session_run_id" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*'), + `ttl_seconds` integer NOT NULL, + `updated_at` integer NOT NULL, + `updates_subject_backup` integer DEFAULT false NOT NULL, + `workspace_session_id` text CHECK ("workspace_session_id" = upper("workspace_session_id") AND length("workspace_session_id") = 26 AND substr("workspace_session_id", 1, 1) GLOB '[0-7]' AND "workspace_session_id" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*'), + CONSTRAINT "sandbox_backup_staging_claim_owner_check" CHECK("__new_sandbox_backup_staging"."claim_owner" IS NULL OR (typeof("__new_sandbox_backup_staging"."claim_owner") = 'text' AND length("__new_sandbox_backup_staging"."claim_owner") > 0)), + CONSTRAINT "sandbox_backup_staging_dir_check" CHECK(typeof("__new_sandbox_backup_staging"."dir") = 'text' AND length("__new_sandbox_backup_staging"."dir") > 0), + CONSTRAINT "sandbox_backup_staging_incarnation_check" CHECK(typeof("__new_sandbox_backup_staging"."sandbox_incarnation") = 'integer' AND "__new_sandbox_backup_staging"."sandbox_incarnation" BETWEEN 1 AND 9007199254740991), + CONSTRAINT "sandbox_backup_staging_ttl_check" CHECK(typeof("__new_sandbox_backup_staging"."ttl_seconds") = 'integer' AND "__new_sandbox_backup_staging"."ttl_seconds" BETWEEN 1 AND 9007199254740991), + CONSTRAINT "sandbox_backup_staging_timestamps_check" CHECK(typeof("__new_sandbox_backup_staging"."created_at") = 'integer' AND "__new_sandbox_backup_staging"."created_at" BETWEEN 0 AND 9007199254740991 AND typeof("__new_sandbox_backup_staging"."updated_at") = 'integer' AND "__new_sandbox_backup_staging"."updated_at" BETWEEN "__new_sandbox_backup_staging"."created_at" AND 9007199254740991), + CONSTRAINT "sandbox_backup_staging_scope_check" CHECK((("__new_sandbox_backup_staging"."operation_id" IS NOT NULL AND "__new_sandbox_backup_staging"."claim_owner" IS NOT NULL AND "__new_sandbox_backup_staging"."session_run_id" IS NULL AND "__new_sandbox_backup_staging"."driver_instance_id" IS NULL AND "__new_sandbox_backup_staging"."driver_generation" IS NULL) OR ("__new_sandbox_backup_staging"."operation_id" IS NULL AND "__new_sandbox_backup_staging"."claim_owner" IS NULL AND "__new_sandbox_backup_staging"."session_run_id" IS NOT NULL AND "__new_sandbox_backup_staging"."workspace_session_id" IS NOT NULL AND "__new_sandbox_backup_staging"."driver_instance_id" IS NOT NULL AND typeof("__new_sandbox_backup_staging"."driver_generation") = 'integer' AND "__new_sandbox_backup_staging"."driver_generation" BETWEEN 0 AND 9007199254740991)) AND ("__new_sandbox_backup_staging"."updates_subject_backup" = false OR ("__new_sandbox_backup_staging"."operation_id" IS NOT NULL AND "__new_sandbox_backup_staging"."workspace_session_id" IS NULL))), + CONSTRAINT "sandbox_backup_staging_updates_subject_check" CHECK(typeof("__new_sandbox_backup_staging"."updates_subject_backup") = 'integer' AND "__new_sandbox_backup_staging"."updates_subject_backup" IN (false, true)) +) WITHOUT ROWID; +--> statement-breakpoint +INSERT INTO `__new_sandbox_backup_staging`("actual_backup_id", "claim_owner", "created_at", "dir", "driver_generation", "driver_instance_id", "id", "operation_id", "sandbox_id", "sandbox_incarnation", "session_run_id", "ttl_seconds", "updated_at", "updates_subject_backup", "workspace_session_id") +SELECT "legacy"."actual_backup_id", + CASE WHEN "legacy"."operation_id" IS NULL THEN NULL ELSE coalesce(( + SELECT "subject"."claim_owner" FROM "sandbox" AS "subject" + WHERE "subject"."id" = "legacy"."sandbox_id" + AND "subject"."incarnation" = "legacy"."sandbox_incarnation" + AND "subject"."status" = 'backing_up' + AND "subject"."status_operation_id" = "legacy"."operation_id" + AND "subject"."claim_owner" IS NOT NULL + LIMIT 1 + ), '__legacy_stale__') END, + "legacy"."created_at", "legacy"."dir", "legacy"."driver_generation", + "legacy"."driver_instance_id", "legacy"."id", "legacy"."operation_id", + "legacy"."sandbox_id", "legacy"."sandbox_incarnation", "legacy"."session_run_id", + "legacy"."ttl_seconds", "legacy"."updated_at", "legacy"."updates_subject_backup", + "legacy"."workspace_session_id" +FROM `sandbox_backup_staging` AS "legacy";--> statement-breakpoint +DROP TABLE `sandbox_backup_staging`;--> statement-breakpoint +ALTER TABLE `__new_sandbox_backup_staging` RENAME TO `sandbox_backup_staging`;--> statement-breakpoint +PRAGMA foreign_keys=ON;--> statement-breakpoint +CREATE INDEX `sandbox_backup_staging_updated_idx` ON `sandbox_backup_staging` (`updated_at`,`id`);--> statement-breakpoint +CREATE UNIQUE INDEX `sandbox_backup_staging_actual_idx` ON `sandbox_backup_staging` (`actual_backup_id`) WHERE "sandbox_backup_staging"."actual_backup_id" IS NOT NULL;--> statement-breakpoint +CREATE UNIQUE INDEX `sandbox_backup_staging_terminal_checkpoint_idx` ON `sandbox_backup_staging` (`sandbox_id`,`sandbox_incarnation`,`dir`,`session_run_id`) WHERE "sandbox_backup_staging"."session_run_id" IS NOT NULL;--> statement-breakpoint +CREATE UNIQUE INDEX `sandbox_backup_staging_operation_checkpoint_idx` ON `sandbox_backup_staging` (`sandbox_id`,`sandbox_incarnation`,`operation_id`,`dir`) WHERE "sandbox_backup_staging"."operation_id" IS NOT NULL; +--> statement-breakpoint +CREATE TRIGGER `environment_package_artifact_backup_staging_authority` +BEFORE INSERT ON `environment_package_artifact_backup_staging` +WHEN NOT EXISTS ( + SELECT 1 + FROM `api_command` AS `command` + WHERE `command`.`id` = NEW.`command_id` + AND `command`.`kind` = 'environment_package_artifact_build' + AND `command`.`status` = 'running' + AND `command`.`delivery_generation` = NEW.`delivery_generation` + AND `command`.`attempt_count` = NEW.`attempt_count` + AND `command`.`claim_owner` = NEW.`claim_owner` + AND typeof(`command`.`claim_expires_at`) = 'integer' + AND `command`.`claim_expires_at` > unixepoch('subsec') * 1000 + AND json_valid(`command`.`payload_json`) = 1 + AND json_extract(`command`.`payload_json`, '$.appId') = NEW.`app_id` + AND json_extract(`command`.`payload_json`, '$.inputDigest') = NEW.`input_digest` +) +BEGIN + SELECT RAISE(ABORT, 'environment artifact backup stage lacks command authority'); +END;--> statement-breakpoint +CREATE TRIGGER `environment_package_artifact_backup_staging_immutable` +BEFORE UPDATE OF `app_id`, `attempt_count`, `claim_owner`, `command_id`, `created_at`, `delivery_generation`, `dir`, `input_digest`, `paths_json` ON `environment_package_artifact_backup_staging` +BEGIN + SELECT RAISE(ABORT, 'environment artifact backup stage is immutable'); +END;--> statement-breakpoint +CREATE TRIGGER `sandbox_backup_delete_intent_authority` +BEFORE INSERT ON `sandbox_backup_delete_intent` +FOR EACH ROW +WHEN NEW.`created_at` IS NOT CAST(unixepoch('subsec') * 1000 AS INTEGER) + OR NEW.`delete_after` < NEW.`created_at` + OR NEW.`attempted_at` IS NOT NULL + OR NEW.`deleted_at` IS NOT NULL + OR EXISTS ( + SELECT 1 FROM `sandbox_backup_delete_intent` + WHERE `backup_id` = NEW.`backup_id` + ) + OR EXISTS ( + SELECT 1 FROM `sandbox_backup` + WHERE `id` = NEW.`backup_id` AND `status` = 'ready' + UNION ALL + SELECT 1 FROM `sandbox_backup_staging` + WHERE `actual_backup_id` = NEW.`backup_id` + UNION ALL + SELECT 1 FROM `environment_package_artifact_backup_staging` + WHERE `actual_backup_id` = NEW.`backup_id` + UNION ALL + SELECT 1 FROM `environment_package_artifact_backup` + WHERE `backup_id` = NEW.`backup_id` + ) +BEGIN + SELECT RAISE(ABORT, 'sandbox backup deletion lacks D1 authority'); +END;--> statement-breakpoint +CREATE TRIGGER `sandbox_backup_delete_intent_blocks_runtime_record` +BEFORE INSERT ON `sandbox_backup` +FOR EACH ROW +WHEN EXISTS ( + SELECT 1 FROM `sandbox_backup` AS `existing` + WHERE `existing`.`id` = NEW.`id` + OR `existing`.`staging_id` = NEW.`staging_id` + OR (`existing`.`sandbox_id` = NEW.`sandbox_id` + AND `existing`.`sandbox_incarnation` = NEW.`sandbox_incarnation` + AND `existing`.`dir` = NEW.`dir` + AND ((NEW.`operation_id` IS NOT NULL + AND `existing`.`operation_id` = NEW.`operation_id`) + OR (NEW.`session_run_id` IS NOT NULL + AND `existing`.`session_run_id` = NEW.`session_run_id`))) + ) + OR (NEW.`status` = 'ready' AND EXISTS ( + SELECT 1 FROM `sandbox_backup_delete_intent` WHERE `backup_id` = NEW.`id` + )) + OR EXISTS ( + SELECT 1 FROM `environment_package_artifact_backup` + WHERE `backup_id` = NEW.`id` + UNION ALL + SELECT 1 FROM `environment_package_artifact_backup_staging` + WHERE `actual_backup_id` = NEW.`id` + ) + OR EXISTS ( + SELECT 1 FROM `sandbox_backup_staging` AS `stage` + WHERE `stage`.`actual_backup_id` = NEW.`id` + AND NOT ( + NEW.`status` = 'ready' + AND NEW.`keep` = 0 + AND `stage`.`id` = NEW.`staging_id` + AND `stage`.`created_at` = NEW.`created_at` + AND `stage`.`dir` = NEW.`dir` + AND `stage`.`operation_id` IS NEW.`operation_id` + AND `stage`.`sandbox_id` = NEW.`sandbox_id` + AND `stage`.`sandbox_incarnation` = NEW.`sandbox_incarnation` + AND `stage`.`session_run_id` IS NEW.`session_run_id` + AND `stage`.`ttl_seconds` = NEW.`ttl_seconds` + AND `stage`.`workspace_session_id` IS NEW.`workspace_session_id` + ) + ) +BEGIN + SELECT RAISE(ABORT, 'sandbox backup object is tombstoned or already referenced'); +END;--> statement-breakpoint +CREATE TRIGGER `sandbox_backup_delete_intent_blocks_runtime_record_update` +BEFORE UPDATE OF `id`, `status` ON `sandbox_backup` +FOR EACH ROW +WHEN (NEW.`status` = 'ready' AND EXISTS ( + SELECT 1 FROM `sandbox_backup_delete_intent` WHERE `backup_id` = NEW.`id` + )) + OR EXISTS ( + SELECT 1 FROM `environment_package_artifact_backup` + WHERE `backup_id` = NEW.`id` + UNION ALL + SELECT 1 FROM `environment_package_artifact_backup_staging` + WHERE `actual_backup_id` = NEW.`id` + ) + OR (NEW.`status` = 'pruned' AND EXISTS ( + SELECT 1 FROM `sandbox_backup_staging` + WHERE `id` = OLD.`staging_id` OR `actual_backup_id` = OLD.`id` + )) +BEGIN + SELECT RAISE(ABORT, 'sandbox backup object is tombstoned or already referenced'); +END;--> statement-breakpoint +CREATE TRIGGER `sandbox_backup_identity_immutable` +BEFORE UPDATE OF `created_at`, `dir`, `id`, `operation_id`, `sandbox_id`, `sandbox_incarnation`, `session_run_id`, `staging_id`, `ttl_seconds`, `workspace_session_id` ON `sandbox_backup` +FOR EACH ROW +BEGIN + SELECT RAISE(ABORT, 'sandbox backup identity is immutable'); +END;--> statement-breakpoint +CREATE TRIGGER `sandbox_backup_status_monotonic` +BEFORE UPDATE OF `status` ON `sandbox_backup` +FOR EACH ROW +WHEN OLD.`status` = 'pruned' AND NEW.`status` <> 'pruned' +BEGIN + SELECT RAISE(ABORT, 'pruned sandbox backup cannot become ready'); +END;--> statement-breakpoint +CREATE TRIGGER `sandbox_backup_permanent` +BEFORE DELETE ON `sandbox_backup` +FOR EACH ROW +BEGIN + SELECT RAISE(ABORT, 'sandbox backup record is permanent'); +END;--> statement-breakpoint +CREATE TRIGGER `sandbox_backup_delete_intent_blocks_runtime_stage_insert` +BEFORE INSERT ON `sandbox_backup_staging` +FOR EACH ROW +WHEN EXISTS ( + SELECT 1 FROM `sandbox_backup_staging` AS `existing` + WHERE `existing`.`id` = NEW.`id` + OR (NEW.`actual_backup_id` IS NOT NULL + AND `existing`.`actual_backup_id` = NEW.`actual_backup_id`) + OR (`existing`.`sandbox_id` = NEW.`sandbox_id` + AND `existing`.`sandbox_incarnation` = NEW.`sandbox_incarnation` + AND `existing`.`dir` = NEW.`dir` + AND ((NEW.`operation_id` IS NOT NULL + AND `existing`.`operation_id` = NEW.`operation_id`) + OR (NEW.`session_run_id` IS NOT NULL + AND `existing`.`session_run_id` = NEW.`session_run_id`))) +) + OR EXISTS ( + SELECT 1 FROM `sandbox_backup` + WHERE `staging_id` = NEW.`id` + ) + OR (NEW.`actual_backup_id` IS NOT NULL AND ( + EXISTS ( + SELECT 1 FROM `sandbox_backup_delete_intent` WHERE `backup_id` = NEW.`actual_backup_id` + ) + OR EXISTS ( + SELECT 1 FROM `sandbox_backup` + WHERE `id` = NEW.`actual_backup_id` + ) + OR EXISTS ( + SELECT 1 FROM `environment_package_artifact_backup` + WHERE `backup_id` = NEW.`actual_backup_id` + UNION ALL + SELECT 1 FROM `environment_package_artifact_backup_staging` + WHERE `actual_backup_id` = NEW.`actual_backup_id` + ) + )) +BEGIN + SELECT RAISE(ABORT, 'sandbox backup stage identity is already owned'); +END;--> statement-breakpoint +CREATE TRIGGER `sandbox_backup_staging_identity_immutable` +BEFORE UPDATE OF `claim_owner`, `created_at`, `dir`, `driver_generation`, `driver_instance_id`, `id`, `operation_id`, `sandbox_id`, `sandbox_incarnation`, `session_run_id`, `ttl_seconds`, `updates_subject_backup`, `workspace_session_id` ON `sandbox_backup_staging` +FOR EACH ROW +BEGIN + SELECT RAISE(ABORT, 'sandbox backup stage identity is immutable'); +END;--> statement-breakpoint +CREATE TRIGGER `sandbox_backup_delete_intent_blocks_runtime_stage_update` +BEFORE UPDATE OF `actual_backup_id` ON `sandbox_backup_staging` +FOR EACH ROW +WHEN NEW.`actual_backup_id` IS NOT NULL AND ( + EXISTS ( + SELECT 1 FROM `sandbox_backup_staging` AS `existing` + WHERE `existing`.`id` <> OLD.`id` + AND `existing`.`actual_backup_id` = NEW.`actual_backup_id` + ) + OR EXISTS ( + SELECT 1 FROM `sandbox_backup_delete_intent` WHERE `backup_id` = NEW.`actual_backup_id` + ) + OR EXISTS ( + SELECT 1 FROM `sandbox_backup` + WHERE `id` = NEW.`actual_backup_id` + ) + OR EXISTS ( + SELECT 1 FROM `environment_package_artifact_backup` + WHERE `backup_id` = NEW.`actual_backup_id` + UNION ALL + SELECT 1 FROM `environment_package_artifact_backup_staging` + WHERE `actual_backup_id` = NEW.`actual_backup_id` + ) +) + OR EXISTS ( + SELECT 1 FROM `sandbox_backup` + WHERE `staging_id` = OLD.`id` + ) +BEGIN + SELECT RAISE(ABORT, 'sandbox backup stage identity is already owned'); +END;--> statement-breakpoint +CREATE TRIGGER `sandbox_backup_delete_intent_blocks_environment_stage_insert` +BEFORE INSERT ON `environment_package_artifact_backup_staging` +FOR EACH ROW +WHEN EXISTS ( + SELECT 1 FROM `environment_package_artifact_backup_staging` AS `existing` + WHERE `existing`.`command_id` = NEW.`command_id` + OR (`existing`.`app_id` = NEW.`app_id` + AND `existing`.`input_digest` = NEW.`input_digest`) + OR (NEW.`actual_backup_id` IS NOT NULL + AND `existing`.`actual_backup_id` = NEW.`actual_backup_id`) +) + OR (NEW.`actual_backup_id` IS NOT NULL AND ( + EXISTS ( + SELECT 1 FROM `sandbox_backup_delete_intent` WHERE `backup_id` = NEW.`actual_backup_id` + ) + OR EXISTS ( + SELECT 1 FROM `sandbox_backup` + WHERE `id` = NEW.`actual_backup_id` + UNION ALL + SELECT 1 FROM `sandbox_backup_staging` + WHERE `actual_backup_id` = NEW.`actual_backup_id` + ) + OR EXISTS ( + SELECT 1 FROM `environment_package_artifact_backup` + WHERE `backup_id` = NEW.`actual_backup_id` + ) + )) +BEGIN + SELECT RAISE(ABORT, 'environment artifact backup stage identity is already owned'); +END;--> statement-breakpoint +CREATE TRIGGER `sandbox_backup_delete_intent_blocks_environment_stage_update` +BEFORE UPDATE OF `actual_backup_id` ON `environment_package_artifact_backup_staging` +FOR EACH ROW +WHEN NEW.`actual_backup_id` IS NOT NULL AND ( + EXISTS ( + SELECT 1 FROM `environment_package_artifact_backup_staging` AS `existing` + WHERE `existing`.`command_id` <> OLD.`command_id` + AND `existing`.`actual_backup_id` = NEW.`actual_backup_id` + ) + OR EXISTS ( + SELECT 1 FROM `sandbox_backup_delete_intent` WHERE `backup_id` = NEW.`actual_backup_id` + ) + OR EXISTS ( + SELECT 1 FROM `sandbox_backup` + WHERE `id` = NEW.`actual_backup_id` + UNION ALL + SELECT 1 FROM `sandbox_backup_staging` + WHERE `actual_backup_id` = NEW.`actual_backup_id` + ) + OR EXISTS ( + SELECT 1 FROM `environment_package_artifact_backup` + WHERE `backup_id` = NEW.`actual_backup_id` + ) +) +BEGIN + SELECT RAISE(ABORT, 'environment artifact backup stage identity is already owned'); +END;--> statement-breakpoint +CREATE TRIGGER `sandbox_backup_delete_intent_blocks_environment_commit` +BEFORE INSERT ON `environment_package_artifact_backup` +FOR EACH ROW +WHEN EXISTS ( + SELECT 1 FROM `sandbox_backup_delete_intent` + WHERE `backup_id` = NEW.`backup_id` + UNION ALL + SELECT 1 FROM `sandbox_backup` + WHERE `id` = NEW.`backup_id` + UNION ALL + SELECT 1 FROM `sandbox_backup_staging` + WHERE `actual_backup_id` = NEW.`backup_id` +) +BEGIN + SELECT RAISE(ABORT, 'sandbox backup object is tombstoned or already referenced'); +END;--> statement-breakpoint +CREATE TRIGGER `sandbox_backup_delete_intent_identity_immutable` +BEFORE UPDATE OF `backup_id`, `created_at`, `delete_after` ON `sandbox_backup_delete_intent` +FOR EACH ROW +BEGIN + SELECT RAISE(ABORT, 'sandbox backup deletion intent identity is immutable'); +END;--> statement-breakpoint +CREATE TRIGGER `sandbox_backup_delete_intent_attempt_clock` +BEFORE UPDATE OF `attempted_at` ON `sandbox_backup_delete_intent` +FOR EACH ROW +WHEN NEW.`deleted_at` IS NOT NULL + OR NEW.`attempted_at` IS NOT CAST(unixepoch('subsec') * 1000 AS INTEGER) + OR NEW.`attempted_at` < OLD.`delete_after` + OR (OLD.`attempted_at` IS NOT NULL AND NEW.`attempted_at` < OLD.`attempted_at`) +BEGIN + SELECT RAISE(ABORT, 'sandbox backup deletion retry must use monotonic D1 time'); +END;--> statement-breakpoint +CREATE TRIGGER `sandbox_backup_delete_intent_completion_monotonic` +BEFORE UPDATE OF `deleted_at` ON `sandbox_backup_delete_intent` +FOR EACH ROW +WHEN (OLD.`deleted_at` IS NOT NULL AND NEW.`deleted_at` IS NOT OLD.`deleted_at`) + OR (OLD.`deleted_at` IS NULL AND NEW.`deleted_at` IS NOT NULL + AND (OLD.`attempted_at` IS NULL + OR NEW.`deleted_at` IS NOT CAST(unixepoch('subsec') * 1000 AS INTEGER))) +BEGIN + SELECT RAISE(ABORT, 'sandbox backup deletion completion must use D1 time and is irreversible'); +END;--> statement-breakpoint +CREATE TRIGGER `sandbox_backup_delete_intent_permanent` +BEFORE DELETE ON `sandbox_backup_delete_intent` +FOR EACH ROW +BEGIN + SELECT RAISE(ABORT, 'sandbox backup deletion intent is permanent'); +END;--> statement-breakpoint +CREATE TRIGGER `environment_package_artifact_backup_authority` +BEFORE INSERT ON `environment_package_artifact_backup` +FOR EACH ROW +WHEN NEW.`committed_at` IS NOT CAST(unixepoch('subsec') * 1000 AS INTEGER) + OR NEW.`manifest_generation` IS NOT 1 + OR NEW.`expires_at` <= NEW.`committed_at` + 86400000 + OR NEW.`expires_at` > NEW.`committed_at` + 315360000000 + OR EXISTS ( + SELECT 1 FROM `environment_package_artifact_backup` + WHERE `backup_id` = NEW.`backup_id` + OR (`app_id` = NEW.`app_id` AND `input_digest` = NEW.`input_digest`) + ) + OR NOT ( + EXISTS ( + SELECT 1 + FROM `environment_package_artifact_backup_staging` AS `stage` + JOIN `api_command` AS `command` ON `command`.`id` = `stage`.`command_id` + WHERE `stage`.`actual_backup_id` = NEW.`backup_id` + AND `stage`.`app_id` = NEW.`app_id` + AND `stage`.`attempt_count` = NEW.`attempt_count` + AND `stage`.`command_id` = NEW.`command_id` + AND `stage`.`delivery_generation` = NEW.`delivery_generation` + AND `stage`.`dir` = '/workspace/.mosoo/environment-artifacts/' || NEW.`input_digest` + AND `stage`.`input_digest` = NEW.`input_digest` + AND `stage`.`paths_json` = NEW.`paths_json` + AND `command`.`kind` = 'environment_package_artifact_build' + AND `command`.`status` = 'running' + AND `command`.`attempt_count` = `stage`.`attempt_count` + AND `command`.`claim_owner` = `stage`.`claim_owner` + AND typeof(`command`.`claim_expires_at`) = 'integer' + AND `command`.`claim_expires_at` > CAST(unixepoch('subsec') * 1000 AS INTEGER) + AND `command`.`delivery_generation` = `stage`.`delivery_generation` + AND json_valid(`command`.`payload_json`) = 1 + AND json_extract(`command`.`payload_json`, '$.appId') = `stage`.`app_id` + AND json_extract(`command`.`payload_json`, '$.inputDigest') = `stage`.`input_digest` + ) + OR EXISTS ( + SELECT 1 FROM `api_command` AS `command` + WHERE `command`.`id` = NEW.`command_id` + AND `command`.`kind` = 'environment_package_artifact_build' + AND `command`.`status` = 'succeeded' + AND typeof(`command`.`completed_at`) = 'integer' + AND `command`.`claim_owner` IS NULL + AND `command`.`claim_expires_at` IS NULL + AND `command`.`attempt_count` = NEW.`attempt_count` + AND `command`.`delivery_generation` = NEW.`delivery_generation` + AND json_valid(`command`.`payload_json`) = 1 + AND json_extract(`command`.`payload_json`, '$.appId') = NEW.`app_id` + AND json_extract(`command`.`payload_json`, '$.inputDigest') = NEW.`input_digest` + AND NOT EXISTS ( + SELECT 1 FROM `environment_package_artifact_backup_staging` + WHERE `actual_backup_id` = NEW.`backup_id` + ) + ) + ) + OR EXISTS ( + SELECT 1 FROM `sandbox_backup_delete_intent` + WHERE `backup_id` = NEW.`backup_id` + ) + OR EXISTS ( + SELECT 1 FROM `sandbox_backup` + WHERE `id` = NEW.`backup_id` + UNION ALL + SELECT 1 FROM `sandbox_backup_staging` + WHERE `actual_backup_id` = NEW.`backup_id` + ) + OR (SELECT count(*) FROM json_each(NEW.`paths_json`)) <> 3 + OR (SELECT count(*) FROM json_each(NEW.`paths_json`) WHERE `key` = 'executable') <> 1 + OR (SELECT count(*) FROM json_each(NEW.`paths_json`) WHERE `key` = 'node') <> 1 + OR (SELECT count(*) FROM json_each(NEW.`paths_json`) WHERE `key` = 'python') <> 1 + OR EXISTS ( + SELECT 1 FROM json_each(NEW.`paths_json`) AS `entry` + WHERE `entry`.`key` NOT IN ('executable', 'node', 'python') + ) + OR EXISTS ( + SELECT 1 + FROM ( + SELECT `value`, `type` FROM json_each(NEW.`paths_json`, '$.executable') + UNION ALL + SELECT `value`, `type` FROM json_each(NEW.`paths_json`, '$.node') + UNION ALL + SELECT `value`, `type` FROM json_each(NEW.`paths_json`, '$.python') + ) AS `path` + WHERE `path`.`type` <> 'text' + OR NOT (`path`.`value` GLOB ('/workspace/.mosoo/environment-artifacts/' || NEW.`input_digest` || '/*')) + OR instr(`path`.`value`, char(0)) > 0 + OR instr(`path`.`value`, ':') > 0 + OR instr(`path`.`value`, '//') > 0 + OR substr(`path`.`value`, -1) = '/' + OR substr(`path`.`value`, -2) = '/.' + OR substr(`path`.`value`, -3) = '/..' + OR `path`.`value` GLOB '*/./*' + OR `path`.`value` GLOB '*/../*' + ) + OR EXISTS ( + SELECT 1 + FROM ( + SELECT `value` FROM json_each(NEW.`paths_json`, '$.executable') + UNION ALL + SELECT `value` FROM json_each(NEW.`paths_json`, '$.node') + UNION ALL + SELECT `value` FROM json_each(NEW.`paths_json`, '$.python') + ) AS `path` + GROUP BY `path`.`value` + HAVING count(*) > 1 + ) +BEGIN + SELECT RAISE(ABORT, 'environment package artifact backup lacks D1 authority'); +END;--> statement-breakpoint +CREATE TRIGGER `environment_package_artifact_backup_rotation_authority` +BEFORE UPDATE ON `environment_package_artifact_backup` +FOR EACH ROW +WHEN NEW.`app_id` IS NOT OLD.`app_id` + OR NEW.`input_digest` IS NOT OLD.`input_digest` + OR NEW.`paths_json` IS NOT OLD.`paths_json` + OR NEW.`backup_id` IS OLD.`backup_id` + OR OLD.`manifest_generation` >= 9007199254740991 + OR NEW.`manifest_generation` IS NOT OLD.`manifest_generation` + 1 + OR NEW.`committed_at` IS NOT CAST(unixepoch('subsec') * 1000 AS INTEGER) + OR NEW.`committed_at` < OLD.`committed_at` + OR NEW.`expires_at` <= NEW.`committed_at` + 86400000 + OR NEW.`expires_at` > NEW.`committed_at` + 315360000000 + OR NEW.`expires_at` <= OLD.`expires_at` + OR EXISTS ( + SELECT 1 FROM `sandbox_backup_delete_intent` + WHERE `backup_id` = NEW.`backup_id` + ) + OR EXISTS ( + SELECT 1 FROM `sandbox_backup` + WHERE `id` = NEW.`backup_id` + UNION ALL + SELECT 1 FROM `sandbox_backup_staging` + WHERE `actual_backup_id` = NEW.`backup_id` + ) + OR EXISTS ( + SELECT 1 FROM `environment_package_artifact_backup` AS `other` + WHERE `other`.`backup_id` = NEW.`backup_id` + AND (`other`.`app_id` <> OLD.`app_id` OR `other`.`input_digest` <> OLD.`input_digest`) + ) + OR NOT EXISTS ( + SELECT 1 + FROM `environment_package_artifact_backup_staging` AS `stage` + JOIN `api_command` AS `command` ON `command`.`id` = `stage`.`command_id` + WHERE `stage`.`actual_backup_id` = NEW.`backup_id` + AND `stage`.`app_id` = NEW.`app_id` + AND `stage`.`attempt_count` = NEW.`attempt_count` + AND `stage`.`command_id` = NEW.`command_id` + AND `stage`.`delivery_generation` = NEW.`delivery_generation` + AND `stage`.`dir` = '/workspace/.mosoo/environment-artifacts/' || NEW.`input_digest` + AND `stage`.`input_digest` = NEW.`input_digest` + AND `stage`.`paths_json` = NEW.`paths_json` + AND `command`.`kind` = 'environment_package_artifact_build' + AND `command`.`status` = 'running' + AND `command`.`attempt_count` = `stage`.`attempt_count` + AND `command`.`claim_owner` = `stage`.`claim_owner` + AND typeof(`command`.`claim_expires_at`) = 'integer' + AND `command`.`claim_expires_at` > CAST(unixepoch('subsec') * 1000 AS INTEGER) + AND `command`.`delivery_generation` = `stage`.`delivery_generation` + AND json_valid(`command`.`payload_json`) = 1 + AND json_extract(`command`.`payload_json`, '$.appId') = `stage`.`app_id` + AND json_extract(`command`.`payload_json`, '$.inputDigest') = `stage`.`input_digest` + ) +BEGIN + SELECT RAISE(ABORT, 'environment package artifact backup rotation lacks D1 authority'); +END;--> statement-breakpoint +CREATE TRIGGER `environment_package_artifact_backup_rotation_tombstone` +AFTER UPDATE OF `backup_id` ON `environment_package_artifact_backup` +FOR EACH ROW +WHEN NEW.`backup_id` IS NOT OLD.`backup_id` +BEGIN + INSERT INTO `sandbox_backup_delete_intent` (`attempted_at`, `backup_id`, `created_at`, `delete_after`, `deleted_at`) + VALUES ( + NULL, + OLD.`backup_id`, + CAST(unixepoch('subsec') * 1000 AS INTEGER), + max(OLD.`expires_at`, CAST(unixepoch('subsec') * 1000 AS INTEGER)), + NULL + ); +END;--> statement-breakpoint +CREATE TRIGGER `environment_package_artifact_backup_retirement_authority` +BEFORE DELETE ON `environment_package_artifact_backup` +FOR EACH ROW +WHEN OLD.`expires_at` > CAST(unixepoch('subsec') * 1000 AS INTEGER) +BEGIN + SELECT RAISE(ABORT, 'environment package artifact backup manifest has not expired'); +END;--> statement-breakpoint +CREATE TRIGGER `environment_package_artifact_backup_retirement_tombstone` +AFTER DELETE ON `environment_package_artifact_backup` +FOR EACH ROW +BEGIN + INSERT INTO `sandbox_backup_delete_intent` (`attempted_at`, `backup_id`, `created_at`, `delete_after`, `deleted_at`) + VALUES ( + NULL, + OLD.`backup_id`, + CAST(unixepoch('subsec') * 1000 AS INTEGER), + CAST(unixepoch('subsec') * 1000 AS INTEGER), + NULL + ); +END;--> statement-breakpoint +CREATE TRIGGER IF NOT EXISTS "__protocol_v3_cutover_sandbox_backup_insert" +BEFORE INSERT ON "sandbox_backup" +WHEN NEW."status" NOT IN ('ready', 'pruned') + AND EXISTS (SELECT 1 FROM "__protocol_v3_cutover" WHERE "enabled" = 1) +BEGIN + SELECT RAISE(ABORT, 'protocol v3 cutover blocks new sandbox backup work'); +END;--> statement-breakpoint +CREATE TRIGGER IF NOT EXISTS "__protocol_v3_cutover_sandbox_backup_update" +BEFORE UPDATE OF "status" ON "sandbox_backup" +WHEN OLD."status" IN ('ready', 'pruned') + AND NEW."status" NOT IN ('ready', 'pruned') + AND EXISTS (SELECT 1 FROM "__protocol_v3_cutover" WHERE "enabled" = 1) +BEGIN + SELECT RAISE(ABORT, 'protocol v3 cutover blocks sandbox backup reactivation'); +END;--> statement-breakpoint +CREATE TRIGGER IF NOT EXISTS "__protocol_v3_cutover_environment_artifact_backup_staging_insert" +BEFORE INSERT ON "environment_package_artifact_backup_staging" +WHEN EXISTS (SELECT 1 FROM "__protocol_v3_cutover" WHERE "enabled" = 1) + AND NOT EXISTS ( + SELECT 1 + FROM "__protocol_v3_cutover" AS "gate" + INNER JOIN "api_command" AS "command" + ON "command"."id" = NEW."command_id" + AND "command"."created_at" <= "gate"."started_at" + AND "command"."kind" = 'environment_package_artifact_build' + AND "command"."status" = 'running' + AND "command"."delivery_generation" = NEW."delivery_generation" + AND "command"."attempt_count" = NEW."attempt_count" + AND "command"."claim_owner" = NEW."claim_owner" + AND typeof("command"."claim_expires_at") = 'integer' + AND "command"."claim_expires_at" > unixepoch('subsec') * 1000 + AND json_valid("command"."payload_json") = 1 + AND json_extract("command"."payload_json", '$.appId') = NEW."app_id" + AND json_extract("command"."payload_json", '$.inputDigest') = NEW."input_digest" + WHERE "gate"."enabled" = 1 + AND "gate"."command_freeze" = 0 + ) +BEGIN + SELECT RAISE(ABORT, 'protocol v3 cutover blocks new environment artifact backup staging'); +END;--> statement-breakpoint +CREATE TRIGGER IF NOT EXISTS "__protocol_v3_cutover_sandbox_backup_staging_insert" +BEFORE INSERT ON "sandbox_backup_staging" +WHEN EXISTS (SELECT 1 FROM "__protocol_v3_cutover" WHERE "enabled" = 1) + AND NOT EXISTS ( + SELECT 1 + FROM "__protocol_v3_cutover" AS "gate" + INNER JOIN "session" AS "smoke_session" + ON "smoke_session"."id" = NEW."workspace_session_id" + AND "smoke_session"."creator_account_id" = "gate"."smoke_account_id" + AND "smoke_session"."end_user_id" = "gate"."smoke_request_key" + INNER JOIN "sandbox" AS "smoke_sandbox" + ON "smoke_sandbox"."id" = NEW."sandbox_id" + AND "smoke_sandbox"."subject_kind" = 'session' + AND "smoke_sandbox"."subject_id" = "smoke_session"."id" + WHERE "gate"."enabled" = 1 + AND "gate"."smoke_request_key" IS NOT NULL + AND ("gate"."smoke_session_id" IS NULL OR "gate"."smoke_session_id" = "smoke_session"."id") + ) +BEGIN + SELECT RAISE(ABORT, 'protocol v3 cutover blocks new sandbox backup staging'); +END; diff --git a/pkgs/db/drizzle/meta/0013_snapshot.json b/pkgs/db/drizzle/meta/0013_snapshot.json new file mode 100644 index 00000000..ef37bf43 --- /dev/null +++ b/pkgs/db/drizzle/meta/0013_snapshot.json @@ -0,0 +1,6634 @@ +{ + "version": "6", + "dialect": "sqlite", + "id": "6e193e8f-0833-4771-9c04-14f3325e2dc9", + "prevId": "57a9cb12-4c89-4d3e-a8f4-8b6fafe633a8", + "tables": { + "agent_deployment_version": { + "name": "agent_deployment_version", + "columns": { + "agent_id": { + "name": "agent_id", + "type": "text CHECK (\"agent_id\" = upper(\"agent_id\") AND length(\"agent_id\") = 26 AND substr(\"agent_id\", 1, 1) GLOB '[0-7]' AND \"agent_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "config_json": { + "name": "config_json", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_by_account_id": { + "name": "created_by_account_id", + "type": "text CHECK (\"created_by_account_id\" = upper(\"created_by_account_id\") AND length(\"created_by_account_id\") = 26 AND substr(\"created_by_account_id\", 1, 1) GLOB '[0-7]' AND \"created_by_account_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "environment_id": { + "name": "environment_id", + "type": "text CHECK (\"environment_id\" = upper(\"environment_id\") AND length(\"environment_id\") = 26 AND substr(\"environment_id\", 1, 1) GLOB '[0-7]' AND \"environment_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "id": { + "name": "id", + "type": "text CHECK (\"id\" = upper(\"id\") AND length(\"id\") = 26 AND substr(\"id\", 1, 1) GLOB '[0-7]' AND \"id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "mcp_bindings_json": { + "name": "mcp_bindings_json", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "prompt": { + "name": "prompt", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "runtime_id": { + "name": "runtime_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "skills_json": { + "name": "skills_json", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "summary": { + "name": "summary", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "version_number": { + "name": "version_number", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "agent_deployment_version_agent_number_idx": { + "name": "agent_deployment_version_agent_number_idx", + "columns": ["agent_id", "version_number"], + "isUnique": true + }, + "agent_deployment_version_agent_created_idx": { + "name": "agent_deployment_version_agent_created_idx", + "columns": ["agent_id", "created_at"], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "agent_mcp_binding": { + "name": "agent_mcp_binding", + "columns": { + "agent_credential_id": { + "name": "agent_credential_id", + "type": "text CHECK (\"agent_credential_id\" = upper(\"agent_credential_id\") AND length(\"agent_credential_id\") = 26 AND substr(\"agent_credential_id\", 1, 1) GLOB '[0-7]' AND \"agent_credential_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "agent_id": { + "name": "agent_id", + "type": "text CHECK (\"agent_id\" = upper(\"agent_id\") AND length(\"agent_id\") = 26 AND substr(\"agent_id\", 1, 1) GLOB '[0-7]' AND \"agent_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "credential_mode": { + "name": "credential_mode", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'runtime_resolved'" + }, + "enabled": { + "name": "enabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "id": { + "name": "id", + "type": "text CHECK (\"id\" = upper(\"id\") AND length(\"id\") = 26 AND substr(\"id\", 1, 1) GLOB '[0-7]' AND \"id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "server_id": { + "name": "server_id", + "type": "text CHECK (\"server_id\" = upper(\"server_id\") AND length(\"server_id\") = 26 AND substr(\"server_id\", 1, 1) GLOB '[0-7]' AND \"server_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "agent_mcp_binding_agent_sort_idx": { + "name": "agent_mcp_binding_agent_sort_idx", + "columns": ["agent_id", "sort_order"], + "isUnique": true + }, + "agent_mcp_binding_server_idx": { + "name": "agent_mcp_binding_server_idx", + "columns": ["server_id"], + "isUnique": false + }, + "agent_mcp_binding_profile_server_idx": { + "name": "agent_mcp_binding_profile_server_idx", + "columns": ["agent_id", "server_id"], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": { + "agent_mcp_binding_agent_credential_shape_check": { + "name": "agent_mcp_binding_agent_credential_shape_check", + "value": "\n (\"agent_mcp_binding\".\"credential_mode\" = 'agent_bound' AND \"agent_mcp_binding\".\"agent_credential_id\" IS NOT NULL)\n OR (\"agent_mcp_binding\".\"credential_mode\" = 'runtime_resolved' AND \"agent_mcp_binding\".\"agent_credential_id\" IS NULL)\n " + } + } + }, + "agent_skill": { + "name": "agent_skill", + "columns": { + "agent_id": { + "name": "agent_id", + "type": "text CHECK (\"agent_id\" = upper(\"agent_id\") AND length(\"agent_id\") = 26 AND substr(\"agent_id\", 1, 1) GLOB '[0-7]' AND \"agent_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "skill_id": { + "name": "skill_id", + "type": "text CHECK (\"skill_id\" = upper(\"skill_id\") AND length(\"skill_id\") = 26 AND substr(\"skill_id\", 1, 1) GLOB '[0-7]' AND \"skill_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "agent_skill_agent_sort_idx": { + "name": "agent_skill_agent_sort_idx", + "columns": ["agent_id", "sort_order"], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "agent_skill_agent_id_skill_id_pk": { + "columns": ["agent_id", "skill_id"], + "name": "agent_skill_agent_id_skill_id_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "agent": { + "name": "agent", + "columns": { + "config_json": { + "name": "config_json", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "environment_id": { + "name": "environment_id", + "type": "text CHECK (\"environment_id\" = upper(\"environment_id\") AND length(\"environment_id\") = 26 AND substr(\"environment_id\", 1, 1) GLOB '[0-7]' AND \"environment_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "id": { + "name": "id", + "type": "text CHECK (\"id\" = upper(\"id\") AND length(\"id\") = 26 AND substr(\"id\", 1, 1) GLOB '[0-7]' AND \"id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'pet'" + }, + "live_deployment_version_id": { + "name": "live_deployment_version_id", + "type": "text CHECK (\"live_deployment_version_id\" = upper(\"live_deployment_version_id\") AND length(\"live_deployment_version_id\") = 26 AND substr(\"live_deployment_version_id\", 1, 1) GLOB '[0-7]' AND \"live_deployment_version_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "owner_account_id": { + "name": "owner_account_id", + "type": "text CHECK (\"owner_account_id\" = upper(\"owner_account_id\") AND length(\"owner_account_id\") = 26 AND substr(\"owner_account_id\", 1, 1) GLOB '[0-7]' AND \"owner_account_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "app_id": { + "name": "app_id", + "type": "text CHECK (\"app_id\" = upper(\"app_id\") AND length(\"app_id\") = 26 AND substr(\"app_id\", 1, 1) GLOB '[0-7]' AND \"app_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "prompt": { + "name": "prompt", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "runtime_id": { + "name": "runtime_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'draft'" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "visibility": { + "name": "visibility", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'private'" + } + }, + "indexes": { + "agent_app_owner_account_idx": { + "name": "agent_app_owner_account_idx", + "columns": ["app_id", "owner_account_id"], + "isUnique": false + }, + "agent_app_status_idx": { + "name": "agent_app_status_idx", + "columns": ["app_id", "status"], + "isUnique": false + }, + "agent_environment_idx": { + "name": "agent_environment_idx", + "columns": ["environment_id"], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": { + "agent_published_live_deployment_version_check": { + "name": "agent_published_live_deployment_version_check", + "value": "\"agent\".\"status\" <> 'published' OR \"agent\".\"live_deployment_version_id\" IS NOT NULL" + } + } + }, + "api_command": { + "name": "api_command", + "columns": { + "attempt_count": { + "name": "attempt_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "claim_expires_at": { + "name": "claim_expires_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "claim_owner": { + "name": "claim_owner", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "completed_at": { + "name": "completed_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "dedupe_key": { + "name": "dedupe_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "id": { + "name": "id", + "type": "text CHECK (\"id\" = upper(\"id\") AND length(\"id\") = 26 AND substr(\"id\", 1, 1) GLOB '[0-7]' AND \"id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "last_error_code": { + "name": "last_error_code", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_error_message": { + "name": "last_error_message", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "payload_json": { + "name": "payload_json", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "api_command_dedupe_idx": { + "name": "api_command_dedupe_idx", + "columns": ["dedupe_key"], + "isUnique": true + }, + "api_command_status_updated_idx": { + "name": "api_command_status_updated_idx", + "columns": ["status", "updated_at"], + "isUnique": false + }, + "api_command_claim_idx": { + "name": "api_command_claim_idx", + "columns": ["status", "claim_expires_at"], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "auth_account": { + "name": "auth_account", + "columns": { + "access_token": { + "name": "access_token", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "access_token_expires_at": { + "name": "access_token_expires_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "provider_account_id": { + "name": "provider_account_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "id_token": { + "name": "id_token", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "refresh_token": { + "name": "refresh_token", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "refresh_token_expires_at": { + "name": "refresh_token_expires_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "account_id": { + "name": "account_id", + "type": "text CHECK (\"account_id\" = upper(\"account_id\") AND length(\"account_id\") = 26 AND substr(\"account_id\", 1, 1) GLOB '[0-7]' AND \"account_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "auth_account_provider_account_idx": { + "name": "auth_account_provider_account_idx", + "columns": ["provider_id", "provider_account_id"], + "isUnique": true + }, + "auth_account_account_id_idx": { + "name": "auth_account_account_id_idx", + "columns": ["account_id"], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "auth_session": { + "name": "auth_session", + "columns": { + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "ip_address": { + "name": "ip_address", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "account_id": { + "name": "account_id", + "type": "text CHECK (\"account_id\" = upper(\"account_id\") AND length(\"account_id\") = 26 AND substr(\"account_id\", 1, 1) GLOB '[0-7]' AND \"account_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "auth_session_expires_at_idx": { + "name": "auth_session_expires_at_idx", + "columns": ["expires_at"], + "isUnique": false + }, + "auth_session_token_idx": { + "name": "auth_session_token_idx", + "columns": ["token"], + "isUnique": true + }, + "auth_session_account_id_idx": { + "name": "auth_session_account_id_idx", + "columns": ["account_id"], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "auth_verification": { + "name": "auth_verification", + "columns": { + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "identifier": { + "name": "identifier", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "auth_verification_expires_at_idx": { + "name": "auth_verification_expires_at_idx", + "columns": ["expires_at"], + "isUnique": false + }, + "auth_verification_identifier_idx": { + "name": "auth_verification_identifier_idx", + "columns": ["identifier"], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "cli_oauth_flow": { + "name": "cli_oauth_flow", + "columns": { + "account_id": { + "name": "account_id", + "type": "text CHECK (\"account_id\" = upper(\"account_id\") AND length(\"account_id\") = 26 AND substr(\"account_id\", 1, 1) GLOB '[0-7]' AND \"account_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "authorized_at": { + "name": "authorized_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "completed_at": { + "name": "completed_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "device_code_hash": { + "name": "device_code_hash", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "hostname": { + "name": "hostname", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "id": { + "name": "id", + "type": "text CHECK (\"id\" = upper(\"id\") AND length(\"id\") = 26 AND substr(\"id\", 1, 1) GLOB '[0-7]' AND \"id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_code": { + "name": "user_code", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "cli_oauth_flow_status_expires_idx": { + "name": "cli_oauth_flow_status_expires_idx", + "columns": ["status", "expires_at"], + "isUnique": false + }, + "cli_oauth_flow_device_code_hash_idx": { + "name": "cli_oauth_flow_device_code_hash_idx", + "columns": ["device_code_hash"], + "isUnique": true + }, + "cli_oauth_flow_user_code_idx": { + "name": "cli_oauth_flow_user_code_idx", + "columns": ["user_code"], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "personal_access_token": { + "name": "personal_access_token", + "columns": { + "account_id": { + "name": "account_id", + "type": "text CHECK (\"account_id\" = upper(\"account_id\") AND length(\"account_id\") = 26 AND substr(\"account_id\", 1, 1) GLOB '[0-7]' AND \"account_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "id": { + "name": "id", + "type": "text CHECK (\"id\" = upper(\"id\") AND length(\"id\") = 26 AND substr(\"id\", 1, 1) GLOB '[0-7]' AND \"id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "last_used_at": { + "name": "last_used_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "revoked_at": { + "name": "revoked_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "token_hash": { + "name": "token_hash", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "personal_access_token_account_created_idx": { + "name": "personal_access_token_account_created_idx", + "columns": ["account_id", "created_at"], + "isUnique": false + }, + "personal_access_token_hash_idx": { + "name": "personal_access_token_hash_idx", + "columns": ["token_hash"], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "email_log": { + "name": "email_log", + "columns": { + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "recipient_domain": { + "name": "recipient_domain", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "recipient_masked": { + "name": "recipient_masked", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "subject": { + "name": "subject", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "email_log_created_at_idx": { + "name": "email_log_created_at_idx", + "columns": ["created_at"], + "isUnique": false + }, + "email_log_type_status_idx": { + "name": "email_log_type_status_idx", + "columns": ["type", "status"], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "environment_revision": { + "name": "environment_revision", + "columns": { + "allow_mcp_servers": { + "name": "allow_mcp_servers", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "allow_package_managers": { + "name": "allow_package_managers", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "allowed_hosts_json": { + "name": "allowed_hosts_json", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_by_account_id": { + "name": "created_by_account_id", + "type": "text CHECK (\"created_by_account_id\" = upper(\"created_by_account_id\") AND length(\"created_by_account_id\") = 26 AND substr(\"created_by_account_id\", 1, 1) GLOB '[0-7]' AND \"created_by_account_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "env_vars_json": { + "name": "env_vars_json", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "environment_id": { + "name": "environment_id", + "type": "text CHECK (\"environment_id\" = upper(\"environment_id\") AND length(\"environment_id\") = 26 AND substr(\"environment_id\", 1, 1) GLOB '[0-7]' AND \"environment_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "id": { + "name": "id", + "type": "text CHECK (\"id\" = upper(\"id\") AND length(\"id\") = 26 AND substr(\"id\", 1, 1) GLOB '[0-7]' AND \"id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "network_policy": { + "name": "network_policy", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "packages_json": { + "name": "packages_json", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "app_id": { + "name": "app_id", + "type": "text CHECK (\"app_id\" = upper(\"app_id\") AND length(\"app_id\") = 26 AND substr(\"app_id\", 1, 1) GLOB '[0-7]' AND \"app_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "setup_script": { + "name": "setup_script", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "environment_revision_environment_created_at_idx": { + "name": "environment_revision_environment_created_at_idx", + "columns": ["environment_id", "created_at"], + "isUnique": false + }, + "environment_revision_app_created_at_idx": { + "name": "environment_revision_app_created_at_idx", + "columns": ["app_id", "created_at"], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": { + "environment_revision_network_policy_check": { + "name": "environment_revision_network_policy_check", + "value": "\"environment_revision\".\"network_policy\" IN ('full', 'limited')" + } + } + }, + "environment": { + "name": "environment", + "columns": { + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "current_revision_id": { + "name": "current_revision_id", + "type": "text CHECK (\"current_revision_id\" = upper(\"current_revision_id\") AND length(\"current_revision_id\") = 26 AND substr(\"current_revision_id\", 1, 1) GLOB '[0-7]' AND \"current_revision_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "forked_from_environment_id": { + "name": "forked_from_environment_id", + "type": "text CHECK (\"forked_from_environment_id\" = upper(\"forked_from_environment_id\") AND length(\"forked_from_environment_id\") = 26 AND substr(\"forked_from_environment_id\", 1, 1) GLOB '[0-7]' AND \"forked_from_environment_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "forked_from_environment_name": { + "name": "forked_from_environment_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "forked_from_owner_name": { + "name": "forked_from_owner_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "id": { + "name": "id", + "type": "text CHECK (\"id\" = upper(\"id\") AND length(\"id\") = 26 AND substr(\"id\", 1, 1) GLOB '[0-7]' AND \"id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "owner_account_id": { + "name": "owner_account_id", + "type": "text CHECK (\"owner_account_id\" = upper(\"owner_account_id\") AND length(\"owner_account_id\") = 26 AND substr(\"owner_account_id\", 1, 1) GLOB '[0-7]' AND \"owner_account_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "app_id": { + "name": "app_id", + "type": "text CHECK (\"app_id\" = upper(\"app_id\") AND length(\"app_id\") = 26 AND substr(\"app_id\", 1, 1) GLOB '[0-7]' AND \"app_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "environment_app_updated_at_idx": { + "name": "environment_app_updated_at_idx", + "columns": ["app_id", "updated_at"], + "isUnique": false + }, + "environment_owner_updated_at_idx": { + "name": "environment_owner_updated_at_idx", + "columns": ["owner_account_id", "updated_at"], + "isUnique": false + }, + "environment_owner_name_idx": { + "name": "environment_owner_name_idx", + "columns": ["app_id", "owner_account_id", "name"], + "isUnique": true, + "where": "\"environment\".\"owner_account_id\" IS NOT NULL" + }, + "environment_system_default_idx": { + "name": "environment_system_default_idx", + "columns": ["app_id"], + "isUnique": true, + "where": "\"environment\".\"owner_account_id\" IS NULL" + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "file_record": { + "name": "file_record", + "columns": { + "committed": { + "name": "committed", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_by_account_id": { + "name": "created_by_account_id", + "type": "text CHECK (\"created_by_account_id\" = upper(\"created_by_account_id\") AND length(\"created_by_account_id\") = 26 AND substr(\"created_by_account_id\", 1, 1) GLOB '[0-7]' AND \"created_by_account_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "etag": { + "name": "etag", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "id": { + "name": "id", + "type": "text CHECK (\"id\" = upper(\"id\") AND length(\"id\") = 26 AND substr(\"id\", 1, 1) GLOB '[0-7]' AND \"id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "mime_type": { + "name": "mime_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "object_key": { + "name": "object_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "owner_id": { + "name": "owner_id", + "type": "text CHECK (\"owner_id\" = upper(\"owner_id\") AND length(\"owner_id\") = 26 AND substr(\"owner_id\", 1, 1) GLOB '[0-7]' AND \"owner_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "owner_kind": { + "name": "owner_kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "parent_path": { + "name": "parent_path", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "path": { + "name": "path", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "purpose": { + "name": "purpose", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "scope_id": { + "name": "scope_id", + "type": "text CHECK (\"scope_id\" = upper(\"scope_id\") AND length(\"scope_id\") = 26 AND substr(\"scope_id\", 1, 1) GLOB '[0-7]' AND \"scope_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "scope_kind": { + "name": "scope_kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "session_kind": { + "name": "session_kind", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "size": { + "name": "size", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "version": { + "name": "version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "file_record_object_key_idx": { + "name": "file_record_object_key_idx", + "columns": ["object_key"], + "isUnique": true + }, + "file_record_unscoped_parent_path_name_status_idx": { + "name": "file_record_unscoped_parent_path_name_status_idx", + "columns": ["scope_kind", "parent_path", "name", "status"], + "isUnique": true, + "where": "\"file_record\".\"scope_id\" IS NULL" + }, + "file_record_scoped_parent_path_name_status_idx": { + "name": "file_record_scoped_parent_path_name_status_idx", + "columns": ["scope_kind", "scope_id", "parent_path", "name", "status"], + "isUnique": true + }, + "file_record_unscoped_pending_path_idx": { + "name": "file_record_unscoped_pending_path_idx", + "columns": ["scope_kind", "path"], + "isUnique": true, + "where": "\"file_record\".\"status\" = 'pending' AND \"file_record\".\"scope_id\" IS NULL" + }, + "file_record_scoped_pending_path_idx": { + "name": "file_record_scoped_pending_path_idx", + "columns": ["scope_kind", "scope_id", "path"], + "isUnique": true, + "where": "\"file_record\".\"status\" = 'pending' AND \"file_record\".\"scope_id\" IS NOT NULL" + }, + "file_record_unscoped_ready_path_idx": { + "name": "file_record_unscoped_ready_path_idx", + "columns": ["scope_kind", "path"], + "isUnique": true, + "where": "\"file_record\".\"status\" = 'ready' AND \"file_record\".\"scope_id\" IS NULL" + }, + "file_record_scoped_ready_path_idx": { + "name": "file_record_scoped_ready_path_idx", + "columns": ["scope_kind", "scope_id", "path"], + "isUnique": true, + "where": "\"file_record\".\"status\" = 'ready' AND \"file_record\".\"scope_id\" IS NOT NULL" + }, + "file_record_governance_idx": { + "name": "file_record_governance_idx", + "columns": ["purpose", "owner_kind", "owner_id", "status", "expires_at"], + "isUnique": false + }, + "file_record_listing_idx": { + "name": "file_record_listing_idx", + "columns": ["scope_kind", "scope_id", "parent_path", "status", "lower(\"name\")"], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "file_upload": { + "name": "file_upload", + "columns": { + "content_type": { + "name": "content_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_by_account_id": { + "name": "created_by_account_id", + "type": "text CHECK (\"created_by_account_id\" = upper(\"created_by_account_id\") AND length(\"created_by_account_id\") = 26 AND substr(\"created_by_account_id\", 1, 1) GLOB '[0-7]' AND \"created_by_account_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expected_size": { + "name": "expected_size", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "file_id": { + "name": "file_id", + "type": "text CHECK (\"file_id\" = upper(\"file_id\") AND length(\"file_id\") = 26 AND substr(\"file_id\", 1, 1) GLOB '[0-7]' AND \"file_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "id": { + "name": "id", + "type": "text CHECK (\"id\" = upper(\"id\") AND length(\"id\") = 26 AND substr(\"id\", 1, 1) GLOB '[0-7]' AND \"id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "if_match_etag": { + "name": "if_match_etag", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "multipart_upload_id": { + "name": "multipart_upload_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "overwrite": { + "name": "overwrite", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "part_size": { + "name": "part_size", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "scope_id": { + "name": "scope_id", + "type": "text CHECK (\"scope_id\" = upper(\"scope_id\") AND length(\"scope_id\") = 26 AND substr(\"scope_id\", 1, 1) GLOB '[0-7]' AND \"scope_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "scope_kind": { + "name": "scope_kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "strategy": { + "name": "strategy", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "file_upload_file_id_idx": { + "name": "file_upload_file_id_idx", + "columns": ["file_id"], + "isUnique": true + }, + "file_upload_status_expires_idx": { + "name": "file_upload_status_expires_idx", + "columns": ["status", "expires_at"], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "file_version": { + "name": "file_version", + "columns": { + "committed": { + "name": "committed", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "committed_at": { + "name": "committed_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_by_account_id": { + "name": "created_by_account_id", + "type": "text CHECK (\"created_by_account_id\" = upper(\"created_by_account_id\") AND length(\"created_by_account_id\") = 26 AND substr(\"created_by_account_id\", 1, 1) GLOB '[0-7]' AND \"created_by_account_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "file_id": { + "name": "file_id", + "type": "text CHECK (\"file_id\" = upper(\"file_id\") AND length(\"file_id\") = 26 AND substr(\"file_id\", 1, 1) GLOB '[0-7]' AND \"file_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "id": { + "name": "id", + "type": "text CHECK (\"id\" = upper(\"id\") AND length(\"id\") = 26 AND substr(\"id\", 1, 1) GLOB '[0-7]' AND \"id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "mime_type": { + "name": "mime_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "object_key": { + "name": "object_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "path": { + "name": "path", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "reason": { + "name": "reason", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "scope_id": { + "name": "scope_id", + "type": "text CHECK (\"scope_id\" = upper(\"scope_id\") AND length(\"scope_id\") = 26 AND substr(\"scope_id\", 1, 1) GLOB '[0-7]' AND \"scope_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "scope_kind": { + "name": "scope_kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "size": { + "name": "size", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "source_etag": { + "name": "source_etag", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "source_object_key": { + "name": "source_object_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "version": { + "name": "version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "file_version_object_key_idx": { + "name": "file_version_object_key_idx", + "columns": ["object_key"], + "isUnique": true + }, + "file_version_scope_path_created_idx": { + "name": "file_version_scope_path_created_idx", + "columns": ["scope_kind", "scope_id", "path", "created_at"], + "isUnique": false + }, + "file_version_file_created_idx": { + "name": "file_version_file_created_idx", + "columns": ["file_id", "created_at"], + "isUnique": false + }, + "file_version_pending_idx": { + "name": "file_version_pending_idx", + "columns": ["committed", "created_at"], + "isUnique": false, + "where": "\"file_version\".\"committed\" = 0" + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "mcp_credential": { + "name": "mcp_credential", + "columns": { + "account_id": { + "name": "account_id", + "type": "text CHECK (\"account_id\" = upper(\"account_id\") AND length(\"account_id\") = 26 AND substr(\"account_id\", 1, 1) GLOB '[0-7]' AND \"account_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "agent_id": { + "name": "agent_id", + "type": "text CHECK (\"agent_id\" = upper(\"agent_id\") AND length(\"agent_id\") = 26 AND substr(\"agent_id\", 1, 1) GLOB '[0-7]' AND \"agent_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "auth_type": { + "name": "auth_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "id": { + "name": "id", + "type": "text CHECK (\"id\" = upper(\"id\") AND length(\"id\") = 26 AND substr(\"id\", 1, 1) GLOB '[0-7]' AND \"id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "last_refreshed_at": { + "name": "last_refreshed_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "oauth_client_id": { + "name": "oauth_client_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "oauth_client_secret_secret_id": { + "name": "oauth_client_secret_secret_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "app_id": { + "name": "app_id", + "type": "text CHECK (\"app_id\" = upper(\"app_id\") AND length(\"app_id\") = 26 AND substr(\"app_id\", 1, 1) GLOB '[0-7]' AND \"app_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "refresh_secret_id": { + "name": "refresh_secret_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "scope_values_json": { + "name": "scope_values_json", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "secret_id": { + "name": "secret_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "server_id": { + "name": "server_id", + "type": "text CHECK (\"server_id\" = upper(\"server_id\") AND length(\"server_id\") = 26 AND substr(\"server_id\", 1, 1) GLOB '[0-7]' AND \"server_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "subject_label": { + "name": "subject_label", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "mcp_credential_server_scope_status_idx": { + "name": "mcp_credential_server_scope_status_idx", + "columns": ["server_id", "scope", "status"], + "isUnique": false + }, + "mcp_credential_app_scope_status_idx": { + "name": "mcp_credential_app_scope_status_idx", + "columns": ["app_id", "scope", "status"], + "isUnique": false + }, + "mcp_credential_app_scope_idx": { + "name": "mcp_credential_app_scope_idx", + "columns": ["server_id", "scope"], + "isUnique": true, + "where": "\"mcp_credential\".\"scope\" = 'app'" + }, + "mcp_credential_agent_scope_idx": { + "name": "mcp_credential_agent_scope_idx", + "columns": ["server_id", "agent_id", "scope"], + "isUnique": true, + "where": "\"mcp_credential\".\"scope\" = 'agent' AND \"mcp_credential\".\"agent_id\" IS NOT NULL" + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": { + "mcp_credential_scope_shape_check": { + "name": "mcp_credential_scope_shape_check", + "value": "\n (\"mcp_credential\".\"scope\" = 'app' AND \"mcp_credential\".\"account_id\" IS NULL AND \"mcp_credential\".\"agent_id\" IS NULL)\n OR (\"mcp_credential\".\"scope\" = 'agent' AND \"mcp_credential\".\"account_id\" IS NULL AND \"mcp_credential\".\"agent_id\" IS NOT NULL)\n " + }, + "mcp_credential_scope_values_json_check": { + "name": "mcp_credential_scope_values_json_check", + "value": "\n \"mcp_credential\".\"scope_values_json\" IS NULL\n OR (json_valid(\"mcp_credential\".\"scope_values_json\") AND json_type(\"mcp_credential\".\"scope_values_json\") = 'array')\n " + }, + "mcp_credential_bearer_shape_check": { + "name": "mcp_credential_bearer_shape_check", + "value": "\n \"mcp_credential\".\"auth_type\" != 'bearer'\n OR (\n \"mcp_credential\".\"oauth_client_id\" IS NULL\n AND \"mcp_credential\".\"oauth_client_secret_secret_id\" IS NULL\n AND \"mcp_credential\".\"refresh_secret_id\" IS NULL\n )\n " + } + } + }, + "mcp_oauth_flow": { + "name": "mcp_oauth_flow", + "columns": { + "authorization_endpoint": { + "name": "authorization_endpoint", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "cleanup_after": { + "name": "cleanup_after", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "code_verifier": { + "name": "code_verifier", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "completed_at": { + "name": "completed_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "id": { + "name": "id", + "type": "text CHECK (\"id\" = upper(\"id\") AND length(\"id\") = 26 AND substr(\"id\", 1, 1) GLOB '[0-7]' AND \"id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "initiator_account_id": { + "name": "initiator_account_id", + "type": "text CHECK (\"initiator_account_id\" = upper(\"initiator_account_id\") AND length(\"initiator_account_id\") = 26 AND substr(\"initiator_account_id\", 1, 1) GLOB '[0-7]' AND \"initiator_account_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "oauth_client_id": { + "name": "oauth_client_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "oauth_client_secret_secret_id": { + "name": "oauth_client_secret_secret_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "app_id": { + "name": "app_id", + "type": "text CHECK (\"app_id\" = upper(\"app_id\") AND length(\"app_id\") = 26 AND substr(\"app_id\", 1, 1) GLOB '[0-7]' AND \"app_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "registration_endpoint": { + "name": "registration_endpoint", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "return_url": { + "name": "return_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "scope_values_json": { + "name": "scope_values_json", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "server_id": { + "name": "server_id", + "type": "text CHECK (\"server_id\" = upper(\"server_id\") AND length(\"server_id\") = 26 AND substr(\"server_id\", 1, 1) GLOB '[0-7]' AND \"server_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "subject_label": { + "name": "subject_label", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "token_endpoint": { + "name": "token_endpoint", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "mcp_oauth_flow_status_cleanup_after_idx": { + "name": "mcp_oauth_flow_status_cleanup_after_idx", + "columns": ["status", "cleanup_after"], + "isUnique": false + }, + "mcp_oauth_flow_expires_at_idx": { + "name": "mcp_oauth_flow_expires_at_idx", + "columns": ["expires_at"], + "isUnique": false + }, + "mcp_oauth_flow_server_account_idx": { + "name": "mcp_oauth_flow_server_account_idx", + "columns": ["server_id", "initiator_account_id"], + "isUnique": false + }, + "mcp_oauth_flow_app_server_account_idx": { + "name": "mcp_oauth_flow_app_server_account_idx", + "columns": ["app_id", "server_id", "initiator_account_id"], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": { + "mcp_oauth_flow_scope_values_json_check": { + "name": "mcp_oauth_flow_scope_values_json_check", + "value": "\n \"mcp_oauth_flow\".\"scope_values_json\" IS NULL\n OR (json_valid(\"mcp_oauth_flow\".\"scope_values_json\") AND json_type(\"mcp_oauth_flow\".\"scope_values_json\") = 'array')\n " + } + } + }, + "mcp_server": { + "name": "mcp_server", + "columns": { + "auth_type": { + "name": "auth_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "byo_client_id": { + "name": "byo_client_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "byo_client_secret_secret_id": { + "name": "byo_client_secret_secret_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "credential_scope": { + "name": "credential_scope", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "enabled": { + "name": "enabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "icon_url": { + "name": "icon_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "id": { + "name": "id", + "type": "text CHECK (\"id\" = upper(\"id\") AND length(\"id\") = 26 AND substr(\"id\", 1, 1) GLOB '[0-7]' AND \"id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "oauth_metadata_json": { + "name": "oauth_metadata_json", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "owner_account_id": { + "name": "owner_account_id", + "type": "text CHECK (\"owner_account_id\" = upper(\"owner_account_id\") AND length(\"owner_account_id\") = 26 AND substr(\"owner_account_id\", 1, 1) GLOB '[0-7]' AND \"owner_account_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "app_id": { + "name": "app_id", + "type": "text CHECK (\"app_id\" = upper(\"app_id\") AND length(\"app_id\") = 26 AND substr(\"app_id\", 1, 1) GLOB '[0-7]' AND \"app_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "mcp_server_app_enabled_idx": { + "name": "mcp_server_app_enabled_idx", + "columns": ["app_id", "enabled"], + "isUnique": false + }, + "mcp_server_owner_app_idx": { + "name": "mcp_server_owner_app_idx", + "columns": ["owner_account_id", "app_id"], + "isUnique": false + }, + "mcp_server_app_url_idx": { + "name": "mcp_server_app_url_idx", + "columns": ["app_id", "url"], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": { + "mcp_server_source_scope_check": { + "name": "mcp_server_source_scope_check", + "value": "\"mcp_server\".\"source\" = 'app' AND \"mcp_server\".\"credential_scope\" = 'app'" + } + } + }, + "vault_secret": { + "name": "vault_secret", + "columns": { + "algorithm": { + "name": "algorithm", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'AES-GCM'" + }, + "ciphertext": { + "name": "ciphertext", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "ciphertext_iv": { + "name": "ciphertext_iv", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "id": { + "name": "id", + "type": "text CHECK (\"id\" = upper(\"id\") AND length(\"id\") = 26 AND substr(\"id\", 1, 1) GLOB '[0-7]' AND \"id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "wrapped_dek": { + "name": "wrapped_dek", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "wrapped_dek_iv": { + "name": "wrapped_dek_iv", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "vault_secret_kind_created_at_idx": { + "name": "vault_secret_kind_created_at_idx", + "columns": ["kind", "created_at"], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "organization": { + "name": "organization", + "columns": { + "avatar_url": { + "name": "avatar_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "creator_account_id": { + "name": "creator_account_id", + "type": "text CHECK (\"creator_account_id\" = upper(\"creator_account_id\") AND length(\"creator_account_id\") = 26 AND substr(\"creator_account_id\", 1, 1) GLOB '[0-7]' AND \"creator_account_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "id": { + "name": "id", + "type": "text CHECK (\"id\" = upper(\"id\") AND length(\"id\") = 26 AND substr(\"id\", 1, 1) GLOB '[0-7]' AND \"id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "organization_creator_account_idx": { + "name": "organization_creator_account_idx", + "columns": ["creator_account_id"], + "isUnique": true, + "where": "\"organization\".\"creator_account_id\" IS NOT NULL" + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "app_deployment_run": { + "name": "app_deployment_run", + "columns": { + "app_id": { + "name": "app_id", + "type": "text CHECK (\"app_id\" = upper(\"app_id\") AND length(\"app_id\") = 26 AND substr(\"app_id\", 1, 1) GLOB '[0-7]' AND \"app_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "deployment_id": { + "name": "deployment_id", + "type": "text CHECK (\"deployment_id\" = upper(\"deployment_id\") AND length(\"deployment_id\") = 26 AND substr(\"deployment_id\", 1, 1) GLOB '[0-7]' AND \"deployment_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "error_code": { + "name": "error_code", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "external_deployment_id": { + "name": "external_deployment_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "external_project_id": { + "name": "external_project_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "external_version_id": { + "name": "external_version_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "generated_wrangler_config_json": { + "name": "generated_wrangler_config_json", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "id": { + "name": "id", + "type": "text CHECK (\"id\" = upper(\"id\") AND length(\"id\") = 26 AND substr(\"id\", 1, 1) GLOB '[0-7]' AND \"id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "mosoo_config_json": { + "name": "mosoo_config_json", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "plan_json": { + "name": "plan_json", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "source_branch": { + "name": "source_branch", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "source_commit_sha": { + "name": "source_commit_sha", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "target_kind": { + "name": "target_kind", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "target_project_name": { + "name": "target_project_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "target_script_name": { + "name": "target_script_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "app_deployment_run_app_id_idx": { + "name": "app_deployment_run_app_id_idx", + "columns": ["app_id", "id"], + "isUnique": false + }, + "app_deployment_run_deployment_id_idx": { + "name": "app_deployment_run_deployment_id_idx", + "columns": ["deployment_id", "id"], + "isUnique": false + }, + "app_deployment_run_active_app_idx": { + "name": "app_deployment_run_active_app_idx", + "columns": ["app_id"], + "isUnique": true, + "where": "\"app_deployment_run\".\"status\" IN ('queued', 'preparing', 'building', 'submitting', 'submitted', 'activating')" + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": { + "app_deployment_run_status_check": { + "name": "app_deployment_run_status_check", + "value": "\"app_deployment_run\".\"status\" IN ('queued', 'preparing', 'building', 'submitting', 'submitted', 'activating', 'success', 'failed')" + }, + "app_deployment_run_target_kind_check": { + "name": "app_deployment_run_target_kind_check", + "value": "\"app_deployment_run\".\"target_kind\" IS NULL OR \"app_deployment_run\".\"target_kind\" IN ('cloudflare_pages', 'cloudflare_worker')" + } + } + }, + "app_deployment_secret": { + "name": "app_deployment_secret", + "columns": { + "app_id": { + "name": "app_id", + "type": "text CHECK (\"app_id\" = upper(\"app_id\") AND length(\"app_id\") = 26 AND substr(\"app_id\", 1, 1) GLOB '[0-7]' AND \"app_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "vault_secret_id": { + "name": "vault_secret_id", + "type": "text CHECK (\"vault_secret_id\" = upper(\"vault_secret_id\") AND length(\"vault_secret_id\") = 26 AND substr(\"vault_secret_id\", 1, 1) GLOB '[0-7]' AND \"vault_secret_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "app_deployment_secret_app_name_idx": { + "name": "app_deployment_secret_app_name_idx", + "columns": ["app_id", "name"], + "isUnique": true + }, + "app_deployment_secret_vault_secret_idx": { + "name": "app_deployment_secret_vault_secret_idx", + "columns": ["vault_secret_id"], + "isUnique": true + } + }, + "foreignKeys": { + "app_deployment_secret_vault_secret_id_vault_secret_id_fk": { + "name": "app_deployment_secret_vault_secret_id_vault_secret_id_fk", + "tableFrom": "app_deployment_secret", + "tableTo": "vault_secret", + "columnsFrom": ["vault_secret_id"], + "columnsTo": ["id"], + "onDelete": "restrict", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "app_deployment": { + "name": "app_deployment", + "columns": { + "app_id": { + "name": "app_id", + "type": "text CHECK (\"app_id\" = upper(\"app_id\") AND length(\"app_id\") = 26 AND substr(\"app_id\", 1, 1) GLOB '[0-7]' AND \"app_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "default_branch": { + "name": "default_branch", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "deleted_at": { + "name": "deleted_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "id": { + "name": "id", + "type": "text CHECK (\"id\" = upper(\"id\") AND length(\"id\") = 26 AND substr(\"id\", 1, 1) GLOB '[0-7]' AND \"id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "last_successful_url": { + "name": "last_successful_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "latest_run_id": { + "name": "latest_run_id", + "type": "text CHECK (\"latest_run_id\" = upper(\"latest_run_id\") AND length(\"latest_run_id\") = 26 AND substr(\"latest_run_id\", 1, 1) GLOB '[0-7]' AND \"latest_run_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "mosoo_subdomain": { + "name": "mosoo_subdomain", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "owner_account_id": { + "name": "owner_account_id", + "type": "text CHECK (\"owner_account_id\" = upper(\"owner_account_id\") AND length(\"owner_account_id\") = 26 AND substr(\"owner_account_id\", 1, 1) GLOB '[0-7]' AND \"owner_account_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "repo_name": { + "name": "repo_name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "repo_owner": { + "name": "repo_owner", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "repo_url": { + "name": "repo_url", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "source_kind": { + "name": "source_kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "app_deployment_active_app_idx": { + "name": "app_deployment_active_app_idx", + "columns": ["app_id"], + "isUnique": true, + "where": "\"app_deployment\".\"deleted_at\" IS NULL" + }, + "app_deployment_active_subdomain_idx": { + "name": "app_deployment_active_subdomain_idx", + "columns": ["mosoo_subdomain"], + "isUnique": true, + "where": "\"app_deployment\".\"deleted_at\" IS NULL" + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": { + "app_deployment_source_kind_check": { + "name": "app_deployment_source_kind_check", + "value": "\"app_deployment\".\"source_kind\" IN ('github_public')" + } + } + }, + "app": { + "name": "app", + "columns": { + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "default_environment_id": { + "name": "default_environment_id", + "type": "text CHECK (\"default_environment_id\" = upper(\"default_environment_id\") AND length(\"default_environment_id\") = 26 AND substr(\"default_environment_id\", 1, 1) GLOB '[0-7]' AND \"default_environment_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "id": { + "name": "id", + "type": "text CHECK (\"id\" = upper(\"id\") AND length(\"id\") = 26 AND substr(\"id\", 1, 1) GLOB '[0-7]' AND \"id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "organization_id": { + "name": "organization_id", + "type": "text CHECK (\"organization_id\" = upper(\"organization_id\") AND length(\"organization_id\") = 26 AND substr(\"organization_id\", 1, 1) GLOB '[0-7]' AND \"organization_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "owner_account_id": { + "name": "owner_account_id", + "type": "text CHECK (\"owner_account_id\" = upper(\"owner_account_id\") AND length(\"owner_account_id\") = 26 AND substr(\"owner_account_id\", 1, 1) GLOB '[0-7]' AND \"owner_account_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "bound_agent_call_idempotency_key": { + "name": "bound_agent_call_idempotency_key", + "columns": { + "body_hash": { + "name": "body_hash", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "id": { + "name": "id", + "type": "text CHECK (\"id\" = upper(\"id\") AND length(\"id\") = 26 AND substr(\"id\", 1, 1) GLOB '[0-7]' AND \"id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "idempotency_key": { + "name": "idempotency_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "run_id": { + "name": "run_id", + "type": "text CHECK (\"run_id\" = upper(\"run_id\") AND length(\"run_id\") = 26 AND substr(\"run_id\", 1, 1) GLOB '[0-7]' AND \"run_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "session_id": { + "name": "session_id", + "type": "text CHECK (\"session_id\" = upper(\"session_id\") AND length(\"session_id\") = 26 AND substr(\"session_id\", 1, 1) GLOB '[0-7]' AND \"session_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "subject_hash": { + "name": "subject_hash", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "bound_agent_call_idempotency_subject_key_idx": { + "name": "bound_agent_call_idempotency_subject_key_idx", + "columns": ["subject_hash", "idempotency_key"], + "isUnique": true + }, + "bound_agent_call_idempotency_updated_idx": { + "name": "bound_agent_call_idempotency_updated_idx", + "columns": ["updated_at"], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "public_api_idempotency_key": { + "name": "public_api_idempotency_key", + "columns": { + "body_hash": { + "name": "body_hash", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "id": { + "name": "id", + "type": "text CHECK (\"id\" = upper(\"id\") AND length(\"id\") = 26 AND substr(\"id\", 1, 1) GLOB '[0-7]' AND \"id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "idempotency_key": { + "name": "idempotency_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "method": { + "name": "method", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "response_json": { + "name": "response_json", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "response_status": { + "name": "response_status", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "route": { + "name": "route", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "token_id": { + "name": "token_id", + "type": "text CHECK (\"token_id\" = upper(\"token_id\") AND length(\"token_id\") = 26 AND substr(\"token_id\", 1, 1) GLOB '[0-7]' AND \"token_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "public_api_idempotency_token_key_idx": { + "name": "public_api_idempotency_token_key_idx", + "columns": ["token_id", "idempotency_key"], + "isUnique": true + }, + "public_api_idempotency_updated_idx": { + "name": "public_api_idempotency_updated_idx", + "columns": ["updated_at"], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "public_api_rate_limit_window": { + "name": "public_api_rate_limit_window", + "columns": { + "bucket_key": { + "name": "bucket_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "request_count": { + "name": "request_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "shard": { + "name": "shard", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "window_start": { + "name": "window_start", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "public_api_rate_limit_window_updated_idx": { + "name": "public_api_rate_limit_window_updated_idx", + "columns": ["updated_at"], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "public_api_rate_limit_window_bucket_key_window_start_shard_pk": { + "columns": ["bucket_key", "window_start", "shard"], + "name": "public_api_rate_limit_window_bucket_key_window_start_shard_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "driver_command": { + "name": "driver_command", + "columns": { + "acked_at": { + "name": "acked_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "completed_at": { + "name": "completed_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "delivery_connection_id": { + "name": "delivery_connection_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "driver_generation": { + "name": "driver_generation", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "driver_instance_id": { + "name": "driver_instance_id", + "type": "text CHECK (\"driver_instance_id\" = upper(\"driver_instance_id\") AND length(\"driver_instance_id\") = 26 AND substr(\"driver_instance_id\", 1, 1) GLOB '[0-7]' AND \"driver_instance_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "error_json": { + "name": "error_json", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "id": { + "name": "id", + "type": "text CHECK (\"id\" = upper(\"id\") AND length(\"id\") = 26 AND substr(\"id\", 1, 1) GLOB '[0-7]' AND \"id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "issued_at": { + "name": "issued_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "payload_json": { + "name": "payload_json", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "result_json": { + "name": "result_json", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "seq": { + "name": "seq", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "driver_command_instance_seq_idx": { + "name": "driver_command_instance_seq_idx", + "columns": ["driver_instance_id", "seq"], + "isUnique": true + }, + "driver_command_instance_status_idx": { + "name": "driver_command_instance_status_idx", + "columns": ["driver_instance_id", "status", "expires_at"], + "isUnique": false + } + }, + "foreignKeys": { + "driver_command_driver_instance_id_driver_instance_id_fk": { + "name": "driver_command_driver_instance_id_driver_instance_id_fk", + "tableFrom": "driver_command", + "tableTo": "driver_instance", + "columnsFrom": ["driver_instance_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": { + "driver_command_generation_check": { + "name": "driver_command_generation_check", + "value": "\"driver_command\".\"driver_generation\" IS NULL OR (typeof(\"driver_command\".\"driver_generation\") = 'integer' AND \"driver_command\".\"driver_generation\" BETWEEN 0 AND 9007199254740991)" + }, + "driver_command_nonterminal_generation_check": { + "name": "driver_command_nonterminal_generation_check", + "value": "\"driver_command\".\"status\" IN ('completed', 'failed', 'expired', 'cancelled') OR \"driver_command\".\"driver_generation\" IS NOT NULL" + } + } + }, + "driver_instance_mcp_grant": { + "name": "driver_instance_mcp_grant", + "columns": { + "auth_type": { + "name": "auth_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "authorization_state": { + "name": "authorization_state", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "can_invalidate": { + "name": "can_invalidate", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "can_refresh": { + "name": "can_refresh", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "credential_id": { + "name": "credential_id", + "type": "text CHECK (\"credential_id\" = upper(\"credential_id\") AND length(\"credential_id\") = 26 AND substr(\"credential_id\", 1, 1) GLOB '[0-7]' AND \"credential_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "driver_instance_id": { + "name": "driver_instance_id", + "type": "text CHECK (\"driver_instance_id\" = upper(\"driver_instance_id\") AND length(\"driver_instance_id\") = 26 AND substr(\"driver_instance_id\", 1, 1) GLOB '[0-7]' AND \"driver_instance_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "app_id": { + "name": "app_id", + "type": "text CHECK (\"app_id\" = upper(\"app_id\") AND length(\"app_id\") = 26 AND substr(\"app_id\", 1, 1) GLOB '[0-7]' AND \"app_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "server_id": { + "name": "server_id", + "type": "text CHECK (\"server_id\" = upper(\"server_id\") AND length(\"server_id\") = 26 AND substr(\"server_id\", 1, 1) GLOB '[0-7]' AND \"server_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "driver_instance_mcp_grant_instance_server_idx": { + "name": "driver_instance_mcp_grant_instance_server_idx", + "columns": ["driver_instance_id", "server_id"], + "isUnique": true + }, + "driver_instance_mcp_grant_instance_credential_idx": { + "name": "driver_instance_mcp_grant_instance_credential_idx", + "columns": ["driver_instance_id", "credential_id"], + "isUnique": false + } + }, + "foreignKeys": { + "driver_instance_mcp_grant_driver_instance_id_driver_instance_id_fk": { + "name": "driver_instance_mcp_grant_driver_instance_id_driver_instance_id_fk", + "tableFrom": "driver_instance_mcp_grant", + "tableTo": "driver_instance", + "columnsFrom": ["driver_instance_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "driver_instance": { + "name": "driver_instance", + "columns": { + "boot_token_expires_at": { + "name": "boot_token_expires_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "boot_token_hash": { + "name": "boot_token_hash", + "type": "blob", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "boot_token_used_at": { + "name": "boot_token_used_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "close_code": { + "name": "close_code", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "close_reason": { + "name": "close_reason", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "connection_id": { + "name": "connection_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "command_seq_cursor": { + "name": "command_seq_cursor", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "driver_pid": { + "name": "driver_pid", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "driver_started_at": { + "name": "driver_started_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "driver_version": { + "name": "driver_version", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "heartbeat_count": { + "name": "heartbeat_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "generation": { + "name": "generation", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "id": { + "name": "id", + "type": "text CHECK (\"id\" = upper(\"id\") AND length(\"id\") = 26 AND substr(\"id\", 1, 1) GLOB '[0-7]' AND \"id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "last_heartbeat_at": { + "name": "last_heartbeat_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "process_id": { + "name": "process_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "protocol": { + "name": "protocol", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "protocol_version": { + "name": "protocol_version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "restart_count": { + "name": "restart_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "runtime": { + "name": "runtime", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "sandbox_id": { + "name": "sandbox_id", + "type": "text CHECK (\"sandbox_id\" = upper(\"sandbox_id\") AND length(\"sandbox_id\") = 26 AND substr(\"sandbox_id\", 1, 1) GLOB '[0-7]' AND \"sandbox_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "sandbox_session_id": { + "name": "sandbox_session_id", + "type": "text CHECK (\"sandbox_session_id\" = upper(\"sandbox_session_id\") AND length(\"sandbox_session_id\") = 26 AND substr(\"sandbox_session_id\", 1, 1) GLOB '[0-7]' AND \"sandbox_session_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status_changed_at": { + "name": "status_changed_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "status_event": { + "name": "status_event", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'driver.provision'" + }, + "status_operation_id": { + "name": "status_operation_id", + "type": "text CHECK (\"status_operation_id\" = upper(\"status_operation_id\") AND length(\"status_operation_id\") = 26 AND substr(\"status_operation_id\", 1, 1) GLOB '[0-7]' AND \"status_operation_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status_seq": { + "name": "status_seq", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "status_source": { + "name": "status_source", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'system'" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "driver_instance_completed_idx": { + "name": "driver_instance_completed_idx", + "columns": ["expires_at", "status"], + "isUnique": false + }, + "driver_instance_connection_idx": { + "name": "driver_instance_connection_idx", + "columns": ["connection_id"], + "isUnique": true, + "where": "\"driver_instance\".\"connection_id\" IS NOT NULL" + }, + "driver_instance_boot_token_expiry_idx": { + "name": "driver_instance_boot_token_expiry_idx", + "columns": ["status", "boot_token_expires_at"], + "isUnique": false, + "where": "\"driver_instance\".\"boot_token_used_at\" IS NULL" + }, + "driver_instance_boot_token_hash_idx": { + "name": "driver_instance_boot_token_hash_idx", + "columns": ["boot_token_hash"], + "isUnique": true + }, + "driver_instance_sandbox_session_idx": { + "name": "driver_instance_sandbox_session_idx", + "columns": ["sandbox_id", "sandbox_session_id", "status", "updated_at"], + "isUnique": false + }, + "driver_instance_live_sandbox_session_idx": { + "name": "driver_instance_live_sandbox_session_idx", + "columns": ["sandbox_id", "sandbox_session_id"], + "isUnique": true, + "where": "\"driver_instance\".\"status\" IN ('provisioning', 'connecting', 'ready', 'stopping')" + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": { + "driver_instance_status_check": { + "name": "driver_instance_status_check", + "value": "\"driver_instance\".\"status\" IN ('provisioning', 'connecting', 'ready', 'stopping', 'stopped', 'failed')" + }, + "driver_instance_status_seq_check": { + "name": "driver_instance_status_seq_check", + "value": "\"driver_instance\".\"status_seq\" >= 0" + } + } + }, + "external_tool_effect_attempt": { + "name": "external_tool_effect_attempt", + "columns": { + "attempt": { + "name": "attempt", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "claim_token": { + "name": "claim_token", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "completed_at": { + "name": "completed_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "effect_id": { + "name": "effect_id", + "type": "text CHECK (\"effect_id\" = upper(\"effect_id\") AND length(\"effect_id\") = 26 AND substr(\"effect_id\", 1, 1) GLOB '[0-7]' AND \"effect_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "provider_receipt_json": { + "name": "provider_receipt_json", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "result_json": { + "name": "result_json", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "external_tool_effect_attempt_status_idx": { + "name": "external_tool_effect_attempt_status_idx", + "columns": ["status", "created_at"], + "isUnique": false + } + }, + "foreignKeys": { + "external_tool_effect_attempt_effect_id_external_tool_effect_id_fk": { + "name": "external_tool_effect_attempt_effect_id_external_tool_effect_id_fk", + "tableFrom": "external_tool_effect_attempt", + "tableTo": "external_tool_effect", + "columnsFrom": ["effect_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "external_tool_effect_attempt_effect_id_attempt_pk": { + "columns": ["effect_id", "attempt"], + "name": "external_tool_effect_attempt_effect_id_attempt_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": { + "external_tool_effect_attempt_status_check": { + "name": "external_tool_effect_attempt_status_check", + "value": "\"external_tool_effect_attempt\".\"status\" IN ('claimed', 'succeeded', 'unknown')" + }, + "external_tool_effect_attempt_claim_token_uuid_check": { + "name": "external_tool_effect_attempt_claim_token_uuid_check", + "value": "length(\"external_tool_effect_attempt\".\"claim_token\") = 36 AND length(replace(\"external_tool_effect_attempt\".\"claim_token\", '-', '')) = 32 AND \"external_tool_effect_attempt\".\"claim_token\" = lower(\"external_tool_effect_attempt\".\"claim_token\") AND substr(\"external_tool_effect_attempt\".\"claim_token\", 9, 1) = '-' AND substr(\"external_tool_effect_attempt\".\"claim_token\", 14, 1) = '-' AND substr(\"external_tool_effect_attempt\".\"claim_token\", 15, 1) = '4' AND substr(\"external_tool_effect_attempt\".\"claim_token\", 19, 1) = '-' AND substr(\"external_tool_effect_attempt\".\"claim_token\", 20, 1) GLOB '[89ab]' AND substr(\"external_tool_effect_attempt\".\"claim_token\", 24, 1) = '-' AND replace(\"external_tool_effect_attempt\".\"claim_token\", '-', '') NOT GLOB '*[^0-9a-f]*'" + } + } + }, + "external_tool_effect": { + "name": "external_tool_effect", + "columns": { + "attempt_count": { + "name": "attempt_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "claim_token": { + "name": "claim_token", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "command_id": { + "name": "command_id", + "type": "text CHECK (\"command_id\" = upper(\"command_id\") AND length(\"command_id\") = 26 AND substr(\"command_id\", 1, 1) GLOB '[0-7]' AND \"command_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "driver_instance_id": { + "name": "driver_instance_id", + "type": "text CHECK (\"driver_instance_id\" = upper(\"driver_instance_id\") AND length(\"driver_instance_id\") = 26 AND substr(\"driver_instance_id\", 1, 1) GLOB '[0-7]' AND \"driver_instance_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "id": { + "name": "id", + "type": "text CHECK (\"id\" = upper(\"id\") AND length(\"id\") = 26 AND substr(\"id\", 1, 1) GLOB '[0-7]' AND \"id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "idempotency_key": { + "name": "idempotency_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "provider_receipt_json": { + "name": "provider_receipt_json", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "result_json": { + "name": "result_json", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "server_id": { + "name": "server_id", + "type": "text CHECK (\"server_id\" = upper(\"server_id\") AND length(\"server_id\") = 26 AND substr(\"server_id\", 1, 1) GLOB '[0-7]' AND \"server_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "session_run_id": { + "name": "session_run_id", + "type": "text CHECK (\"session_run_id\" = upper(\"session_run_id\") AND length(\"session_run_id\") = 26 AND substr(\"session_run_id\", 1, 1) GLOB '[0-7]' AND \"session_run_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "tool_name": { + "name": "tool_name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "external_tool_effect_command_idx": { + "name": "external_tool_effect_command_idx", + "columns": ["command_id"], + "isUnique": true + }, + "external_tool_effect_idempotency_key_idx": { + "name": "external_tool_effect_idempotency_key_idx", + "columns": ["idempotency_key"], + "isUnique": true + }, + "external_tool_effect_run_status_idx": { + "name": "external_tool_effect_run_status_idx", + "columns": ["session_run_id", "status", "id"], + "isUnique": false + }, + "external_tool_effect_driver_status_idx": { + "name": "external_tool_effect_driver_status_idx", + "columns": ["driver_instance_id", "status"], + "isUnique": false + } + }, + "foreignKeys": { + "external_tool_effect_command_id_driver_command_id_fk": { + "name": "external_tool_effect_command_id_driver_command_id_fk", + "tableFrom": "external_tool_effect", + "tableTo": "driver_command", + "columnsFrom": ["command_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "external_tool_effect_driver_instance_id_driver_instance_id_fk": { + "name": "external_tool_effect_driver_instance_id_driver_instance_id_fk", + "tableFrom": "external_tool_effect", + "tableTo": "driver_instance", + "columnsFrom": ["driver_instance_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "external_tool_effect_session_run_id_session_run_id_fk": { + "name": "external_tool_effect_session_run_id_session_run_id_fk", + "tableFrom": "external_tool_effect", + "tableTo": "session_run", + "columnsFrom": ["session_run_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": { + "external_tool_effect_status_check": { + "name": "external_tool_effect_status_check", + "value": "\"external_tool_effect\".\"status\" IN ('intent', 'claimed', 'succeeded', 'unknown')" + }, + "external_tool_effect_claim_token_uuid_check": { + "name": "external_tool_effect_claim_token_uuid_check", + "value": "\"external_tool_effect\".\"claim_token\" IS NULL OR (length(\"external_tool_effect\".\"claim_token\") = 36 AND length(replace(\"external_tool_effect\".\"claim_token\", '-', '')) = 32 AND \"external_tool_effect\".\"claim_token\" = lower(\"external_tool_effect\".\"claim_token\") AND substr(\"external_tool_effect\".\"claim_token\", 9, 1) = '-' AND substr(\"external_tool_effect\".\"claim_token\", 14, 1) = '-' AND substr(\"external_tool_effect\".\"claim_token\", 15, 1) = '4' AND substr(\"external_tool_effect\".\"claim_token\", 19, 1) = '-' AND substr(\"external_tool_effect\".\"claim_token\", 20, 1) GLOB '[89ab]' AND substr(\"external_tool_effect\".\"claim_token\", 24, 1) = '-' AND replace(\"external_tool_effect\".\"claim_token\", '-', '') NOT GLOB '*[^0-9a-f]*')" + } + } + }, + "native_resume_ref": { + "name": "native_resume_ref", + "columns": { + "committed_session_run_id": { + "name": "committed_session_run_id", + "type": "text CHECK (\"committed_session_run_id\" = upper(\"committed_session_run_id\") AND length(\"committed_session_run_id\") = 26 AND substr(\"committed_session_run_id\", 1, 1) GLOB '[0-7]' AND \"committed_session_run_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "committed_value": { + "name": "committed_value", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "observed_driver_instance_id": { + "name": "observed_driver_instance_id", + "type": "text CHECK (\"observed_driver_instance_id\" = upper(\"observed_driver_instance_id\") AND length(\"observed_driver_instance_id\") = 26 AND substr(\"observed_driver_instance_id\", 1, 1) GLOB '[0-7]' AND \"observed_driver_instance_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "observed_session_run_id": { + "name": "observed_session_run_id", + "type": "text CHECK (\"observed_session_run_id\" = upper(\"observed_session_run_id\") AND length(\"observed_session_run_id\") = 26 AND substr(\"observed_session_run_id\", 1, 1) GLOB '[0-7]' AND \"observed_session_run_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "runtime_id": { + "name": "runtime_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "session_id": { + "name": "session_id", + "type": "text CHECK (\"session_id\" = upper(\"session_id\") AND length(\"session_id\") = 26 AND substr(\"session_id\", 1, 1) GLOB '[0-7]' AND \"session_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "native_resume_ref_runtime_updated_idx": { + "name": "native_resume_ref_runtime_updated_idx", + "columns": ["runtime_id", "updated_at"], + "isUnique": false + } + }, + "foreignKeys": { + "native_resume_ref_session_id_session_id_fk": { + "name": "native_resume_ref_session_id_session_id_fk", + "tableFrom": "native_resume_ref", + "tableTo": "session", + "columnsFrom": ["session_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "sandbox_backup": { + "name": "sandbox_backup", + "columns": { + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "dir": { + "name": "dir", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "id": { + "name": "id", + "type": "text CHECK (\"id\" = upper(\"id\") AND length(\"id\") = 26 AND substr(\"id\", 1, 1) GLOB '[0-7]' AND \"id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "keep": { + "name": "keep", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "sandbox_id": { + "name": "sandbox_id", + "type": "text CHECK (\"sandbox_id\" = upper(\"sandbox_id\") AND length(\"sandbox_id\") = 26 AND substr(\"sandbox_id\", 1, 1) GLOB '[0-7]' AND \"sandbox_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "session_run_id": { + "name": "session_run_id", + "type": "text CHECK (\"session_run_id\" = upper(\"session_run_id\") AND length(\"session_run_id\") = 26 AND substr(\"session_run_id\", 1, 1) GLOB '[0-7]' AND \"session_run_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "ttl_seconds": { + "name": "ttl_seconds", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "sandbox_backup_sandbox_status_created_idx": { + "name": "sandbox_backup_sandbox_status_created_idx", + "columns": ["sandbox_id", "status", "created_at"], + "isUnique": false + }, + "sandbox_backup_terminal_checkpoint_idx": { + "name": "sandbox_backup_terminal_checkpoint_idx", + "columns": ["sandbox_id", "dir", "session_run_id"], + "isUnique": true, + "where": "\"sandbox_backup\".\"session_run_id\" IS NOT NULL AND \"sandbox_backup\".\"status\" = 'ready'" + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "sandbox_session": { + "name": "sandbox_session", + "columns": { + "cloudflare_session_id": { + "name": "cloudflare_session_id", + "type": "text CHECK (\"cloudflare_session_id\" = upper(\"cloudflare_session_id\") AND length(\"cloudflare_session_id\") = 26 AND substr(\"cloudflare_session_id\", 1, 1) GLOB '[0-7]' AND \"cloudflare_session_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "cwd": { + "name": "cwd", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "origin_json": { + "name": "origin_json", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "sandbox_id": { + "name": "sandbox_id", + "type": "text CHECK (\"sandbox_id\" = upper(\"sandbox_id\") AND length(\"sandbox_id\") = 26 AND substr(\"sandbox_id\", 1, 1) GLOB '[0-7]' AND \"sandbox_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "session_id": { + "name": "session_id", + "type": "text CHECK (\"session_id\" = upper(\"session_id\") AND length(\"session_id\") = 26 AND substr(\"session_id\", 1, 1) GLOB '[0-7]' AND \"session_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "sandbox_session_sandbox_status_idx": { + "name": "sandbox_session_sandbox_status_idx", + "columns": ["sandbox_id", "status", "updated_at"], + "isUnique": false + }, + "sandbox_session_cloudflare_session_idx": { + "name": "sandbox_session_cloudflare_session_idx", + "columns": ["cloudflare_session_id"], + "isUnique": true + } + }, + "foreignKeys": { + "sandbox_session_session_id_session_id_fk": { + "name": "sandbox_session_session_id_session_id_fk", + "tableFrom": "sandbox_session", + "tableTo": "session", + "columnsFrom": ["session_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "sandbox": { + "name": "sandbox", + "columns": { + "agent_id": { + "name": "agent_id", + "type": "text CHECK (\"agent_id\" = upper(\"agent_id\") AND length(\"agent_id\") = 26 AND substr(\"agent_id\", 1, 1) GLOB '[0-7]' AND \"agent_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "app_id": { + "name": "app_id", + "type": "text CHECK (\"app_id\" = upper(\"app_id\") AND length(\"app_id\") = 26 AND substr(\"app_id\", 1, 1) GLOB '[0-7]' AND \"app_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "bind_mount_ready": { + "name": "bind_mount_ready", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "claim_expires_at": { + "name": "claim_expires_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "claim_owner": { + "name": "claim_owner", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "global_mounts_json": { + "name": "global_mounts_json", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'[]'" + }, + "id": { + "name": "id", + "type": "text CHECK (\"id\" = upper(\"id\") AND length(\"id\") = 26 AND substr(\"id\", 1, 1) GLOB '[0-7]' AND \"id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "inactive_deadline_at": { + "name": "inactive_deadline_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "last_backup_id": { + "name": "last_backup_id", + "type": "text CHECK (\"last_backup_id\" = upper(\"last_backup_id\") AND length(\"last_backup_id\") = 26 AND substr(\"last_backup_id\", 1, 1) GLOB '[0-7]' AND \"last_backup_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_error_code": { + "name": "last_error_code", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_restore_backup_id": { + "name": "last_restore_backup_id", + "type": "text CHECK (\"last_restore_backup_id\" = upper(\"last_restore_backup_id\") AND length(\"last_restore_backup_id\") = 26 AND substr(\"last_restore_backup_id\", 1, 1) GLOB '[0-7]' AND \"last_restore_backup_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "owner_account_id": { + "name": "owner_account_id", + "type": "text CHECK (\"owner_account_id\" = upper(\"owner_account_id\") AND length(\"owner_account_id\") = 26 AND substr(\"owner_account_id\", 1, 1) GLOB '[0-7]' AND \"owner_account_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status_changed_at": { + "name": "status_changed_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "status_event": { + "name": "status_event", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'runtime_subject.cold'" + }, + "status_operation_id": { + "name": "status_operation_id", + "type": "text CHECK (\"status_operation_id\" = upper(\"status_operation_id\") AND length(\"status_operation_id\") = 26 AND substr(\"status_operation_id\", 1, 1) GLOB '[0-7]' AND \"status_operation_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status_seq": { + "name": "status_seq", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "status_source": { + "name": "status_source", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'system'" + }, + "subject_id": { + "name": "subject_id", + "type": "text CHECK (\"subject_id\" = upper(\"subject_id\") AND length(\"subject_id\") = 26 AND substr(\"subject_id\", 1, 1) GLOB '[0-7]' AND \"subject_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "subject_kind": { + "name": "subject_kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "sandbox_subject_idx": { + "name": "sandbox_subject_idx", + "columns": ["kind", "subject_kind", "subject_id"], + "isUnique": true + }, + "sandbox_status_deadline_idx": { + "name": "sandbox_status_deadline_idx", + "columns": ["status", "inactive_deadline_at", "updated_at"], + "isUnique": false + }, + "sandbox_claim_idx": { + "name": "sandbox_claim_idx", + "columns": ["claim_expires_at", "claim_owner"], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": { + "sandbox_status_check": { + "name": "sandbox_status_check", + "value": "\"sandbox\".\"status\" IN ('cold', 'restoring', 'active', 'backing_up', 'destroying', 'error')" + }, + "sandbox_status_seq_check": { + "name": "sandbox_status_seq_check", + "value": "\"sandbox\".\"status_seq\" >= 0" + } + } + }, + "session_message": { + "name": "session_message", + "columns": { + "content_text": { + "name": "content_text", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_by_account_id": { + "name": "created_by_account_id", + "type": "text CHECK (\"created_by_account_id\" = upper(\"created_by_account_id\") AND length(\"created_by_account_id\") = 26 AND substr(\"created_by_account_id\", 1, 1) GLOB '[0-7]' AND \"created_by_account_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "id": { + "name": "id", + "type": "text CHECK (\"id\" = upper(\"id\") AND length(\"id\") = 26 AND substr(\"id\", 1, 1) GLOB '[0-7]' AND \"id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "plan_json": { + "name": "plan_json", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "segments_json": { + "name": "segments_json", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "seq": { + "name": "seq", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "session_id": { + "name": "session_id", + "type": "text CHECK (\"session_id\" = upper(\"session_id\") AND length(\"session_id\") = 26 AND substr(\"session_id\", 1, 1) GLOB '[0-7]' AND \"session_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "session_run_id": { + "name": "session_run_id", + "type": "text CHECK (\"session_run_id\" = upper(\"session_run_id\") AND length(\"session_run_id\") = 26 AND substr(\"session_run_id\", 1, 1) GLOB '[0-7]' AND \"session_run_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "session_message_session_seq_idx": { + "name": "session_message_session_seq_idx", + "columns": ["session_id", "seq"], + "isUnique": true + }, + "session_message_run_idx": { + "name": "session_message_run_idx", + "columns": ["session_run_id"], + "isUnique": false + } + }, + "foreignKeys": { + "session_message_session_id_session_id_fk": { + "name": "session_message_session_id_session_id_fk", + "tableFrom": "session_message", + "tableTo": "session", + "columnsFrom": ["session_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "session": { + "name": "session", + "columns": { + "agent_id": { + "name": "agent_id", + "type": "text CHECK (\"agent_id\" = upper(\"agent_id\") AND length(\"agent_id\") = 26 AND substr(\"agent_id\", 1, 1) GLOB '[0-7]' AND \"agent_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "archived_at": { + "name": "archived_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "end_user_id": { + "name": "end_user_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "attributed_user_id": { + "name": "attributed_user_id", + "type": "text CHECK (\"attributed_user_id\" = upper(\"attributed_user_id\") AND length(\"attributed_user_id\") = 26 AND substr(\"attributed_user_id\", 1, 1) GLOB '[0-7]' AND \"attributed_user_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "creator_account_id": { + "name": "creator_account_id", + "type": "text CHECK (\"creator_account_id\" = upper(\"creator_account_id\") AND length(\"creator_account_id\") = 26 AND substr(\"creator_account_id\", 1, 1) GLOB '[0-7]' AND \"creator_account_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "deployment_version_id": { + "name": "deployment_version_id", + "type": "text CHECK (\"deployment_version_id\" = upper(\"deployment_version_id\") AND length(\"deployment_version_id\") = 26 AND substr(\"deployment_version_id\", 1, 1) GLOB '[0-7]' AND \"deployment_version_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "deployment_version_number": { + "name": "deployment_version_number", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "id": { + "name": "id", + "type": "text CHECK (\"id\" = upper(\"id\") AND length(\"id\") = 26 AND substr(\"id\", 1, 1) GLOB '[0-7]' AND \"id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "last_message_at": { + "name": "last_message_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_run_id": { + "name": "last_run_id", + "type": "text CHECK (\"last_run_id\" = upper(\"last_run_id\") AND length(\"last_run_id\") = 26 AND substr(\"last_run_id\", 1, 1) GLOB '[0-7]' AND \"last_run_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "message_seq_cursor": { + "name": "message_seq_cursor", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "metadata_json": { + "name": "metadata_json", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'{}'" + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "app_id": { + "name": "app_id", + "type": "text CHECK (\"app_id\" = upper(\"app_id\") AND length(\"app_id\") = 26 AND substr(\"app_id\", 1, 1) GLOB '[0-7]' AND \"app_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "renamed": { + "name": "renamed", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "runtime_id": { + "name": "runtime_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status_operation_id": { + "name": "status_operation_id", + "type": "text CHECK (\"status_operation_id\" = upper(\"status_operation_id\") AND length(\"status_operation_id\") = 26 AND substr(\"status_operation_id\", 1, 1) GLOB '[0-7]' AND \"status_operation_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status_seq": { + "name": "status_seq", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "runtime_event_seq_cursor": { + "name": "runtime_event_seq_cursor", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'preview'" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "workspace_checkpoint_required": { + "name": "workspace_checkpoint_required", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + } + }, + "indexes": { + "session_agent_updated_idx": { + "name": "session_agent_updated_idx", + "columns": ["agent_id", "updated_at", "id"], + "isUnique": false + }, + "session_app_creator_archived_updated_idx": { + "name": "session_app_creator_archived_updated_idx", + "columns": ["app_id", "creator_account_id", "archived_at", "updated_at", "id"], + "isUnique": false + }, + "session_app_attributed_archived_updated_idx": { + "name": "session_app_attributed_archived_updated_idx", + "columns": ["app_id", "attributed_user_id", "archived_at", "updated_at", "id"], + "isUnique": false + }, + "session_app_creator_type_archived_updated_idx": { + "name": "session_app_creator_type_archived_updated_idx", + "columns": ["app_id", "creator_account_id", "type", "archived_at", "updated_at", "id"], + "isUnique": false + }, + "session_app_attributed_type_archived_updated_idx": { + "name": "session_app_attributed_type_archived_updated_idx", + "columns": ["app_id", "attributed_user_id", "type", "archived_at", "updated_at", "id"], + "isUnique": false + }, + "session_status_operation_updated_idx": { + "name": "session_status_operation_updated_idx", + "columns": ["status", "status_operation_id", "updated_at"], + "isUnique": false + }, + "session_status_updated_idx": { + "name": "session_status_updated_idx", + "columns": ["status", "updated_at", "id"], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": { + "session_status_check": { + "name": "session_status_check", + "value": "\"session\".\"status\" IN ('IDLE', 'RUNNING', 'RESCHEDULING', 'TERMINATED')" + }, + "session_status_seq_check": { + "name": "session_status_seq_check", + "value": "\"session\".\"status_seq\" >= 0" + } + } + }, + "session_execution_snapshot": { + "name": "session_execution_snapshot", + "columns": { + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "plan_json": { + "name": "plan_json", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "session_id": { + "name": "session_id", + "type": "text CHECK (\"session_id\" = upper(\"session_id\") AND length(\"session_id\") = 26 AND substr(\"session_id\", 1, 1) GLOB '[0-7]' AND \"session_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": true, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "session_execution_snapshot_session_id_session_id_fk": { + "name": "session_execution_snapshot_session_id_session_id_fk", + "tableFrom": "session_execution_snapshot", + "tableTo": "session", + "columnsFrom": ["session_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "session_run_skill": { + "name": "session_run_skill", + "columns": { + "blob_sha256": { + "name": "blob_sha256", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "materialization_status": { + "name": "materialization_status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "mount_path": { + "name": "mount_path", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "resolution_mode": { + "name": "resolution_mode", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "session_run_id": { + "name": "session_run_id", + "type": "text CHECK (\"session_run_id\" = upper(\"session_run_id\") AND length(\"session_run_id\") = 26 AND substr(\"session_run_id\", 1, 1) GLOB '[0-7]' AND \"session_run_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "skill_id": { + "name": "skill_id", + "type": "text CHECK (\"skill_id\" = upper(\"skill_id\") AND length(\"skill_id\") = 26 AND substr(\"skill_id\", 1, 1) GLOB '[0-7]' AND \"skill_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "skill_name": { + "name": "skill_name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "snapshot_id": { + "name": "snapshot_id", + "type": "text CHECK (\"snapshot_id\" = upper(\"snapshot_id\") AND length(\"snapshot_id\") = 26 AND substr(\"snapshot_id\", 1, 1) GLOB '[0-7]' AND \"snapshot_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "warning_code": { + "name": "warning_code", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "session_run_skill_run_resolution_idx": { + "name": "session_run_skill_run_resolution_idx", + "columns": ["session_run_id", "resolution_mode"], + "isUnique": false + } + }, + "foreignKeys": { + "session_run_skill_session_run_id_session_run_id_fk": { + "name": "session_run_skill_session_run_id_session_run_id_fk", + "tableFrom": "session_run_skill", + "tableTo": "session_run", + "columnsFrom": ["session_run_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "session_run_skill_session_run_id_skill_id_pk": { + "columns": ["session_run_id", "skill_id"], + "name": "session_run_skill_session_run_id_skill_id_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "session_run": { + "name": "session_run", + "columns": { + "agent_id": { + "name": "agent_id", + "type": "text CHECK (\"agent_id\" = upper(\"agent_id\") AND length(\"agent_id\") = 26 AND substr(\"agent_id\", 1, 1) GLOB '[0-7]' AND \"agent_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "bound_capability_agent_id": { + "name": "bound_capability_agent_id", + "type": "text CHECK (\"bound_capability_agent_id\" = upper(\"bound_capability_agent_id\") AND length(\"bound_capability_agent_id\") = 26 AND substr(\"bound_capability_agent_id\", 1, 1) GLOB '[0-7]' AND \"bound_capability_agent_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "bound_capability_app_id": { + "name": "bound_capability_app_id", + "type": "text CHECK (\"bound_capability_app_id\" = upper(\"bound_capability_app_id\") AND length(\"bound_capability_app_id\") = 26 AND substr(\"bound_capability_app_id\", 1, 1) GLOB '[0-7]' AND \"bound_capability_app_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "bound_capability_binding_env": { + "name": "bound_capability_binding_env", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "bound_capability_binding_name": { + "name": "bound_capability_binding_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "bound_capability_deployment_id": { + "name": "bound_capability_deployment_id", + "type": "text CHECK (\"bound_capability_deployment_id\" = upper(\"bound_capability_deployment_id\") AND length(\"bound_capability_deployment_id\") = 26 AND substr(\"bound_capability_deployment_id\", 1, 1) GLOB '[0-7]' AND \"bound_capability_deployment_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "bound_capability_deployment_run_id": { + "name": "bound_capability_deployment_run_id", + "type": "text CHECK (\"bound_capability_deployment_run_id\" = upper(\"bound_capability_deployment_run_id\") AND length(\"bound_capability_deployment_run_id\") = 26 AND substr(\"bound_capability_deployment_run_id\", 1, 1) GLOB '[0-7]' AND \"bound_capability_deployment_run_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "completed_at": { + "name": "completed_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_by_account_id": { + "name": "created_by_account_id", + "type": "text CHECK (\"created_by_account_id\" = upper(\"created_by_account_id\") AND length(\"created_by_account_id\") = 26 AND substr(\"created_by_account_id\", 1, 1) GLOB '[0-7]' AND \"created_by_account_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "deployment_version_id": { + "name": "deployment_version_id", + "type": "text CHECK (\"deployment_version_id\" = upper(\"deployment_version_id\") AND length(\"deployment_version_id\") = 26 AND substr(\"deployment_version_id\", 1, 1) GLOB '[0-7]' AND \"deployment_version_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "deployment_version_number": { + "name": "deployment_version_number", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "driver_instance_id": { + "name": "driver_instance_id", + "type": "text CHECK (\"driver_instance_id\" = upper(\"driver_instance_id\") AND length(\"driver_instance_id\") = 26 AND substr(\"driver_instance_id\", 1, 1) GLOB '[0-7]' AND \"driver_instance_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "error_code": { + "name": "error_code", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "error_details_json": { + "name": "error_details_json", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "id": { + "name": "id", + "type": "text CHECK (\"id\" = upper(\"id\") AND length(\"id\") = 26 AND substr(\"id\", 1, 1) GLOB '[0-7]' AND \"id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "runtime_id": { + "name": "runtime_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "session_id": { + "name": "session_id", + "type": "text CHECK (\"session_id\" = upper(\"session_id\") AND length(\"session_id\") = 26 AND substr(\"session_id\", 1, 1) GLOB '[0-7]' AND \"session_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "started_at": { + "name": "started_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status_changed_at": { + "name": "status_changed_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "status_event": { + "name": "status_event", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'run.queue'" + }, + "status_operation_id": { + "name": "status_operation_id", + "type": "text CHECK (\"status_operation_id\" = upper(\"status_operation_id\") AND length(\"status_operation_id\") = 26 AND substr(\"status_operation_id\", 1, 1) GLOB '[0-7]' AND \"status_operation_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status_seq": { + "name": "status_seq", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "status_source": { + "name": "status_source", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'system'" + }, + "trace_id": { + "name": "trace_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "trigger": { + "name": "trigger", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "session_run_driver_instance_idx": { + "name": "session_run_driver_instance_idx", + "columns": ["driver_instance_id", "created_at"], + "isUnique": false + }, + "session_run_active_driver_lease_idx": { + "name": "session_run_active_driver_lease_idx", + "columns": ["driver_instance_id"], + "isUnique": true, + "where": "\"session_run\".\"driver_instance_id\" IS NOT NULL AND \"session_run\".\"status\" IN ('queued', 'booting', 'running', 'waiting_input')" + }, + "session_run_session_created_at_idx": { + "name": "session_run_session_created_at_idx", + "columns": ["session_id", "created_at"], + "isUnique": false + }, + "session_run_session_status_idx": { + "name": "session_run_session_status_idx", + "columns": ["session_id", "status"], + "isUnique": false + } + }, + "foreignKeys": { + "session_run_session_id_session_id_fk": { + "name": "session_run_session_id_session_id_fk", + "tableFrom": "session_run", + "tableTo": "session", + "columnsFrom": ["session_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": { + "session_run_status_check": { + "name": "session_run_status_check", + "value": "\"session_run\".\"status\" IN ('queued', 'booting', 'running', 'waiting_input', 'completed', 'failed', 'cancelled', 'expired')" + }, + "session_run_status_seq_check": { + "name": "session_run_status_seq_check", + "value": "\"session_run\".\"status_seq\" >= 0" + } + } + }, + "session_agent_task_snapshot": { + "name": "session_agent_task_snapshot", + "columns": { + "driver_instance_id": { + "name": "driver_instance_id", + "type": "text CHECK (\"driver_instance_id\" = upper(\"driver_instance_id\") AND length(\"driver_instance_id\") = 26 AND substr(\"driver_instance_id\", 1, 1) GLOB '[0-7]' AND \"driver_instance_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "run_id": { + "name": "run_id", + "type": "text CHECK (\"run_id\" = upper(\"run_id\") AND length(\"run_id\") = 26 AND substr(\"run_id\", 1, 1) GLOB '[0-7]' AND \"run_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "seq": { + "name": "seq", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "session_id": { + "name": "session_id", + "type": "text CHECK (\"session_id\" = upper(\"session_id\") AND length(\"session_id\") = 26 AND substr(\"session_id\", 1, 1) GLOB '[0-7]' AND \"session_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tasks_json": { + "name": "tasks_json", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "session_agent_task_snapshot_run_id_session_run_id_fk": { + "name": "session_agent_task_snapshot_run_id_session_run_id_fk", + "tableFrom": "session_agent_task_snapshot", + "tableTo": "session_run", + "columnsFrom": ["run_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "session_agent_task_snapshot_session_id_session_id_fk": { + "name": "session_agent_task_snapshot_session_id_session_id_fk", + "tableFrom": "session_agent_task_snapshot", + "tableTo": "session", + "columnsFrom": ["session_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "session_event": { + "name": "session_event", + "columns": { + "agent_id": { + "name": "agent_id", + "type": "text CHECK (\"agent_id\" = upper(\"agent_id\") AND length(\"agent_id\") = 26 AND substr(\"agent_id\", 1, 1) GLOB '[0-7]' AND \"agent_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "content_text": { + "name": "content_text", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "ended_at": { + "name": "ended_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "event_type": { + "name": "event_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "family": { + "name": "family", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "id": { + "name": "id", + "type": "text CHECK (\"id\" = upper(\"id\") AND length(\"id\") = 26 AND substr(\"id\", 1, 1) GLOB '[0-7]' AND \"id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "occurred_at": { + "name": "occurred_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "process_status": { + "name": "process_status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "process_type": { + "name": "process_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "run_id": { + "name": "run_id", + "type": "text CHECK (\"run_id\" = upper(\"run_id\") AND length(\"run_id\") = 26 AND substr(\"run_id\", 1, 1) GLOB '[0-7]' AND \"run_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "seq": { + "name": "seq", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "session_id": { + "name": "session_id", + "type": "text CHECK (\"session_id\" = upper(\"session_id\") AND length(\"session_id\") = 26 AND substr(\"session_id\", 1, 1) GLOB '[0-7]' AND \"session_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "source_event_id": { + "name": "source_event_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "tool_call_id": { + "name": "tool_call_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "tool_input_json": { + "name": "tool_input_json", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "tool_name": { + "name": "tool_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "tokens": { + "name": "tokens", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "trace_id": { + "name": "trace_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "visibility": { + "name": "visibility", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "session_event_agent_family_created_idx": { + "name": "session_event_agent_family_created_idx", + "columns": ["agent_id", "family", "created_at", "id"], + "isUnique": false + }, + "session_event_agent_visibility_created_idx": { + "name": "session_event_agent_visibility_created_idx", + "columns": ["agent_id", "visibility", "created_at", "id"], + "isUnique": false + }, + "session_event_agent_created_idx": { + "name": "session_event_agent_created_idx", + "columns": ["agent_id", "created_at", "id"], + "isUnique": false + }, + "session_event_session_visibility_seq_idx": { + "name": "session_event_session_visibility_seq_idx", + "columns": ["session_id", "visibility", "seq"], + "isUnique": false + }, + "session_event_run_event_type_idx": { + "name": "session_event_run_event_type_idx", + "columns": ["run_id", "event_type"], + "isUnique": false + }, + "session_event_session_seq_idx": { + "name": "session_event_session_seq_idx", + "columns": ["session_id", "seq"], + "isUnique": true + }, + "session_event_session_source_idx": { + "name": "session_event_session_source_idx", + "columns": ["session_id", "source_event_id"], + "isUnique": true + } + }, + "foreignKeys": { + "session_event_session_id_session_id_fk": { + "name": "session_event_session_id_session_id_fk", + "tableFrom": "session_event", + "tableTo": "session", + "columnsFrom": ["session_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "session_model_call": { + "name": "session_model_call", + "columns": { + "cache_creation_tokens": { + "name": "cache_creation_tokens", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "cache_read_tokens": { + "name": "cache_read_tokens", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "call_key": { + "name": "call_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "completed_at": { + "name": "completed_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "cost_currency": { + "name": "cost_currency", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "driver_instance_id": { + "name": "driver_instance_id", + "type": "text CHECK (\"driver_instance_id\" = upper(\"driver_instance_id\") AND length(\"driver_instance_id\") = 26 AND substr(\"driver_instance_id\", 1, 1) GLOB '[0-7]' AND \"driver_instance_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "error_code": { + "name": "error_code", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "id": { + "name": "id", + "type": "text CHECK (\"id\" = upper(\"id\") AND length(\"id\") = 26 AND substr(\"id\", 1, 1) GLOB '[0-7]' AND \"id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "input_tokens": { + "name": "input_tokens", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "metadata_json": { + "name": "metadata_json", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "native_call_id": { + "name": "native_call_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "output_tokens": { + "name": "output_tokens", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "session_id": { + "name": "session_id", + "type": "text CHECK (\"session_id\" = upper(\"session_id\") AND length(\"session_id\") = 26 AND substr(\"session_id\", 1, 1) GLOB '[0-7]' AND \"session_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "session_run_id": { + "name": "session_run_id", + "type": "text CHECK (\"session_run_id\" = upper(\"session_run_id\") AND length(\"session_run_id\") = 26 AND substr(\"session_run_id\", 1, 1) GLOB '[0-7]' AND \"session_run_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "started_at": { + "name": "started_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "total_cost_usd_micros": { + "name": "total_cost_usd_micros", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "trace_id": { + "name": "trace_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "session_model_call_run_created_idx": { + "name": "session_model_call_run_created_idx", + "columns": ["session_run_id", "created_at"], + "isUnique": false + }, + "session_model_call_session_created_idx": { + "name": "session_model_call_session_created_idx", + "columns": ["session_id", "created_at"], + "isUnique": false + }, + "session_model_call_run_key_idx": { + "name": "session_model_call_run_key_idx", + "columns": ["session_run_id", "call_key"], + "isUnique": true + }, + "session_model_call_native_idx": { + "name": "session_model_call_native_idx", + "columns": ["driver_instance_id", "native_call_id"], + "isUnique": true + } + }, + "foreignKeys": { + "session_model_call_session_id_session_id_fk": { + "name": "session_model_call_session_id_session_id_fk", + "tableFrom": "session_model_call", + "tableTo": "session", + "columnsFrom": ["session_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "session_model_call_session_run_id_session_run_id_fk": { + "name": "session_model_call_session_run_id_session_run_id_fk", + "tableFrom": "session_model_call", + "tableTo": "session_run", + "columnsFrom": ["session_run_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "session_permission_request": { + "name": "session_permission_request", + "columns": { + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "driver_instance_id": { + "name": "driver_instance_id", + "type": "text CHECK (\"driver_instance_id\" = upper(\"driver_instance_id\") AND length(\"driver_instance_id\") = 26 AND substr(\"driver_instance_id\", 1, 1) GLOB '[0-7]' AND \"driver_instance_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "raw_input": { + "name": "raw_input", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "request_id": { + "name": "request_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "run_id": { + "name": "run_id", + "type": "text CHECK (\"run_id\" = upper(\"run_id\") AND length(\"run_id\") = 26 AND substr(\"run_id\", 1, 1) GLOB '[0-7]' AND \"run_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "session_id": { + "name": "session_id", + "type": "text CHECK (\"session_id\" = upper(\"session_id\") AND length(\"session_id\") = 26 AND substr(\"session_id\", 1, 1) GLOB '[0-7]' AND \"session_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "tool_call_id": { + "name": "tool_call_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "tool_kind": { + "name": "tool_kind", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "session_permission_request_run_idx": { + "name": "session_permission_request_run_idx", + "columns": ["session_id", "run_id"], + "isUnique": false + } + }, + "foreignKeys": { + "session_permission_request_session_id_session_id_fk": { + "name": "session_permission_request_session_id_session_id_fk", + "tableFrom": "session_permission_request", + "tableTo": "session", + "columnsFrom": ["session_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "session_permission_request_session_id_request_id_pk": { + "columns": ["session_id", "request_id"], + "name": "session_permission_request_session_id_request_id_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "session_readiness_snapshot": { + "name": "session_readiness_snapshot", + "columns": { + "readiness_json": { + "name": "readiness_json", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "session_id": { + "name": "session_id", + "type": "text CHECK (\"session_id\" = upper(\"session_id\") AND length(\"session_id\") = 26 AND substr(\"session_id\", 1, 1) GLOB '[0-7]' AND \"session_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "session_readiness_snapshot_session_id_session_id_fk": { + "name": "session_readiness_snapshot_session_id_session_id_fk", + "tableFrom": "session_readiness_snapshot", + "tableTo": "session", + "columnsFrom": ["session_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "skill_snapshot_entry": { + "name": "skill_snapshot_entry", + "columns": { + "entry_kind": { + "name": "entry_kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "is_executable": { + "name": "is_executable", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "mime_type": { + "name": "mime_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "path": { + "name": "path", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "sha256": { + "name": "sha256", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "size": { + "name": "size", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "snapshot_id": { + "name": "snapshot_id", + "type": "text CHECK (\"snapshot_id\" = upper(\"snapshot_id\") AND length(\"snapshot_id\") = 26 AND substr(\"snapshot_id\", 1, 1) GLOB '[0-7]' AND \"snapshot_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "skill_snapshot_entry_snapshot_id_path_pk": { + "columns": ["snapshot_id", "path"], + "name": "skill_snapshot_entry_snapshot_id_path_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "skill_snapshot": { + "name": "skill_snapshot", + "columns": { + "author": { + "name": "author", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "blob_key": { + "name": "blob_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "blob_sha256": { + "name": "blob_sha256", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "blob_size": { + "name": "blob_size", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "id": { + "name": "id", + "type": "text CHECK (\"id\" = upper(\"id\") AND length(\"id\") = 26 AND substr(\"id\", 1, 1) GLOB '[0-7]' AND \"id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "app_id": { + "name": "app_id", + "type": "text CHECK (\"app_id\" = upper(\"app_id\") AND length(\"app_id\") = 26 AND substr(\"app_id\", 1, 1) GLOB '[0-7]' AND \"app_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "skill_markdown_path": { + "name": "skill_markdown_path", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "uncompressed_size": { + "name": "uncompressed_size", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "version": { + "name": "version", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "skill_snapshot_app_created_at_idx": { + "name": "skill_snapshot_app_created_at_idx", + "columns": ["app_id", "created_at"], + "isUnique": false + }, + "skill_snapshot_blob_sha256_idx": { + "name": "skill_snapshot_blob_sha256_idx", + "columns": ["app_id", "blob_sha256"], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "skill": { + "name": "skill", + "columns": { + "author": { + "name": "author", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "current_snapshot_id": { + "name": "current_snapshot_id", + "type": "text CHECK (\"current_snapshot_id\" = upper(\"current_snapshot_id\") AND length(\"current_snapshot_id\") = 26 AND substr(\"current_snapshot_id\", 1, 1) GLOB '[0-7]' AND \"current_snapshot_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "forked_from_owner_name": { + "name": "forked_from_owner_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "forked_from_skill_id": { + "name": "forked_from_skill_id", + "type": "text CHECK (\"forked_from_skill_id\" = upper(\"forked_from_skill_id\") AND length(\"forked_from_skill_id\") = 26 AND substr(\"forked_from_skill_id\", 1, 1) GLOB '[0-7]' AND \"forked_from_skill_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "forked_from_skill_name": { + "name": "forked_from_skill_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "id": { + "name": "id", + "type": "text CHECK (\"id\" = upper(\"id\") AND length(\"id\") = 26 AND substr(\"id\", 1, 1) GLOB '[0-7]' AND \"id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "owner_account_id": { + "name": "owner_account_id", + "type": "text CHECK (\"owner_account_id\" = upper(\"owner_account_id\") AND length(\"owner_account_id\") = 26 AND substr(\"owner_account_id\", 1, 1) GLOB '[0-7]' AND \"owner_account_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "app_id": { + "name": "app_id", + "type": "text CHECK (\"app_id\" = upper(\"app_id\") AND length(\"app_id\") = 26 AND substr(\"app_id\", 1, 1) GLOB '[0-7]' AND \"app_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "source_kind": { + "name": "source_kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "version": { + "name": "version", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "skill_app_updated_at_idx": { + "name": "skill_app_updated_at_idx", + "columns": ["app_id", "updated_at"], + "isUnique": false + }, + "skill_owner_account_updated_at_idx": { + "name": "skill_owner_account_updated_at_idx", + "columns": ["owner_account_id", "updated_at"], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "account": { + "name": "account", + "columns": { + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "email_verified": { + "name": "email_verified", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "id": { + "name": "id", + "type": "text CHECK (\"id\" = upper(\"id\") AND length(\"id\") = 26 AND substr(\"id\", 1, 1) GLOB '[0-7]' AND \"id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "image_url": { + "name": "image_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_active_organization_id": { + "name": "last_active_organization_id", + "type": "text CHECK (\"last_active_organization_id\" = upper(\"last_active_organization_id\") AND length(\"last_active_organization_id\") = 26 AND substr(\"last_active_organization_id\", 1, 1) GLOB '[0-7]' AND \"last_active_organization_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "system_agent_model": { + "name": "system_agent_model", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "account_email_idx": { + "name": "account_email_idx", + "columns": ["email"], + "isUnique": true + }, + "account_last_active_organization_idx": { + "name": "account_last_active_organization_idx", + "columns": ["last_active_organization_id"], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "usage_daily_rollup": { + "name": "usage_daily_rollup", + "columns": { + "actor_user_id": { + "name": "actor_user_id", + "type": "text CHECK (\"actor_user_id\" = upper(\"actor_user_id\") AND length(\"actor_user_id\") = 26 AND substr(\"actor_user_id\", 1, 1) GLOB '[0-7]' AND \"actor_user_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "agent_id": { + "name": "agent_id", + "type": "text CHECK (\"agent_id\" = upper(\"agent_id\") AND length(\"agent_id\") = 26 AND substr(\"agent_id\", 1, 1) GLOB '[0-7]' AND \"agent_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "agent_owner_user_id": { + "name": "agent_owner_user_id", + "type": "text CHECK (\"agent_owner_user_id\" = upper(\"agent_owner_user_id\") AND length(\"agent_owner_user_id\") = 26 AND substr(\"agent_owner_user_id\", 1, 1) GLOB '[0-7]' AND \"agent_owner_user_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "agent_publication_state_at_run": { + "name": "agent_publication_state_at_run", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "cache_creation_tokens": { + "name": "cache_creation_tokens", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "cache_read_tokens": { + "name": "cache_read_tokens", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "date": { + "name": "date", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "input_tokens": { + "name": "input_tokens", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "organization_id": { + "name": "organization_id", + "type": "text CHECK (\"organization_id\" = upper(\"organization_id\") AND length(\"organization_id\") = 26 AND substr(\"organization_id\", 1, 1) GLOB '[0-7]' AND \"organization_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "app_id": { + "name": "app_id", + "type": "text CHECK (\"app_id\" = upper(\"app_id\") AND length(\"app_id\") = 26 AND substr(\"app_id\", 1, 1) GLOB '[0-7]' AND \"app_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "output_tokens": { + "name": "output_tokens", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "request_count": { + "name": "request_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "run_purpose": { + "name": "run_purpose", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "total_cost_usd_micros": { + "name": "total_cost_usd_micros", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "unpriced_request_count": { + "name": "unpriced_request_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "usage_daily_rollup_app_date_idx": { + "name": "usage_daily_rollup_app_date_idx", + "columns": ["app_id", "date"], + "isUnique": false + }, + "usage_daily_rollup_organization_date_idx": { + "name": "usage_daily_rollup_organization_date_idx", + "columns": ["organization_id", "date"], + "isUnique": false + }, + "usage_daily_rollup_agent_date_idx": { + "name": "usage_daily_rollup_agent_date_idx", + "columns": ["agent_id", "date"], + "isUnique": false + }, + "usage_daily_rollup_actor_date_idx": { + "name": "usage_daily_rollup_actor_date_idx", + "columns": ["actor_user_id", "date"], + "isUnique": false + }, + "usage_daily_rollup_owner_date_idx": { + "name": "usage_daily_rollup_owner_date_idx", + "columns": ["agent_owner_user_id", "date"], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "usage_daily_rollup_organization_id_app_id_agent_id_actor_user_id_agent_owner_user_id_date_agent_publication_state_at_run_run_purpose_provider_model_pk": { + "columns": [ + "organization_id", + "app_id", + "agent_id", + "actor_user_id", + "agent_owner_user_id", + "date", + "agent_publication_state_at_run", + "run_purpose", + "provider", + "model" + ], + "name": "usage_daily_rollup_organization_id_app_id_agent_id_actor_user_id_agent_owner_user_id_date_agent_publication_state_at_run_run_purpose_provider_model_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "usage_event_rollup_receipt": { + "name": "usage_event_rollup_receipt", + "columns": { + "rolled_up_at": { + "name": "rolled_up_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "source_event_id": { + "name": "source_event_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "usage_event_rollup_receipt_rolled_up_at_idx": { + "name": "usage_event_rollup_receipt_rolled_up_at_idx", + "columns": ["rolled_up_at"], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "usage_event_rollup_receipt_source_source_event_id_pk": { + "columns": ["source", "source_event_id"], + "name": "usage_event_rollup_receipt_source_source_event_id_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "usage_event": { + "name": "usage_event", + "columns": { + "actor_user_id": { + "name": "actor_user_id", + "type": "text CHECK (\"actor_user_id\" = upper(\"actor_user_id\") AND length(\"actor_user_id\") = 26 AND substr(\"actor_user_id\", 1, 1) GLOB '[0-7]' AND \"actor_user_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "agent_id": { + "name": "agent_id", + "type": "text CHECK (\"agent_id\" = upper(\"agent_id\") AND length(\"agent_id\") = 26 AND substr(\"agent_id\", 1, 1) GLOB '[0-7]' AND \"agent_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "agent_owner_user_id": { + "name": "agent_owner_user_id", + "type": "text CHECK (\"agent_owner_user_id\" = upper(\"agent_owner_user_id\") AND length(\"agent_owner_user_id\") = 26 AND substr(\"agent_owner_user_id\", 1, 1) GLOB '[0-7]' AND \"agent_owner_user_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "agent_publication_state_at_run": { + "name": "agent_publication_state_at_run", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "agent_revision_id": { + "name": "agent_revision_id", + "type": "text CHECK (\"agent_revision_id\" = upper(\"agent_revision_id\") AND length(\"agent_revision_id\") = 26 AND substr(\"agent_revision_id\", 1, 1) GLOB '[0-7]' AND \"agent_revision_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "cache_creation_tokens": { + "name": "cache_creation_tokens", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "cache_read_tokens": { + "name": "cache_read_tokens", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "id": { + "name": "id", + "type": "text CHECK (\"id\" = upper(\"id\") AND length(\"id\") = 26 AND substr(\"id\", 1, 1) GLOB '[0-7]' AND \"id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "input_tokens": { + "name": "input_tokens", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "organization_id": { + "name": "organization_id", + "type": "text CHECK (\"organization_id\" = upper(\"organization_id\") AND length(\"organization_id\") = 26 AND substr(\"organization_id\", 1, 1) GLOB '[0-7]' AND \"organization_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "app_id": { + "name": "app_id", + "type": "text CHECK (\"app_id\" = upper(\"app_id\") AND length(\"app_id\") = 26 AND substr(\"app_id\", 1, 1) GLOB '[0-7]' AND \"app_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "output_tokens": { + "name": "output_tokens", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "price_snapshot_json": { + "name": "price_snapshot_json", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "pricing_status": { + "name": "pricing_status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "run_purpose": { + "name": "run_purpose", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "runtime_id": { + "name": "runtime_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "session_id": { + "name": "session_id", + "type": "text CHECK (\"session_id\" = upper(\"session_id\") AND length(\"session_id\") = 26 AND substr(\"session_id\", 1, 1) GLOB '[0-7]' AND \"session_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "session_run_id": { + "name": "session_run_id", + "type": "text CHECK (\"session_run_id\" = upper(\"session_run_id\") AND length(\"session_run_id\") = 26 AND substr(\"session_run_id\", 1, 1) GLOB '[0-7]' AND \"session_run_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "source_event_id": { + "name": "source_event_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "total_cost_usd_micros": { + "name": "total_cost_usd_micros", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "usage_contract": { + "name": "usage_contract", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "usage_event_app_created_idx": { + "name": "usage_event_app_created_idx", + "columns": ["app_id", "created_at"], + "isUnique": false + }, + "usage_event_organization_created_idx": { + "name": "usage_event_organization_created_idx", + "columns": ["organization_id", "created_at"], + "isUnique": false + }, + "usage_event_agent_created_idx": { + "name": "usage_event_agent_created_idx", + "columns": ["agent_id", "created_at"], + "isUnique": false + }, + "usage_event_actor_created_idx": { + "name": "usage_event_actor_created_idx", + "columns": ["actor_user_id", "created_at"], + "isUnique": false + }, + "usage_event_owner_created_idx": { + "name": "usage_event_owner_created_idx", + "columns": ["agent_owner_user_id", "created_at"], + "isUnique": false + }, + "usage_event_session_run_idx": { + "name": "usage_event_session_run_idx", + "columns": ["session_run_id"], + "isUnique": false + }, + "usage_event_source_event_idx": { + "name": "usage_event_source_event_idx", + "columns": ["source", "source_event_id"], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "vendor_credential": { + "name": "vendor_credential", + "columns": { + "api_base": { + "name": "api_base", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "api_key_secret_id": { + "name": "api_key_secret_id", + "type": "text CHECK (\"api_key_secret_id\" = upper(\"api_key_secret_id\") AND length(\"api_key_secret_id\") = 26 AND substr(\"api_key_secret_id\", 1, 1) GLOB '[0-7]' AND \"api_key_secret_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "id": { + "name": "id", + "type": "text CHECK (\"id\" = upper(\"id\") AND length(\"id\") = 26 AND substr(\"id\", 1, 1) GLOB '[0-7]' AND \"id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "is_default": { + "name": "is_default", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "models": { + "name": "models", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "app_id": { + "name": "app_id", + "type": "text CHECK (\"app_id\" = upper(\"app_id\") AND length(\"app_id\") = 26 AND substr(\"app_id\", 1, 1) GLOB '[0-7]' AND \"app_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "vendor_id": { + "name": "vendor_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "vendor_credential_app_vendor_idx": { + "name": "vendor_credential_app_vendor_idx", + "columns": ["app_id", "vendor_id"], + "isUnique": false + }, + "vendor_credential_app_vendor_name_idx": { + "name": "vendor_credential_app_vendor_name_idx", + "columns": ["app_id", "vendor_id", "name"], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + } + }, + "views": {}, + "enums": {}, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + }, + "internal": { + "indexes": { + "file_record_listing_idx": { + "columns": { + "lower(\"name\")": { + "isExpression": true + } + } + } + } + } +} diff --git a/pkgs/db/drizzle/meta/0014_snapshot.json b/pkgs/db/drizzle/meta/0014_snapshot.json new file mode 100644 index 00000000..bf26fc18 --- /dev/null +++ b/pkgs/db/drizzle/meta/0014_snapshot.json @@ -0,0 +1,6768 @@ +{ + "version": "6", + "dialect": "sqlite", + "id": "52fe8870-3794-4575-8107-6e113af1d104", + "prevId": "6e193e8f-0833-4771-9c04-14f3325e2dc9", + "tables": { + "agent_deployment_version": { + "name": "agent_deployment_version", + "columns": { + "agent_id": { + "name": "agent_id", + "type": "text CHECK (\"agent_id\" = upper(\"agent_id\") AND length(\"agent_id\") = 26 AND substr(\"agent_id\", 1, 1) GLOB '[0-7]' AND \"agent_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "config_json": { + "name": "config_json", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_by_account_id": { + "name": "created_by_account_id", + "type": "text CHECK (\"created_by_account_id\" = upper(\"created_by_account_id\") AND length(\"created_by_account_id\") = 26 AND substr(\"created_by_account_id\", 1, 1) GLOB '[0-7]' AND \"created_by_account_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "environment_id": { + "name": "environment_id", + "type": "text CHECK (\"environment_id\" = upper(\"environment_id\") AND length(\"environment_id\") = 26 AND substr(\"environment_id\", 1, 1) GLOB '[0-7]' AND \"environment_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "id": { + "name": "id", + "type": "text CHECK (\"id\" = upper(\"id\") AND length(\"id\") = 26 AND substr(\"id\", 1, 1) GLOB '[0-7]' AND \"id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "mcp_bindings_json": { + "name": "mcp_bindings_json", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "prompt": { + "name": "prompt", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "runtime_id": { + "name": "runtime_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "skills_json": { + "name": "skills_json", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "summary": { + "name": "summary", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "version_number": { + "name": "version_number", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "agent_deployment_version_agent_number_idx": { + "name": "agent_deployment_version_agent_number_idx", + "columns": ["agent_id", "version_number"], + "isUnique": true + }, + "agent_deployment_version_agent_created_idx": { + "name": "agent_deployment_version_agent_created_idx", + "columns": ["agent_id", "created_at"], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "agent_mcp_binding": { + "name": "agent_mcp_binding", + "columns": { + "agent_credential_id": { + "name": "agent_credential_id", + "type": "text CHECK (\"agent_credential_id\" = upper(\"agent_credential_id\") AND length(\"agent_credential_id\") = 26 AND substr(\"agent_credential_id\", 1, 1) GLOB '[0-7]' AND \"agent_credential_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "agent_id": { + "name": "agent_id", + "type": "text CHECK (\"agent_id\" = upper(\"agent_id\") AND length(\"agent_id\") = 26 AND substr(\"agent_id\", 1, 1) GLOB '[0-7]' AND \"agent_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "credential_mode": { + "name": "credential_mode", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'runtime_resolved'" + }, + "enabled": { + "name": "enabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "id": { + "name": "id", + "type": "text CHECK (\"id\" = upper(\"id\") AND length(\"id\") = 26 AND substr(\"id\", 1, 1) GLOB '[0-7]' AND \"id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "server_id": { + "name": "server_id", + "type": "text CHECK (\"server_id\" = upper(\"server_id\") AND length(\"server_id\") = 26 AND substr(\"server_id\", 1, 1) GLOB '[0-7]' AND \"server_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "agent_mcp_binding_agent_sort_idx": { + "name": "agent_mcp_binding_agent_sort_idx", + "columns": ["agent_id", "sort_order"], + "isUnique": true + }, + "agent_mcp_binding_server_idx": { + "name": "agent_mcp_binding_server_idx", + "columns": ["server_id"], + "isUnique": false + }, + "agent_mcp_binding_profile_server_idx": { + "name": "agent_mcp_binding_profile_server_idx", + "columns": ["agent_id", "server_id"], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": { + "agent_mcp_binding_agent_credential_shape_check": { + "name": "agent_mcp_binding_agent_credential_shape_check", + "value": "\n (\"agent_mcp_binding\".\"credential_mode\" = 'agent_bound' AND \"agent_mcp_binding\".\"agent_credential_id\" IS NOT NULL)\n OR (\"agent_mcp_binding\".\"credential_mode\" = 'runtime_resolved' AND \"agent_mcp_binding\".\"agent_credential_id\" IS NULL)\n " + } + } + }, + "agent_skill": { + "name": "agent_skill", + "columns": { + "agent_id": { + "name": "agent_id", + "type": "text CHECK (\"agent_id\" = upper(\"agent_id\") AND length(\"agent_id\") = 26 AND substr(\"agent_id\", 1, 1) GLOB '[0-7]' AND \"agent_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "skill_id": { + "name": "skill_id", + "type": "text CHECK (\"skill_id\" = upper(\"skill_id\") AND length(\"skill_id\") = 26 AND substr(\"skill_id\", 1, 1) GLOB '[0-7]' AND \"skill_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "agent_skill_agent_sort_idx": { + "name": "agent_skill_agent_sort_idx", + "columns": ["agent_id", "sort_order"], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "agent_skill_agent_id_skill_id_pk": { + "columns": ["agent_id", "skill_id"], + "name": "agent_skill_agent_id_skill_id_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "agent": { + "name": "agent", + "columns": { + "config_json": { + "name": "config_json", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "environment_id": { + "name": "environment_id", + "type": "text CHECK (\"environment_id\" = upper(\"environment_id\") AND length(\"environment_id\") = 26 AND substr(\"environment_id\", 1, 1) GLOB '[0-7]' AND \"environment_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "id": { + "name": "id", + "type": "text CHECK (\"id\" = upper(\"id\") AND length(\"id\") = 26 AND substr(\"id\", 1, 1) GLOB '[0-7]' AND \"id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'pet'" + }, + "live_deployment_version_id": { + "name": "live_deployment_version_id", + "type": "text CHECK (\"live_deployment_version_id\" = upper(\"live_deployment_version_id\") AND length(\"live_deployment_version_id\") = 26 AND substr(\"live_deployment_version_id\", 1, 1) GLOB '[0-7]' AND \"live_deployment_version_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "owner_account_id": { + "name": "owner_account_id", + "type": "text CHECK (\"owner_account_id\" = upper(\"owner_account_id\") AND length(\"owner_account_id\") = 26 AND substr(\"owner_account_id\", 1, 1) GLOB '[0-7]' AND \"owner_account_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "app_id": { + "name": "app_id", + "type": "text CHECK (\"app_id\" = upper(\"app_id\") AND length(\"app_id\") = 26 AND substr(\"app_id\", 1, 1) GLOB '[0-7]' AND \"app_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "prompt": { + "name": "prompt", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "runtime_id": { + "name": "runtime_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'draft'" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "visibility": { + "name": "visibility", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'private'" + } + }, + "indexes": { + "agent_app_owner_account_idx": { + "name": "agent_app_owner_account_idx", + "columns": ["app_id", "owner_account_id"], + "isUnique": false + }, + "agent_app_status_idx": { + "name": "agent_app_status_idx", + "columns": ["app_id", "status"], + "isUnique": false + }, + "agent_environment_idx": { + "name": "agent_environment_idx", + "columns": ["environment_id"], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": { + "agent_published_live_deployment_version_check": { + "name": "agent_published_live_deployment_version_check", + "value": "\"agent\".\"status\" <> 'published' OR \"agent\".\"live_deployment_version_id\" IS NOT NULL" + } + } + }, + "api_command": { + "name": "api_command", + "columns": { + "attempt_count": { + "name": "attempt_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "claim_expires_at": { + "name": "claim_expires_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "claim_owner": { + "name": "claim_owner", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "completed_at": { + "name": "completed_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "dedupe_key": { + "name": "dedupe_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "id": { + "name": "id", + "type": "text CHECK (\"id\" = upper(\"id\") AND length(\"id\") = 26 AND substr(\"id\", 1, 1) GLOB '[0-7]' AND \"id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "last_error_code": { + "name": "last_error_code", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_error_message": { + "name": "last_error_message", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "payload_json": { + "name": "payload_json", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "api_command_dedupe_idx": { + "name": "api_command_dedupe_idx", + "columns": ["dedupe_key"], + "isUnique": true + }, + "api_command_status_updated_idx": { + "name": "api_command_status_updated_idx", + "columns": ["status", "updated_at"], + "isUnique": false + }, + "api_command_claim_idx": { + "name": "api_command_claim_idx", + "columns": ["status", "claim_expires_at"], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "auth_account": { + "name": "auth_account", + "columns": { + "access_token": { + "name": "access_token", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "access_token_expires_at": { + "name": "access_token_expires_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "provider_account_id": { + "name": "provider_account_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "id_token": { + "name": "id_token", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "refresh_token": { + "name": "refresh_token", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "refresh_token_expires_at": { + "name": "refresh_token_expires_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "account_id": { + "name": "account_id", + "type": "text CHECK (\"account_id\" = upper(\"account_id\") AND length(\"account_id\") = 26 AND substr(\"account_id\", 1, 1) GLOB '[0-7]' AND \"account_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "auth_account_provider_account_idx": { + "name": "auth_account_provider_account_idx", + "columns": ["provider_id", "provider_account_id"], + "isUnique": true + }, + "auth_account_account_id_idx": { + "name": "auth_account_account_id_idx", + "columns": ["account_id"], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "auth_session": { + "name": "auth_session", + "columns": { + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "ip_address": { + "name": "ip_address", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "account_id": { + "name": "account_id", + "type": "text CHECK (\"account_id\" = upper(\"account_id\") AND length(\"account_id\") = 26 AND substr(\"account_id\", 1, 1) GLOB '[0-7]' AND \"account_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "auth_session_expires_at_idx": { + "name": "auth_session_expires_at_idx", + "columns": ["expires_at"], + "isUnique": false + }, + "auth_session_token_idx": { + "name": "auth_session_token_idx", + "columns": ["token"], + "isUnique": true + }, + "auth_session_account_id_idx": { + "name": "auth_session_account_id_idx", + "columns": ["account_id"], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "auth_verification": { + "name": "auth_verification", + "columns": { + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "identifier": { + "name": "identifier", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "auth_verification_expires_at_idx": { + "name": "auth_verification_expires_at_idx", + "columns": ["expires_at"], + "isUnique": false + }, + "auth_verification_identifier_idx": { + "name": "auth_verification_identifier_idx", + "columns": ["identifier"], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "cli_oauth_flow": { + "name": "cli_oauth_flow", + "columns": { + "account_id": { + "name": "account_id", + "type": "text CHECK (\"account_id\" = upper(\"account_id\") AND length(\"account_id\") = 26 AND substr(\"account_id\", 1, 1) GLOB '[0-7]' AND \"account_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "authorized_at": { + "name": "authorized_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "completed_at": { + "name": "completed_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "device_code_hash": { + "name": "device_code_hash", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "hostname": { + "name": "hostname", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "id": { + "name": "id", + "type": "text CHECK (\"id\" = upper(\"id\") AND length(\"id\") = 26 AND substr(\"id\", 1, 1) GLOB '[0-7]' AND \"id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_code": { + "name": "user_code", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "cli_oauth_flow_status_expires_idx": { + "name": "cli_oauth_flow_status_expires_idx", + "columns": ["status", "expires_at"], + "isUnique": false + }, + "cli_oauth_flow_device_code_hash_idx": { + "name": "cli_oauth_flow_device_code_hash_idx", + "columns": ["device_code_hash"], + "isUnique": true + }, + "cli_oauth_flow_user_code_idx": { + "name": "cli_oauth_flow_user_code_idx", + "columns": ["user_code"], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "personal_access_token": { + "name": "personal_access_token", + "columns": { + "account_id": { + "name": "account_id", + "type": "text CHECK (\"account_id\" = upper(\"account_id\") AND length(\"account_id\") = 26 AND substr(\"account_id\", 1, 1) GLOB '[0-7]' AND \"account_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "id": { + "name": "id", + "type": "text CHECK (\"id\" = upper(\"id\") AND length(\"id\") = 26 AND substr(\"id\", 1, 1) GLOB '[0-7]' AND \"id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "last_used_at": { + "name": "last_used_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "revoked_at": { + "name": "revoked_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "token_hash": { + "name": "token_hash", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "personal_access_token_account_created_idx": { + "name": "personal_access_token_account_created_idx", + "columns": ["account_id", "created_at"], + "isUnique": false + }, + "personal_access_token_hash_idx": { + "name": "personal_access_token_hash_idx", + "columns": ["token_hash"], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "email_log": { + "name": "email_log", + "columns": { + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "recipient_domain": { + "name": "recipient_domain", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "recipient_masked": { + "name": "recipient_masked", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "subject": { + "name": "subject", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "email_log_created_at_idx": { + "name": "email_log_created_at_idx", + "columns": ["created_at"], + "isUnique": false + }, + "email_log_type_status_idx": { + "name": "email_log_type_status_idx", + "columns": ["type", "status"], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "environment_revision": { + "name": "environment_revision", + "columns": { + "allow_mcp_servers": { + "name": "allow_mcp_servers", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "allow_package_managers": { + "name": "allow_package_managers", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "allowed_hosts_json": { + "name": "allowed_hosts_json", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_by_account_id": { + "name": "created_by_account_id", + "type": "text CHECK (\"created_by_account_id\" = upper(\"created_by_account_id\") AND length(\"created_by_account_id\") = 26 AND substr(\"created_by_account_id\", 1, 1) GLOB '[0-7]' AND \"created_by_account_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "env_vars_json": { + "name": "env_vars_json", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "environment_id": { + "name": "environment_id", + "type": "text CHECK (\"environment_id\" = upper(\"environment_id\") AND length(\"environment_id\") = 26 AND substr(\"environment_id\", 1, 1) GLOB '[0-7]' AND \"environment_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "id": { + "name": "id", + "type": "text CHECK (\"id\" = upper(\"id\") AND length(\"id\") = 26 AND substr(\"id\", 1, 1) GLOB '[0-7]' AND \"id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "network_policy": { + "name": "network_policy", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "packages_json": { + "name": "packages_json", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "app_id": { + "name": "app_id", + "type": "text CHECK (\"app_id\" = upper(\"app_id\") AND length(\"app_id\") = 26 AND substr(\"app_id\", 1, 1) GLOB '[0-7]' AND \"app_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "setup_script": { + "name": "setup_script", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "environment_revision_environment_created_at_idx": { + "name": "environment_revision_environment_created_at_idx", + "columns": ["environment_id", "created_at"], + "isUnique": false + }, + "environment_revision_app_created_at_idx": { + "name": "environment_revision_app_created_at_idx", + "columns": ["app_id", "created_at"], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": { + "environment_revision_network_policy_check": { + "name": "environment_revision_network_policy_check", + "value": "\"environment_revision\".\"network_policy\" IN ('full', 'limited')" + } + } + }, + "environment": { + "name": "environment", + "columns": { + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "current_revision_id": { + "name": "current_revision_id", + "type": "text CHECK (\"current_revision_id\" = upper(\"current_revision_id\") AND length(\"current_revision_id\") = 26 AND substr(\"current_revision_id\", 1, 1) GLOB '[0-7]' AND \"current_revision_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "forked_from_environment_id": { + "name": "forked_from_environment_id", + "type": "text CHECK (\"forked_from_environment_id\" = upper(\"forked_from_environment_id\") AND length(\"forked_from_environment_id\") = 26 AND substr(\"forked_from_environment_id\", 1, 1) GLOB '[0-7]' AND \"forked_from_environment_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "forked_from_environment_name": { + "name": "forked_from_environment_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "forked_from_owner_name": { + "name": "forked_from_owner_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "id": { + "name": "id", + "type": "text CHECK (\"id\" = upper(\"id\") AND length(\"id\") = 26 AND substr(\"id\", 1, 1) GLOB '[0-7]' AND \"id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "owner_account_id": { + "name": "owner_account_id", + "type": "text CHECK (\"owner_account_id\" = upper(\"owner_account_id\") AND length(\"owner_account_id\") = 26 AND substr(\"owner_account_id\", 1, 1) GLOB '[0-7]' AND \"owner_account_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "app_id": { + "name": "app_id", + "type": "text CHECK (\"app_id\" = upper(\"app_id\") AND length(\"app_id\") = 26 AND substr(\"app_id\", 1, 1) GLOB '[0-7]' AND \"app_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "environment_app_updated_at_idx": { + "name": "environment_app_updated_at_idx", + "columns": ["app_id", "updated_at"], + "isUnique": false + }, + "environment_owner_updated_at_idx": { + "name": "environment_owner_updated_at_idx", + "columns": ["owner_account_id", "updated_at"], + "isUnique": false + }, + "environment_owner_name_idx": { + "name": "environment_owner_name_idx", + "columns": ["app_id", "owner_account_id", "name"], + "isUnique": true, + "where": "\"environment\".\"owner_account_id\" IS NOT NULL" + }, + "environment_system_default_idx": { + "name": "environment_system_default_idx", + "columns": ["app_id"], + "isUnique": true, + "where": "\"environment\".\"owner_account_id\" IS NULL" + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "file_record": { + "name": "file_record", + "columns": { + "committed": { + "name": "committed", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_by_account_id": { + "name": "created_by_account_id", + "type": "text CHECK (\"created_by_account_id\" = upper(\"created_by_account_id\") AND length(\"created_by_account_id\") = 26 AND substr(\"created_by_account_id\", 1, 1) GLOB '[0-7]' AND \"created_by_account_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "etag": { + "name": "etag", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "id": { + "name": "id", + "type": "text CHECK (\"id\" = upper(\"id\") AND length(\"id\") = 26 AND substr(\"id\", 1, 1) GLOB '[0-7]' AND \"id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "mime_type": { + "name": "mime_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "object_key": { + "name": "object_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "owner_id": { + "name": "owner_id", + "type": "text CHECK (\"owner_id\" = upper(\"owner_id\") AND length(\"owner_id\") = 26 AND substr(\"owner_id\", 1, 1) GLOB '[0-7]' AND \"owner_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "owner_kind": { + "name": "owner_kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "parent_path": { + "name": "parent_path", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "path": { + "name": "path", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "purpose": { + "name": "purpose", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "scope_id": { + "name": "scope_id", + "type": "text CHECK (\"scope_id\" = upper(\"scope_id\") AND length(\"scope_id\") = 26 AND substr(\"scope_id\", 1, 1) GLOB '[0-7]' AND \"scope_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "scope_kind": { + "name": "scope_kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "session_kind": { + "name": "session_kind", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "size": { + "name": "size", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "version": { + "name": "version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "file_record_object_key_idx": { + "name": "file_record_object_key_idx", + "columns": ["object_key"], + "isUnique": true + }, + "file_record_unscoped_parent_path_name_status_idx": { + "name": "file_record_unscoped_parent_path_name_status_idx", + "columns": ["scope_kind", "parent_path", "name", "status"], + "isUnique": true, + "where": "\"file_record\".\"scope_id\" IS NULL" + }, + "file_record_scoped_parent_path_name_status_idx": { + "name": "file_record_scoped_parent_path_name_status_idx", + "columns": ["scope_kind", "scope_id", "parent_path", "name", "status"], + "isUnique": true + }, + "file_record_unscoped_pending_path_idx": { + "name": "file_record_unscoped_pending_path_idx", + "columns": ["scope_kind", "path"], + "isUnique": true, + "where": "\"file_record\".\"status\" = 'pending' AND \"file_record\".\"scope_id\" IS NULL" + }, + "file_record_scoped_pending_path_idx": { + "name": "file_record_scoped_pending_path_idx", + "columns": ["scope_kind", "scope_id", "path"], + "isUnique": true, + "where": "\"file_record\".\"status\" = 'pending' AND \"file_record\".\"scope_id\" IS NOT NULL" + }, + "file_record_unscoped_ready_path_idx": { + "name": "file_record_unscoped_ready_path_idx", + "columns": ["scope_kind", "path"], + "isUnique": true, + "where": "\"file_record\".\"status\" = 'ready' AND \"file_record\".\"scope_id\" IS NULL" + }, + "file_record_scoped_ready_path_idx": { + "name": "file_record_scoped_ready_path_idx", + "columns": ["scope_kind", "scope_id", "path"], + "isUnique": true, + "where": "\"file_record\".\"status\" = 'ready' AND \"file_record\".\"scope_id\" IS NOT NULL" + }, + "file_record_governance_idx": { + "name": "file_record_governance_idx", + "columns": ["purpose", "owner_kind", "owner_id", "status", "expires_at"], + "isUnique": false + }, + "file_record_listing_idx": { + "name": "file_record_listing_idx", + "columns": ["scope_kind", "scope_id", "parent_path", "status", "lower(\"name\")"], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "file_upload": { + "name": "file_upload", + "columns": { + "content_type": { + "name": "content_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_by_account_id": { + "name": "created_by_account_id", + "type": "text CHECK (\"created_by_account_id\" = upper(\"created_by_account_id\") AND length(\"created_by_account_id\") = 26 AND substr(\"created_by_account_id\", 1, 1) GLOB '[0-7]' AND \"created_by_account_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expected_size": { + "name": "expected_size", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "file_id": { + "name": "file_id", + "type": "text CHECK (\"file_id\" = upper(\"file_id\") AND length(\"file_id\") = 26 AND substr(\"file_id\", 1, 1) GLOB '[0-7]' AND \"file_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "id": { + "name": "id", + "type": "text CHECK (\"id\" = upper(\"id\") AND length(\"id\") = 26 AND substr(\"id\", 1, 1) GLOB '[0-7]' AND \"id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "if_match_etag": { + "name": "if_match_etag", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "multipart_upload_id": { + "name": "multipart_upload_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "overwrite": { + "name": "overwrite", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "part_size": { + "name": "part_size", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "scope_id": { + "name": "scope_id", + "type": "text CHECK (\"scope_id\" = upper(\"scope_id\") AND length(\"scope_id\") = 26 AND substr(\"scope_id\", 1, 1) GLOB '[0-7]' AND \"scope_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "scope_kind": { + "name": "scope_kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "strategy": { + "name": "strategy", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "file_upload_file_id_idx": { + "name": "file_upload_file_id_idx", + "columns": ["file_id"], + "isUnique": true + }, + "file_upload_status_expires_idx": { + "name": "file_upload_status_expires_idx", + "columns": ["status", "expires_at"], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "file_version": { + "name": "file_version", + "columns": { + "committed": { + "name": "committed", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "committed_at": { + "name": "committed_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_by_account_id": { + "name": "created_by_account_id", + "type": "text CHECK (\"created_by_account_id\" = upper(\"created_by_account_id\") AND length(\"created_by_account_id\") = 26 AND substr(\"created_by_account_id\", 1, 1) GLOB '[0-7]' AND \"created_by_account_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "file_id": { + "name": "file_id", + "type": "text CHECK (\"file_id\" = upper(\"file_id\") AND length(\"file_id\") = 26 AND substr(\"file_id\", 1, 1) GLOB '[0-7]' AND \"file_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "id": { + "name": "id", + "type": "text CHECK (\"id\" = upper(\"id\") AND length(\"id\") = 26 AND substr(\"id\", 1, 1) GLOB '[0-7]' AND \"id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "mime_type": { + "name": "mime_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "object_key": { + "name": "object_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "path": { + "name": "path", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "reason": { + "name": "reason", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "scope_id": { + "name": "scope_id", + "type": "text CHECK (\"scope_id\" = upper(\"scope_id\") AND length(\"scope_id\") = 26 AND substr(\"scope_id\", 1, 1) GLOB '[0-7]' AND \"scope_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "scope_kind": { + "name": "scope_kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "size": { + "name": "size", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "source_etag": { + "name": "source_etag", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "source_object_key": { + "name": "source_object_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "version": { + "name": "version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "file_version_object_key_idx": { + "name": "file_version_object_key_idx", + "columns": ["object_key"], + "isUnique": true + }, + "file_version_scope_path_created_idx": { + "name": "file_version_scope_path_created_idx", + "columns": ["scope_kind", "scope_id", "path", "created_at"], + "isUnique": false + }, + "file_version_file_created_idx": { + "name": "file_version_file_created_idx", + "columns": ["file_id", "created_at"], + "isUnique": false + }, + "file_version_pending_idx": { + "name": "file_version_pending_idx", + "columns": ["committed", "created_at"], + "isUnique": false, + "where": "\"file_version\".\"committed\" = 0" + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "mcp_credential": { + "name": "mcp_credential", + "columns": { + "account_id": { + "name": "account_id", + "type": "text CHECK (\"account_id\" = upper(\"account_id\") AND length(\"account_id\") = 26 AND substr(\"account_id\", 1, 1) GLOB '[0-7]' AND \"account_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "agent_id": { + "name": "agent_id", + "type": "text CHECK (\"agent_id\" = upper(\"agent_id\") AND length(\"agent_id\") = 26 AND substr(\"agent_id\", 1, 1) GLOB '[0-7]' AND \"agent_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "auth_type": { + "name": "auth_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "id": { + "name": "id", + "type": "text CHECK (\"id\" = upper(\"id\") AND length(\"id\") = 26 AND substr(\"id\", 1, 1) GLOB '[0-7]' AND \"id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "last_refreshed_at": { + "name": "last_refreshed_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "oauth_client_id": { + "name": "oauth_client_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "oauth_client_secret_secret_id": { + "name": "oauth_client_secret_secret_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "app_id": { + "name": "app_id", + "type": "text CHECK (\"app_id\" = upper(\"app_id\") AND length(\"app_id\") = 26 AND substr(\"app_id\", 1, 1) GLOB '[0-7]' AND \"app_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "refresh_secret_id": { + "name": "refresh_secret_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "scope_values_json": { + "name": "scope_values_json", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "secret_id": { + "name": "secret_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "server_id": { + "name": "server_id", + "type": "text CHECK (\"server_id\" = upper(\"server_id\") AND length(\"server_id\") = 26 AND substr(\"server_id\", 1, 1) GLOB '[0-7]' AND \"server_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "subject_label": { + "name": "subject_label", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "mcp_credential_server_scope_status_idx": { + "name": "mcp_credential_server_scope_status_idx", + "columns": ["server_id", "scope", "status"], + "isUnique": false + }, + "mcp_credential_app_scope_status_idx": { + "name": "mcp_credential_app_scope_status_idx", + "columns": ["app_id", "scope", "status"], + "isUnique": false + }, + "mcp_credential_app_scope_idx": { + "name": "mcp_credential_app_scope_idx", + "columns": ["server_id", "scope"], + "isUnique": true, + "where": "\"mcp_credential\".\"scope\" = 'app'" + }, + "mcp_credential_agent_scope_idx": { + "name": "mcp_credential_agent_scope_idx", + "columns": ["server_id", "agent_id", "scope"], + "isUnique": true, + "where": "\"mcp_credential\".\"scope\" = 'agent' AND \"mcp_credential\".\"agent_id\" IS NOT NULL" + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": { + "mcp_credential_scope_shape_check": { + "name": "mcp_credential_scope_shape_check", + "value": "\n (\"mcp_credential\".\"scope\" = 'app' AND \"mcp_credential\".\"account_id\" IS NULL AND \"mcp_credential\".\"agent_id\" IS NULL)\n OR (\"mcp_credential\".\"scope\" = 'agent' AND \"mcp_credential\".\"account_id\" IS NULL AND \"mcp_credential\".\"agent_id\" IS NOT NULL)\n " + }, + "mcp_credential_scope_values_json_check": { + "name": "mcp_credential_scope_values_json_check", + "value": "\n \"mcp_credential\".\"scope_values_json\" IS NULL\n OR (json_valid(\"mcp_credential\".\"scope_values_json\") AND json_type(\"mcp_credential\".\"scope_values_json\") = 'array')\n " + }, + "mcp_credential_bearer_shape_check": { + "name": "mcp_credential_bearer_shape_check", + "value": "\n \"mcp_credential\".\"auth_type\" != 'bearer'\n OR (\n \"mcp_credential\".\"oauth_client_id\" IS NULL\n AND \"mcp_credential\".\"oauth_client_secret_secret_id\" IS NULL\n AND \"mcp_credential\".\"refresh_secret_id\" IS NULL\n )\n " + } + } + }, + "mcp_oauth_flow": { + "name": "mcp_oauth_flow", + "columns": { + "authorization_endpoint": { + "name": "authorization_endpoint", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "cleanup_after": { + "name": "cleanup_after", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "code_verifier": { + "name": "code_verifier", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "completed_at": { + "name": "completed_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "id": { + "name": "id", + "type": "text CHECK (\"id\" = upper(\"id\") AND length(\"id\") = 26 AND substr(\"id\", 1, 1) GLOB '[0-7]' AND \"id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "initiator_account_id": { + "name": "initiator_account_id", + "type": "text CHECK (\"initiator_account_id\" = upper(\"initiator_account_id\") AND length(\"initiator_account_id\") = 26 AND substr(\"initiator_account_id\", 1, 1) GLOB '[0-7]' AND \"initiator_account_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "oauth_client_id": { + "name": "oauth_client_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "oauth_client_secret_secret_id": { + "name": "oauth_client_secret_secret_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "app_id": { + "name": "app_id", + "type": "text CHECK (\"app_id\" = upper(\"app_id\") AND length(\"app_id\") = 26 AND substr(\"app_id\", 1, 1) GLOB '[0-7]' AND \"app_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "registration_endpoint": { + "name": "registration_endpoint", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "return_url": { + "name": "return_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "scope_values_json": { + "name": "scope_values_json", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "server_id": { + "name": "server_id", + "type": "text CHECK (\"server_id\" = upper(\"server_id\") AND length(\"server_id\") = 26 AND substr(\"server_id\", 1, 1) GLOB '[0-7]' AND \"server_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "subject_label": { + "name": "subject_label", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "token_endpoint": { + "name": "token_endpoint", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "mcp_oauth_flow_status_cleanup_after_idx": { + "name": "mcp_oauth_flow_status_cleanup_after_idx", + "columns": ["status", "cleanup_after"], + "isUnique": false + }, + "mcp_oauth_flow_expires_at_idx": { + "name": "mcp_oauth_flow_expires_at_idx", + "columns": ["expires_at"], + "isUnique": false + }, + "mcp_oauth_flow_server_account_idx": { + "name": "mcp_oauth_flow_server_account_idx", + "columns": ["server_id", "initiator_account_id"], + "isUnique": false + }, + "mcp_oauth_flow_app_server_account_idx": { + "name": "mcp_oauth_flow_app_server_account_idx", + "columns": ["app_id", "server_id", "initiator_account_id"], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": { + "mcp_oauth_flow_scope_values_json_check": { + "name": "mcp_oauth_flow_scope_values_json_check", + "value": "\n \"mcp_oauth_flow\".\"scope_values_json\" IS NULL\n OR (json_valid(\"mcp_oauth_flow\".\"scope_values_json\") AND json_type(\"mcp_oauth_flow\".\"scope_values_json\") = 'array')\n " + } + } + }, + "mcp_server": { + "name": "mcp_server", + "columns": { + "auth_type": { + "name": "auth_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "byo_client_id": { + "name": "byo_client_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "byo_client_secret_secret_id": { + "name": "byo_client_secret_secret_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "credential_scope": { + "name": "credential_scope", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "enabled": { + "name": "enabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "icon_url": { + "name": "icon_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "id": { + "name": "id", + "type": "text CHECK (\"id\" = upper(\"id\") AND length(\"id\") = 26 AND substr(\"id\", 1, 1) GLOB '[0-7]' AND \"id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "oauth_metadata_json": { + "name": "oauth_metadata_json", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "owner_account_id": { + "name": "owner_account_id", + "type": "text CHECK (\"owner_account_id\" = upper(\"owner_account_id\") AND length(\"owner_account_id\") = 26 AND substr(\"owner_account_id\", 1, 1) GLOB '[0-7]' AND \"owner_account_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "app_id": { + "name": "app_id", + "type": "text CHECK (\"app_id\" = upper(\"app_id\") AND length(\"app_id\") = 26 AND substr(\"app_id\", 1, 1) GLOB '[0-7]' AND \"app_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "mcp_server_app_enabled_idx": { + "name": "mcp_server_app_enabled_idx", + "columns": ["app_id", "enabled"], + "isUnique": false + }, + "mcp_server_owner_app_idx": { + "name": "mcp_server_owner_app_idx", + "columns": ["owner_account_id", "app_id"], + "isUnique": false + }, + "mcp_server_app_url_idx": { + "name": "mcp_server_app_url_idx", + "columns": ["app_id", "url"], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": { + "mcp_server_source_scope_check": { + "name": "mcp_server_source_scope_check", + "value": "\"mcp_server\".\"source\" = 'app' AND \"mcp_server\".\"credential_scope\" = 'app'" + } + } + }, + "vault_secret": { + "name": "vault_secret", + "columns": { + "algorithm": { + "name": "algorithm", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'AES-GCM'" + }, + "ciphertext": { + "name": "ciphertext", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "ciphertext_iv": { + "name": "ciphertext_iv", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "id": { + "name": "id", + "type": "text CHECK (\"id\" = upper(\"id\") AND length(\"id\") = 26 AND substr(\"id\", 1, 1) GLOB '[0-7]' AND \"id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "wrapped_dek": { + "name": "wrapped_dek", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "wrapped_dek_iv": { + "name": "wrapped_dek_iv", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "vault_secret_kind_created_at_idx": { + "name": "vault_secret_kind_created_at_idx", + "columns": ["kind", "created_at"], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "organization": { + "name": "organization", + "columns": { + "avatar_url": { + "name": "avatar_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "creator_account_id": { + "name": "creator_account_id", + "type": "text CHECK (\"creator_account_id\" = upper(\"creator_account_id\") AND length(\"creator_account_id\") = 26 AND substr(\"creator_account_id\", 1, 1) GLOB '[0-7]' AND \"creator_account_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "id": { + "name": "id", + "type": "text CHECK (\"id\" = upper(\"id\") AND length(\"id\") = 26 AND substr(\"id\", 1, 1) GLOB '[0-7]' AND \"id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "organization_creator_account_idx": { + "name": "organization_creator_account_idx", + "columns": ["creator_account_id"], + "isUnique": true, + "where": "\"organization\".\"creator_account_id\" IS NOT NULL" + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "app_deployment_run": { + "name": "app_deployment_run", + "columns": { + "app_id": { + "name": "app_id", + "type": "text CHECK (\"app_id\" = upper(\"app_id\") AND length(\"app_id\") = 26 AND substr(\"app_id\", 1, 1) GLOB '[0-7]' AND \"app_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "deployment_id": { + "name": "deployment_id", + "type": "text CHECK (\"deployment_id\" = upper(\"deployment_id\") AND length(\"deployment_id\") = 26 AND substr(\"deployment_id\", 1, 1) GLOB '[0-7]' AND \"deployment_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "error_code": { + "name": "error_code", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "external_deployment_id": { + "name": "external_deployment_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "external_project_id": { + "name": "external_project_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "external_version_id": { + "name": "external_version_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "generated_wrangler_config_json": { + "name": "generated_wrangler_config_json", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "id": { + "name": "id", + "type": "text CHECK (\"id\" = upper(\"id\") AND length(\"id\") = 26 AND substr(\"id\", 1, 1) GLOB '[0-7]' AND \"id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "mosoo_config_json": { + "name": "mosoo_config_json", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "plan_json": { + "name": "plan_json", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "source_branch": { + "name": "source_branch", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "source_commit_sha": { + "name": "source_commit_sha", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "target_kind": { + "name": "target_kind", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "target_project_name": { + "name": "target_project_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "target_script_name": { + "name": "target_script_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "app_deployment_run_app_id_idx": { + "name": "app_deployment_run_app_id_idx", + "columns": ["app_id", "id"], + "isUnique": false + }, + "app_deployment_run_deployment_id_idx": { + "name": "app_deployment_run_deployment_id_idx", + "columns": ["deployment_id", "id"], + "isUnique": false + }, + "app_deployment_run_active_app_idx": { + "name": "app_deployment_run_active_app_idx", + "columns": ["app_id"], + "isUnique": true, + "where": "\"app_deployment_run\".\"status\" IN ('queued', 'preparing', 'building', 'submitting', 'submitted', 'activating')" + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": { + "app_deployment_run_status_check": { + "name": "app_deployment_run_status_check", + "value": "\"app_deployment_run\".\"status\" IN ('queued', 'preparing', 'building', 'submitting', 'submitted', 'activating', 'success', 'failed')" + }, + "app_deployment_run_target_kind_check": { + "name": "app_deployment_run_target_kind_check", + "value": "\"app_deployment_run\".\"target_kind\" IS NULL OR \"app_deployment_run\".\"target_kind\" IN ('cloudflare_pages', 'cloudflare_worker')" + } + } + }, + "app_deployment_secret": { + "name": "app_deployment_secret", + "columns": { + "app_id": { + "name": "app_id", + "type": "text CHECK (\"app_id\" = upper(\"app_id\") AND length(\"app_id\") = 26 AND substr(\"app_id\", 1, 1) GLOB '[0-7]' AND \"app_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "vault_secret_id": { + "name": "vault_secret_id", + "type": "text CHECK (\"vault_secret_id\" = upper(\"vault_secret_id\") AND length(\"vault_secret_id\") = 26 AND substr(\"vault_secret_id\", 1, 1) GLOB '[0-7]' AND \"vault_secret_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "app_deployment_secret_app_name_idx": { + "name": "app_deployment_secret_app_name_idx", + "columns": ["app_id", "name"], + "isUnique": true + }, + "app_deployment_secret_vault_secret_idx": { + "name": "app_deployment_secret_vault_secret_idx", + "columns": ["vault_secret_id"], + "isUnique": true + } + }, + "foreignKeys": { + "app_deployment_secret_vault_secret_id_vault_secret_id_fk": { + "name": "app_deployment_secret_vault_secret_id_vault_secret_id_fk", + "tableFrom": "app_deployment_secret", + "tableTo": "vault_secret", + "columnsFrom": ["vault_secret_id"], + "columnsTo": ["id"], + "onDelete": "restrict", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "app_deployment": { + "name": "app_deployment", + "columns": { + "app_id": { + "name": "app_id", + "type": "text CHECK (\"app_id\" = upper(\"app_id\") AND length(\"app_id\") = 26 AND substr(\"app_id\", 1, 1) GLOB '[0-7]' AND \"app_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "default_branch": { + "name": "default_branch", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "deleted_at": { + "name": "deleted_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "id": { + "name": "id", + "type": "text CHECK (\"id\" = upper(\"id\") AND length(\"id\") = 26 AND substr(\"id\", 1, 1) GLOB '[0-7]' AND \"id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "last_successful_url": { + "name": "last_successful_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "latest_run_id": { + "name": "latest_run_id", + "type": "text CHECK (\"latest_run_id\" = upper(\"latest_run_id\") AND length(\"latest_run_id\") = 26 AND substr(\"latest_run_id\", 1, 1) GLOB '[0-7]' AND \"latest_run_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "mosoo_subdomain": { + "name": "mosoo_subdomain", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "owner_account_id": { + "name": "owner_account_id", + "type": "text CHECK (\"owner_account_id\" = upper(\"owner_account_id\") AND length(\"owner_account_id\") = 26 AND substr(\"owner_account_id\", 1, 1) GLOB '[0-7]' AND \"owner_account_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "repo_name": { + "name": "repo_name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "repo_owner": { + "name": "repo_owner", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "repo_url": { + "name": "repo_url", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "source_kind": { + "name": "source_kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "app_deployment_active_app_idx": { + "name": "app_deployment_active_app_idx", + "columns": ["app_id"], + "isUnique": true, + "where": "\"app_deployment\".\"deleted_at\" IS NULL" + }, + "app_deployment_active_subdomain_idx": { + "name": "app_deployment_active_subdomain_idx", + "columns": ["mosoo_subdomain"], + "isUnique": true, + "where": "\"app_deployment\".\"deleted_at\" IS NULL" + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": { + "app_deployment_source_kind_check": { + "name": "app_deployment_source_kind_check", + "value": "\"app_deployment\".\"source_kind\" IN ('github_public')" + } + } + }, + "app": { + "name": "app", + "columns": { + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "default_environment_id": { + "name": "default_environment_id", + "type": "text CHECK (\"default_environment_id\" = upper(\"default_environment_id\") AND length(\"default_environment_id\") = 26 AND substr(\"default_environment_id\", 1, 1) GLOB '[0-7]' AND \"default_environment_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "id": { + "name": "id", + "type": "text CHECK (\"id\" = upper(\"id\") AND length(\"id\") = 26 AND substr(\"id\", 1, 1) GLOB '[0-7]' AND \"id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "organization_id": { + "name": "organization_id", + "type": "text CHECK (\"organization_id\" = upper(\"organization_id\") AND length(\"organization_id\") = 26 AND substr(\"organization_id\", 1, 1) GLOB '[0-7]' AND \"organization_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "owner_account_id": { + "name": "owner_account_id", + "type": "text CHECK (\"owner_account_id\" = upper(\"owner_account_id\") AND length(\"owner_account_id\") = 26 AND substr(\"owner_account_id\", 1, 1) GLOB '[0-7]' AND \"owner_account_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "bound_agent_call_idempotency_key": { + "name": "bound_agent_call_idempotency_key", + "columns": { + "body_hash": { + "name": "body_hash", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "id": { + "name": "id", + "type": "text CHECK (\"id\" = upper(\"id\") AND length(\"id\") = 26 AND substr(\"id\", 1, 1) GLOB '[0-7]' AND \"id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "idempotency_key": { + "name": "idempotency_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "run_id": { + "name": "run_id", + "type": "text CHECK (\"run_id\" = upper(\"run_id\") AND length(\"run_id\") = 26 AND substr(\"run_id\", 1, 1) GLOB '[0-7]' AND \"run_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "session_id": { + "name": "session_id", + "type": "text CHECK (\"session_id\" = upper(\"session_id\") AND length(\"session_id\") = 26 AND substr(\"session_id\", 1, 1) GLOB '[0-7]' AND \"session_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "subject_hash": { + "name": "subject_hash", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "bound_agent_call_idempotency_subject_key_idx": { + "name": "bound_agent_call_idempotency_subject_key_idx", + "columns": ["subject_hash", "idempotency_key"], + "isUnique": true + }, + "bound_agent_call_idempotency_updated_idx": { + "name": "bound_agent_call_idempotency_updated_idx", + "columns": ["updated_at"], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "public_api_idempotency_key": { + "name": "public_api_idempotency_key", + "columns": { + "body_hash": { + "name": "body_hash", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "id": { + "name": "id", + "type": "text CHECK (\"id\" = upper(\"id\") AND length(\"id\") = 26 AND substr(\"id\", 1, 1) GLOB '[0-7]' AND \"id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "idempotency_key": { + "name": "idempotency_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "method": { + "name": "method", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "response_json": { + "name": "response_json", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "response_status": { + "name": "response_status", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "route": { + "name": "route", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "token_id": { + "name": "token_id", + "type": "text CHECK (\"token_id\" = upper(\"token_id\") AND length(\"token_id\") = 26 AND substr(\"token_id\", 1, 1) GLOB '[0-7]' AND \"token_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "public_api_idempotency_token_key_idx": { + "name": "public_api_idempotency_token_key_idx", + "columns": ["token_id", "idempotency_key"], + "isUnique": true + }, + "public_api_idempotency_updated_idx": { + "name": "public_api_idempotency_updated_idx", + "columns": ["updated_at"], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "public_api_rate_limit_window": { + "name": "public_api_rate_limit_window", + "columns": { + "bucket_key": { + "name": "bucket_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "request_count": { + "name": "request_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "shard": { + "name": "shard", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "window_start": { + "name": "window_start", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "public_api_rate_limit_window_updated_idx": { + "name": "public_api_rate_limit_window_updated_idx", + "columns": ["updated_at"], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "public_api_rate_limit_window_bucket_key_window_start_shard_pk": { + "columns": ["bucket_key", "window_start", "shard"], + "name": "public_api_rate_limit_window_bucket_key_window_start_shard_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "driver_command": { + "name": "driver_command", + "columns": { + "acked_at": { + "name": "acked_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "completed_at": { + "name": "completed_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "delivery_connection_id": { + "name": "delivery_connection_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "driver_generation": { + "name": "driver_generation", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "driver_instance_id": { + "name": "driver_instance_id", + "type": "text CHECK (\"driver_instance_id\" = upper(\"driver_instance_id\") AND length(\"driver_instance_id\") = 26 AND substr(\"driver_instance_id\", 1, 1) GLOB '[0-7]' AND \"driver_instance_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "error_json": { + "name": "error_json", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "id": { + "name": "id", + "type": "text CHECK (\"id\" = upper(\"id\") AND length(\"id\") = 26 AND substr(\"id\", 1, 1) GLOB '[0-7]' AND \"id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "issued_at": { + "name": "issued_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "payload_json": { + "name": "payload_json", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "result_json": { + "name": "result_json", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "seq": { + "name": "seq", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "driver_command_instance_seq_idx": { + "name": "driver_command_instance_seq_idx", + "columns": ["driver_instance_id", "seq"], + "isUnique": true + }, + "driver_command_instance_status_idx": { + "name": "driver_command_instance_status_idx", + "columns": ["driver_instance_id", "status", "expires_at"], + "isUnique": false + } + }, + "foreignKeys": { + "driver_command_driver_instance_id_driver_instance_id_fk": { + "name": "driver_command_driver_instance_id_driver_instance_id_fk", + "tableFrom": "driver_command", + "tableTo": "driver_instance", + "columnsFrom": ["driver_instance_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": { + "driver_command_generation_check": { + "name": "driver_command_generation_check", + "value": "\"driver_command\".\"driver_generation\" IS NULL OR (typeof(\"driver_command\".\"driver_generation\") = 'integer' AND \"driver_command\".\"driver_generation\" BETWEEN 0 AND 9007199254740991)" + }, + "driver_command_nonterminal_generation_check": { + "name": "driver_command_nonterminal_generation_check", + "value": "\"driver_command\".\"status\" IN ('completed', 'failed', 'expired', 'cancelled') OR \"driver_command\".\"driver_generation\" IS NOT NULL" + } + } + }, + "driver_instance_mcp_grant": { + "name": "driver_instance_mcp_grant", + "columns": { + "auth_type": { + "name": "auth_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "authorization_state": { + "name": "authorization_state", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "can_invalidate": { + "name": "can_invalidate", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "can_refresh": { + "name": "can_refresh", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "credential_id": { + "name": "credential_id", + "type": "text CHECK (\"credential_id\" = upper(\"credential_id\") AND length(\"credential_id\") = 26 AND substr(\"credential_id\", 1, 1) GLOB '[0-7]' AND \"credential_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "driver_instance_id": { + "name": "driver_instance_id", + "type": "text CHECK (\"driver_instance_id\" = upper(\"driver_instance_id\") AND length(\"driver_instance_id\") = 26 AND substr(\"driver_instance_id\", 1, 1) GLOB '[0-7]' AND \"driver_instance_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "app_id": { + "name": "app_id", + "type": "text CHECK (\"app_id\" = upper(\"app_id\") AND length(\"app_id\") = 26 AND substr(\"app_id\", 1, 1) GLOB '[0-7]' AND \"app_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "server_id": { + "name": "server_id", + "type": "text CHECK (\"server_id\" = upper(\"server_id\") AND length(\"server_id\") = 26 AND substr(\"server_id\", 1, 1) GLOB '[0-7]' AND \"server_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "driver_instance_mcp_grant_instance_server_idx": { + "name": "driver_instance_mcp_grant_instance_server_idx", + "columns": ["driver_instance_id", "server_id"], + "isUnique": true + }, + "driver_instance_mcp_grant_instance_credential_idx": { + "name": "driver_instance_mcp_grant_instance_credential_idx", + "columns": ["driver_instance_id", "credential_id"], + "isUnique": false + } + }, + "foreignKeys": { + "driver_instance_mcp_grant_driver_instance_id_driver_instance_id_fk": { + "name": "driver_instance_mcp_grant_driver_instance_id_driver_instance_id_fk", + "tableFrom": "driver_instance_mcp_grant", + "tableTo": "driver_instance", + "columnsFrom": ["driver_instance_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "driver_instance": { + "name": "driver_instance", + "columns": { + "boot_token_expires_at": { + "name": "boot_token_expires_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "boot_token_hash": { + "name": "boot_token_hash", + "type": "blob", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "boot_token_used_at": { + "name": "boot_token_used_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "close_code": { + "name": "close_code", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "close_reason": { + "name": "close_reason", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "connection_id": { + "name": "connection_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "command_seq_cursor": { + "name": "command_seq_cursor", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "driver_pid": { + "name": "driver_pid", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "driver_started_at": { + "name": "driver_started_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "driver_version": { + "name": "driver_version", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "heartbeat_count": { + "name": "heartbeat_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "generation": { + "name": "generation", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "id": { + "name": "id", + "type": "text CHECK (\"id\" = upper(\"id\") AND length(\"id\") = 26 AND substr(\"id\", 1, 1) GLOB '[0-7]' AND \"id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "last_heartbeat_at": { + "name": "last_heartbeat_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "process_id": { + "name": "process_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "protocol": { + "name": "protocol", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "protocol_version": { + "name": "protocol_version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "restart_count": { + "name": "restart_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "runtime": { + "name": "runtime", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "sandbox_id": { + "name": "sandbox_id", + "type": "text CHECK (\"sandbox_id\" = upper(\"sandbox_id\") AND length(\"sandbox_id\") = 26 AND substr(\"sandbox_id\", 1, 1) GLOB '[0-7]' AND \"sandbox_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "sandbox_session_id": { + "name": "sandbox_session_id", + "type": "text CHECK (\"sandbox_session_id\" = upper(\"sandbox_session_id\") AND length(\"sandbox_session_id\") = 26 AND substr(\"sandbox_session_id\", 1, 1) GLOB '[0-7]' AND \"sandbox_session_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status_changed_at": { + "name": "status_changed_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "status_event": { + "name": "status_event", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'driver.provision'" + }, + "status_operation_id": { + "name": "status_operation_id", + "type": "text CHECK (\"status_operation_id\" = upper(\"status_operation_id\") AND length(\"status_operation_id\") = 26 AND substr(\"status_operation_id\", 1, 1) GLOB '[0-7]' AND \"status_operation_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status_seq": { + "name": "status_seq", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "status_source": { + "name": "status_source", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'system'" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "driver_instance_completed_idx": { + "name": "driver_instance_completed_idx", + "columns": ["expires_at", "status"], + "isUnique": false + }, + "driver_instance_connection_idx": { + "name": "driver_instance_connection_idx", + "columns": ["connection_id"], + "isUnique": true, + "where": "\"driver_instance\".\"connection_id\" IS NOT NULL" + }, + "driver_instance_boot_token_expiry_idx": { + "name": "driver_instance_boot_token_expiry_idx", + "columns": ["status", "boot_token_expires_at"], + "isUnique": false, + "where": "\"driver_instance\".\"boot_token_used_at\" IS NULL" + }, + "driver_instance_boot_token_hash_idx": { + "name": "driver_instance_boot_token_hash_idx", + "columns": ["boot_token_hash"], + "isUnique": true + }, + "driver_instance_sandbox_session_idx": { + "name": "driver_instance_sandbox_session_idx", + "columns": ["sandbox_id", "sandbox_session_id", "status", "updated_at"], + "isUnique": false + }, + "driver_instance_live_sandbox_session_idx": { + "name": "driver_instance_live_sandbox_session_idx", + "columns": ["sandbox_id", "sandbox_session_id"], + "isUnique": true, + "where": "\"driver_instance\".\"status\" IN ('provisioning', 'connecting', 'ready', 'stopping')" + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": { + "driver_instance_status_check": { + "name": "driver_instance_status_check", + "value": "\"driver_instance\".\"status\" IN ('provisioning', 'connecting', 'ready', 'stopping', 'stopped', 'failed')" + }, + "driver_instance_status_seq_check": { + "name": "driver_instance_status_seq_check", + "value": "\"driver_instance\".\"status_seq\" >= 0" + } + } + }, + "external_tool_effect_attempt": { + "name": "external_tool_effect_attempt", + "columns": { + "attempt": { + "name": "attempt", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "claim_token": { + "name": "claim_token", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "completed_at": { + "name": "completed_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "effect_id": { + "name": "effect_id", + "type": "text CHECK (\"effect_id\" = upper(\"effect_id\") AND length(\"effect_id\") = 26 AND substr(\"effect_id\", 1, 1) GLOB '[0-7]' AND \"effect_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "provider_receipt_json": { + "name": "provider_receipt_json", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "result_json": { + "name": "result_json", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "external_tool_effect_attempt_status_idx": { + "name": "external_tool_effect_attempt_status_idx", + "columns": ["status", "created_at"], + "isUnique": false + } + }, + "foreignKeys": { + "external_tool_effect_attempt_effect_id_external_tool_effect_id_fk": { + "name": "external_tool_effect_attempt_effect_id_external_tool_effect_id_fk", + "tableFrom": "external_tool_effect_attempt", + "tableTo": "external_tool_effect", + "columnsFrom": ["effect_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "external_tool_effect_attempt_effect_id_attempt_pk": { + "columns": ["effect_id", "attempt"], + "name": "external_tool_effect_attempt_effect_id_attempt_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": { + "external_tool_effect_attempt_status_check": { + "name": "external_tool_effect_attempt_status_check", + "value": "\"external_tool_effect_attempt\".\"status\" IN ('claimed', 'succeeded', 'unknown')" + }, + "external_tool_effect_attempt_claim_token_uuid_check": { + "name": "external_tool_effect_attempt_claim_token_uuid_check", + "value": "length(\"external_tool_effect_attempt\".\"claim_token\") = 36 AND length(replace(\"external_tool_effect_attempt\".\"claim_token\", '-', '')) = 32 AND \"external_tool_effect_attempt\".\"claim_token\" = lower(\"external_tool_effect_attempt\".\"claim_token\") AND substr(\"external_tool_effect_attempt\".\"claim_token\", 9, 1) = '-' AND substr(\"external_tool_effect_attempt\".\"claim_token\", 14, 1) = '-' AND substr(\"external_tool_effect_attempt\".\"claim_token\", 15, 1) = '4' AND substr(\"external_tool_effect_attempt\".\"claim_token\", 19, 1) = '-' AND substr(\"external_tool_effect_attempt\".\"claim_token\", 20, 1) GLOB '[89ab]' AND substr(\"external_tool_effect_attempt\".\"claim_token\", 24, 1) = '-' AND replace(\"external_tool_effect_attempt\".\"claim_token\", '-', '') NOT GLOB '*[^0-9a-f]*'" + } + } + }, + "external_tool_effect": { + "name": "external_tool_effect", + "columns": { + "attempt_count": { + "name": "attempt_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "claim_token": { + "name": "claim_token", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "command_id": { + "name": "command_id", + "type": "text CHECK (\"command_id\" = upper(\"command_id\") AND length(\"command_id\") = 26 AND substr(\"command_id\", 1, 1) GLOB '[0-7]' AND \"command_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "driver_instance_id": { + "name": "driver_instance_id", + "type": "text CHECK (\"driver_instance_id\" = upper(\"driver_instance_id\") AND length(\"driver_instance_id\") = 26 AND substr(\"driver_instance_id\", 1, 1) GLOB '[0-7]' AND \"driver_instance_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "id": { + "name": "id", + "type": "text CHECK (\"id\" = upper(\"id\") AND length(\"id\") = 26 AND substr(\"id\", 1, 1) GLOB '[0-7]' AND \"id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "idempotency_key": { + "name": "idempotency_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "provider_receipt_json": { + "name": "provider_receipt_json", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "result_json": { + "name": "result_json", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "server_id": { + "name": "server_id", + "type": "text CHECK (\"server_id\" = upper(\"server_id\") AND length(\"server_id\") = 26 AND substr(\"server_id\", 1, 1) GLOB '[0-7]' AND \"server_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "session_run_id": { + "name": "session_run_id", + "type": "text CHECK (\"session_run_id\" = upper(\"session_run_id\") AND length(\"session_run_id\") = 26 AND substr(\"session_run_id\", 1, 1) GLOB '[0-7]' AND \"session_run_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "tool_name": { + "name": "tool_name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "external_tool_effect_command_idx": { + "name": "external_tool_effect_command_idx", + "columns": ["command_id"], + "isUnique": true + }, + "external_tool_effect_idempotency_key_idx": { + "name": "external_tool_effect_idempotency_key_idx", + "columns": ["idempotency_key"], + "isUnique": true + }, + "external_tool_effect_run_status_idx": { + "name": "external_tool_effect_run_status_idx", + "columns": ["session_run_id", "status", "id"], + "isUnique": false + }, + "external_tool_effect_driver_status_idx": { + "name": "external_tool_effect_driver_status_idx", + "columns": ["driver_instance_id", "status"], + "isUnique": false + } + }, + "foreignKeys": { + "external_tool_effect_command_id_driver_command_id_fk": { + "name": "external_tool_effect_command_id_driver_command_id_fk", + "tableFrom": "external_tool_effect", + "tableTo": "driver_command", + "columnsFrom": ["command_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "external_tool_effect_driver_instance_id_driver_instance_id_fk": { + "name": "external_tool_effect_driver_instance_id_driver_instance_id_fk", + "tableFrom": "external_tool_effect", + "tableTo": "driver_instance", + "columnsFrom": ["driver_instance_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "external_tool_effect_session_run_id_session_run_id_fk": { + "name": "external_tool_effect_session_run_id_session_run_id_fk", + "tableFrom": "external_tool_effect", + "tableTo": "session_run", + "columnsFrom": ["session_run_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": { + "external_tool_effect_status_check": { + "name": "external_tool_effect_status_check", + "value": "\"external_tool_effect\".\"status\" IN ('intent', 'claimed', 'succeeded', 'unknown')" + }, + "external_tool_effect_claim_token_uuid_check": { + "name": "external_tool_effect_claim_token_uuid_check", + "value": "\"external_tool_effect\".\"claim_token\" IS NULL OR (length(\"external_tool_effect\".\"claim_token\") = 36 AND length(replace(\"external_tool_effect\".\"claim_token\", '-', '')) = 32 AND \"external_tool_effect\".\"claim_token\" = lower(\"external_tool_effect\".\"claim_token\") AND substr(\"external_tool_effect\".\"claim_token\", 9, 1) = '-' AND substr(\"external_tool_effect\".\"claim_token\", 14, 1) = '-' AND substr(\"external_tool_effect\".\"claim_token\", 15, 1) = '4' AND substr(\"external_tool_effect\".\"claim_token\", 19, 1) = '-' AND substr(\"external_tool_effect\".\"claim_token\", 20, 1) GLOB '[89ab]' AND substr(\"external_tool_effect\".\"claim_token\", 24, 1) = '-' AND replace(\"external_tool_effect\".\"claim_token\", '-', '') NOT GLOB '*[^0-9a-f]*')" + } + } + }, + "native_resume_ref": { + "name": "native_resume_ref", + "columns": { + "committed_session_run_id": { + "name": "committed_session_run_id", + "type": "text CHECK (\"committed_session_run_id\" = upper(\"committed_session_run_id\") AND length(\"committed_session_run_id\") = 26 AND substr(\"committed_session_run_id\", 1, 1) GLOB '[0-7]' AND \"committed_session_run_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "committed_value": { + "name": "committed_value", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "observed_driver_instance_id": { + "name": "observed_driver_instance_id", + "type": "text CHECK (\"observed_driver_instance_id\" = upper(\"observed_driver_instance_id\") AND length(\"observed_driver_instance_id\") = 26 AND substr(\"observed_driver_instance_id\", 1, 1) GLOB '[0-7]' AND \"observed_driver_instance_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "observed_session_run_id": { + "name": "observed_session_run_id", + "type": "text CHECK (\"observed_session_run_id\" = upper(\"observed_session_run_id\") AND length(\"observed_session_run_id\") = 26 AND substr(\"observed_session_run_id\", 1, 1) GLOB '[0-7]' AND \"observed_session_run_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "runtime_id": { + "name": "runtime_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "session_id": { + "name": "session_id", + "type": "text CHECK (\"session_id\" = upper(\"session_id\") AND length(\"session_id\") = 26 AND substr(\"session_id\", 1, 1) GLOB '[0-7]' AND \"session_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "native_resume_ref_runtime_updated_idx": { + "name": "native_resume_ref_runtime_updated_idx", + "columns": ["runtime_id", "updated_at"], + "isUnique": false + } + }, + "foreignKeys": { + "native_resume_ref_session_id_session_id_fk": { + "name": "native_resume_ref_session_id_session_id_fk", + "tableFrom": "native_resume_ref", + "tableTo": "session", + "columnsFrom": ["session_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "sandbox_backup": { + "name": "sandbox_backup", + "columns": { + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "dir": { + "name": "dir", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "id": { + "name": "id", + "type": "text CHECK (\"id\" = upper(\"id\") AND length(\"id\") = 26 AND substr(\"id\", 1, 1) GLOB '[0-7]' AND \"id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "keep": { + "name": "keep", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "sandbox_id": { + "name": "sandbox_id", + "type": "text CHECK (\"sandbox_id\" = upper(\"sandbox_id\") AND length(\"sandbox_id\") = 26 AND substr(\"sandbox_id\", 1, 1) GLOB '[0-7]' AND \"sandbox_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "session_run_id": { + "name": "session_run_id", + "type": "text CHECK (\"session_run_id\" = upper(\"session_run_id\") AND length(\"session_run_id\") = 26 AND substr(\"session_run_id\", 1, 1) GLOB '[0-7]' AND \"session_run_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "ttl_seconds": { + "name": "ttl_seconds", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "sandbox_backup_sandbox_status_created_idx": { + "name": "sandbox_backup_sandbox_status_created_idx", + "columns": ["sandbox_id", "status", "created_at"], + "isUnique": false + }, + "sandbox_backup_terminal_checkpoint_idx": { + "name": "sandbox_backup_terminal_checkpoint_idx", + "columns": ["sandbox_id", "dir", "session_run_id"], + "isUnique": true, + "where": "\"sandbox_backup\".\"session_run_id\" IS NOT NULL AND \"sandbox_backup\".\"status\" = 'ready'" + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "sandbox_session": { + "name": "sandbox_session", + "columns": { + "cloudflare_session_id": { + "name": "cloudflare_session_id", + "type": "text CHECK (\"cloudflare_session_id\" = upper(\"cloudflare_session_id\") AND length(\"cloudflare_session_id\") = 26 AND substr(\"cloudflare_session_id\", 1, 1) GLOB '[0-7]' AND \"cloudflare_session_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "cwd": { + "name": "cwd", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "origin_json": { + "name": "origin_json", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "sandbox_id": { + "name": "sandbox_id", + "type": "text CHECK (\"sandbox_id\" = upper(\"sandbox_id\") AND length(\"sandbox_id\") = 26 AND substr(\"sandbox_id\", 1, 1) GLOB '[0-7]' AND \"sandbox_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "session_id": { + "name": "session_id", + "type": "text CHECK (\"session_id\" = upper(\"session_id\") AND length(\"session_id\") = 26 AND substr(\"session_id\", 1, 1) GLOB '[0-7]' AND \"session_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "sandbox_session_sandbox_status_idx": { + "name": "sandbox_session_sandbox_status_idx", + "columns": ["sandbox_id", "status", "updated_at"], + "isUnique": false + }, + "sandbox_session_cloudflare_session_idx": { + "name": "sandbox_session_cloudflare_session_idx", + "columns": ["cloudflare_session_id"], + "isUnique": true + } + }, + "foreignKeys": { + "sandbox_session_session_id_session_id_fk": { + "name": "sandbox_session_session_id_session_id_fk", + "tableFrom": "sandbox_session", + "tableTo": "session", + "columnsFrom": ["session_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "sandbox": { + "name": "sandbox", + "columns": { + "agent_id": { + "name": "agent_id", + "type": "text CHECK (\"agent_id\" = upper(\"agent_id\") AND length(\"agent_id\") = 26 AND substr(\"agent_id\", 1, 1) GLOB '[0-7]' AND \"agent_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "app_id": { + "name": "app_id", + "type": "text CHECK (\"app_id\" = upper(\"app_id\") AND length(\"app_id\") = 26 AND substr(\"app_id\", 1, 1) GLOB '[0-7]' AND \"app_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "bind_mount_ready": { + "name": "bind_mount_ready", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "claim_expires_at": { + "name": "claim_expires_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "claim_owner": { + "name": "claim_owner", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "global_mounts_json": { + "name": "global_mounts_json", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'[]'" + }, + "id": { + "name": "id", + "type": "text CHECK (\"id\" = upper(\"id\") AND length(\"id\") = 26 AND substr(\"id\", 1, 1) GLOB '[0-7]' AND \"id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "inactive_deadline_at": { + "name": "inactive_deadline_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "last_backup_id": { + "name": "last_backup_id", + "type": "text CHECK (\"last_backup_id\" = upper(\"last_backup_id\") AND length(\"last_backup_id\") = 26 AND substr(\"last_backup_id\", 1, 1) GLOB '[0-7]' AND \"last_backup_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_error_code": { + "name": "last_error_code", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_restore_backup_id": { + "name": "last_restore_backup_id", + "type": "text CHECK (\"last_restore_backup_id\" = upper(\"last_restore_backup_id\") AND length(\"last_restore_backup_id\") = 26 AND substr(\"last_restore_backup_id\", 1, 1) GLOB '[0-7]' AND \"last_restore_backup_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "owner_account_id": { + "name": "owner_account_id", + "type": "text CHECK (\"owner_account_id\" = upper(\"owner_account_id\") AND length(\"owner_account_id\") = 26 AND substr(\"owner_account_id\", 1, 1) GLOB '[0-7]' AND \"owner_account_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status_changed_at": { + "name": "status_changed_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "status_event": { + "name": "status_event", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'runtime_subject.cold'" + }, + "status_operation_id": { + "name": "status_operation_id", + "type": "text CHECK (\"status_operation_id\" = upper(\"status_operation_id\") AND length(\"status_operation_id\") = 26 AND substr(\"status_operation_id\", 1, 1) GLOB '[0-7]' AND \"status_operation_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status_seq": { + "name": "status_seq", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "status_source": { + "name": "status_source", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'system'" + }, + "subject_id": { + "name": "subject_id", + "type": "text CHECK (\"subject_id\" = upper(\"subject_id\") AND length(\"subject_id\") = 26 AND substr(\"subject_id\", 1, 1) GLOB '[0-7]' AND \"subject_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "subject_kind": { + "name": "subject_kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "sandbox_subject_idx": { + "name": "sandbox_subject_idx", + "columns": ["kind", "subject_kind", "subject_id"], + "isUnique": true + }, + "sandbox_status_deadline_idx": { + "name": "sandbox_status_deadline_idx", + "columns": ["status", "inactive_deadline_at", "updated_at"], + "isUnique": false + }, + "sandbox_claim_idx": { + "name": "sandbox_claim_idx", + "columns": ["claim_expires_at", "claim_owner"], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": { + "sandbox_status_check": { + "name": "sandbox_status_check", + "value": "\"sandbox\".\"status\" IN ('cold', 'restoring', 'active', 'backing_up', 'destroying', 'error')" + }, + "sandbox_status_seq_check": { + "name": "sandbox_status_seq_check", + "value": "\"sandbox\".\"status_seq\" >= 0" + } + } + }, + "session_message": { + "name": "session_message", + "columns": { + "content_text": { + "name": "content_text", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_by_account_id": { + "name": "created_by_account_id", + "type": "text CHECK (\"created_by_account_id\" = upper(\"created_by_account_id\") AND length(\"created_by_account_id\") = 26 AND substr(\"created_by_account_id\", 1, 1) GLOB '[0-7]' AND \"created_by_account_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "id": { + "name": "id", + "type": "text CHECK (\"id\" = upper(\"id\") AND length(\"id\") = 26 AND substr(\"id\", 1, 1) GLOB '[0-7]' AND \"id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "plan_json": { + "name": "plan_json", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "projection_format": { + "name": "projection_format", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'materialized'" + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "segments_json": { + "name": "segments_json", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "seq": { + "name": "seq", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "session_id": { + "name": "session_id", + "type": "text CHECK (\"session_id\" = upper(\"session_id\") AND length(\"session_id\") = 26 AND substr(\"session_id\", 1, 1) GLOB '[0-7]' AND \"session_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "session_run_id": { + "name": "session_run_id", + "type": "text CHECK (\"session_run_id\" = upper(\"session_run_id\") AND length(\"session_run_id\") = 26 AND substr(\"session_run_id\", 1, 1) GLOB '[0-7]' AND \"session_run_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "session_message_session_seq_idx": { + "name": "session_message_session_seq_idx", + "columns": ["session_id", "seq"], + "isUnique": true + }, + "session_message_run_idx": { + "name": "session_message_run_idx", + "columns": ["session_run_id"], + "isUnique": false + } + }, + "foreignKeys": { + "session_message_session_id_session_id_fk": { + "name": "session_message_session_id_session_id_fk", + "tableFrom": "session_message", + "tableTo": "session", + "columnsFrom": ["session_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": { + "session_message_event_stream_v3_check": { + "name": "session_message_event_stream_v3_check", + "value": "\"session_message\".\"projection_format\" <> 'event_stream_v3' OR (\"session_message\".\"role\" = 'assistant' AND \"session_message\".\"session_run_id\" IS NOT NULL AND \"session_message\".\"content_text\" = '' AND \"session_message\".\"plan_json\" IS NULL AND \"session_message\".\"segments_json\" IS NULL)" + }, + "session_message_projection_format_check": { + "name": "session_message_projection_format_check", + "value": "\"session_message\".\"projection_format\" IN ('materialized', 'event_stream_v3')" + } + } + }, + "session": { + "name": "session", + "columns": { + "agent_id": { + "name": "agent_id", + "type": "text CHECK (\"agent_id\" = upper(\"agent_id\") AND length(\"agent_id\") = 26 AND substr(\"agent_id\", 1, 1) GLOB '[0-7]' AND \"agent_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "archived_at": { + "name": "archived_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "end_user_id": { + "name": "end_user_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "attributed_user_id": { + "name": "attributed_user_id", + "type": "text CHECK (\"attributed_user_id\" = upper(\"attributed_user_id\") AND length(\"attributed_user_id\") = 26 AND substr(\"attributed_user_id\", 1, 1) GLOB '[0-7]' AND \"attributed_user_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "creator_account_id": { + "name": "creator_account_id", + "type": "text CHECK (\"creator_account_id\" = upper(\"creator_account_id\") AND length(\"creator_account_id\") = 26 AND substr(\"creator_account_id\", 1, 1) GLOB '[0-7]' AND \"creator_account_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "deployment_version_id": { + "name": "deployment_version_id", + "type": "text CHECK (\"deployment_version_id\" = upper(\"deployment_version_id\") AND length(\"deployment_version_id\") = 26 AND substr(\"deployment_version_id\", 1, 1) GLOB '[0-7]' AND \"deployment_version_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "deployment_version_number": { + "name": "deployment_version_number", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "id": { + "name": "id", + "type": "text CHECK (\"id\" = upper(\"id\") AND length(\"id\") = 26 AND substr(\"id\", 1, 1) GLOB '[0-7]' AND \"id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "last_message_at": { + "name": "last_message_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_run_id": { + "name": "last_run_id", + "type": "text CHECK (\"last_run_id\" = upper(\"last_run_id\") AND length(\"last_run_id\") = 26 AND substr(\"last_run_id\", 1, 1) GLOB '[0-7]' AND \"last_run_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "message_seq_cursor": { + "name": "message_seq_cursor", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "metadata_json": { + "name": "metadata_json", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'{}'" + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "app_id": { + "name": "app_id", + "type": "text CHECK (\"app_id\" = upper(\"app_id\") AND length(\"app_id\") = 26 AND substr(\"app_id\", 1, 1) GLOB '[0-7]' AND \"app_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "renamed": { + "name": "renamed", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "runtime_id": { + "name": "runtime_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status_operation_id": { + "name": "status_operation_id", + "type": "text CHECK (\"status_operation_id\" = upper(\"status_operation_id\") AND length(\"status_operation_id\") = 26 AND substr(\"status_operation_id\", 1, 1) GLOB '[0-7]' AND \"status_operation_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status_seq": { + "name": "status_seq", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "runtime_event_seq_cursor": { + "name": "runtime_event_seq_cursor", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'preview'" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "workspace_checkpoint_required": { + "name": "workspace_checkpoint_required", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + } + }, + "indexes": { + "session_agent_updated_idx": { + "name": "session_agent_updated_idx", + "columns": ["agent_id", "updated_at", "id"], + "isUnique": false + }, + "session_app_creator_archived_updated_idx": { + "name": "session_app_creator_archived_updated_idx", + "columns": ["app_id", "creator_account_id", "archived_at", "updated_at", "id"], + "isUnique": false + }, + "session_app_attributed_archived_updated_idx": { + "name": "session_app_attributed_archived_updated_idx", + "columns": ["app_id", "attributed_user_id", "archived_at", "updated_at", "id"], + "isUnique": false + }, + "session_app_creator_type_archived_updated_idx": { + "name": "session_app_creator_type_archived_updated_idx", + "columns": ["app_id", "creator_account_id", "type", "archived_at", "updated_at", "id"], + "isUnique": false + }, + "session_app_attributed_type_archived_updated_idx": { + "name": "session_app_attributed_type_archived_updated_idx", + "columns": ["app_id", "attributed_user_id", "type", "archived_at", "updated_at", "id"], + "isUnique": false + }, + "session_status_operation_updated_idx": { + "name": "session_status_operation_updated_idx", + "columns": ["status", "status_operation_id", "updated_at"], + "isUnique": false + }, + "session_status_updated_idx": { + "name": "session_status_updated_idx", + "columns": ["status", "updated_at", "id"], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": { + "session_status_check": { + "name": "session_status_check", + "value": "\"session\".\"status\" IN ('IDLE', 'RUNNING', 'RESCHEDULING', 'TERMINATED')" + }, + "session_status_seq_check": { + "name": "session_status_seq_check", + "value": "\"session\".\"status_seq\" >= 0" + } + } + }, + "session_execution_snapshot": { + "name": "session_execution_snapshot", + "columns": { + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "plan_json": { + "name": "plan_json", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "session_id": { + "name": "session_id", + "type": "text CHECK (\"session_id\" = upper(\"session_id\") AND length(\"session_id\") = 26 AND substr(\"session_id\", 1, 1) GLOB '[0-7]' AND \"session_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": true, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "session_execution_snapshot_session_id_session_id_fk": { + "name": "session_execution_snapshot_session_id_session_id_fk", + "tableFrom": "session_execution_snapshot", + "tableTo": "session", + "columnsFrom": ["session_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "session_run_skill": { + "name": "session_run_skill", + "columns": { + "blob_sha256": { + "name": "blob_sha256", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "materialization_status": { + "name": "materialization_status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "mount_path": { + "name": "mount_path", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "resolution_mode": { + "name": "resolution_mode", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "session_run_id": { + "name": "session_run_id", + "type": "text CHECK (\"session_run_id\" = upper(\"session_run_id\") AND length(\"session_run_id\") = 26 AND substr(\"session_run_id\", 1, 1) GLOB '[0-7]' AND \"session_run_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "skill_id": { + "name": "skill_id", + "type": "text CHECK (\"skill_id\" = upper(\"skill_id\") AND length(\"skill_id\") = 26 AND substr(\"skill_id\", 1, 1) GLOB '[0-7]' AND \"skill_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "skill_name": { + "name": "skill_name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "snapshot_id": { + "name": "snapshot_id", + "type": "text CHECK (\"snapshot_id\" = upper(\"snapshot_id\") AND length(\"snapshot_id\") = 26 AND substr(\"snapshot_id\", 1, 1) GLOB '[0-7]' AND \"snapshot_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "warning_code": { + "name": "warning_code", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "session_run_skill_run_resolution_idx": { + "name": "session_run_skill_run_resolution_idx", + "columns": ["session_run_id", "resolution_mode"], + "isUnique": false + } + }, + "foreignKeys": { + "session_run_skill_session_run_id_session_run_id_fk": { + "name": "session_run_skill_session_run_id_session_run_id_fk", + "tableFrom": "session_run_skill", + "tableTo": "session_run", + "columnsFrom": ["session_run_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "session_run_skill_session_run_id_skill_id_pk": { + "columns": ["session_run_id", "skill_id"], + "name": "session_run_skill_session_run_id_skill_id_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "session_run": { + "name": "session_run", + "columns": { + "agent_id": { + "name": "agent_id", + "type": "text CHECK (\"agent_id\" = upper(\"agent_id\") AND length(\"agent_id\") = 26 AND substr(\"agent_id\", 1, 1) GLOB '[0-7]' AND \"agent_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "bound_capability_agent_id": { + "name": "bound_capability_agent_id", + "type": "text CHECK (\"bound_capability_agent_id\" = upper(\"bound_capability_agent_id\") AND length(\"bound_capability_agent_id\") = 26 AND substr(\"bound_capability_agent_id\", 1, 1) GLOB '[0-7]' AND \"bound_capability_agent_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "bound_capability_app_id": { + "name": "bound_capability_app_id", + "type": "text CHECK (\"bound_capability_app_id\" = upper(\"bound_capability_app_id\") AND length(\"bound_capability_app_id\") = 26 AND substr(\"bound_capability_app_id\", 1, 1) GLOB '[0-7]' AND \"bound_capability_app_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "bound_capability_binding_env": { + "name": "bound_capability_binding_env", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "bound_capability_binding_name": { + "name": "bound_capability_binding_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "bound_capability_deployment_id": { + "name": "bound_capability_deployment_id", + "type": "text CHECK (\"bound_capability_deployment_id\" = upper(\"bound_capability_deployment_id\") AND length(\"bound_capability_deployment_id\") = 26 AND substr(\"bound_capability_deployment_id\", 1, 1) GLOB '[0-7]' AND \"bound_capability_deployment_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "bound_capability_deployment_run_id": { + "name": "bound_capability_deployment_run_id", + "type": "text CHECK (\"bound_capability_deployment_run_id\" = upper(\"bound_capability_deployment_run_id\") AND length(\"bound_capability_deployment_run_id\") = 26 AND substr(\"bound_capability_deployment_run_id\", 1, 1) GLOB '[0-7]' AND \"bound_capability_deployment_run_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "completed_at": { + "name": "completed_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_by_account_id": { + "name": "created_by_account_id", + "type": "text CHECK (\"created_by_account_id\" = upper(\"created_by_account_id\") AND length(\"created_by_account_id\") = 26 AND substr(\"created_by_account_id\", 1, 1) GLOB '[0-7]' AND \"created_by_account_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "deployment_version_id": { + "name": "deployment_version_id", + "type": "text CHECK (\"deployment_version_id\" = upper(\"deployment_version_id\") AND length(\"deployment_version_id\") = 26 AND substr(\"deployment_version_id\", 1, 1) GLOB '[0-7]' AND \"deployment_version_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "deployment_version_number": { + "name": "deployment_version_number", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "driver_instance_id": { + "name": "driver_instance_id", + "type": "text CHECK (\"driver_instance_id\" = upper(\"driver_instance_id\") AND length(\"driver_instance_id\") = 26 AND substr(\"driver_instance_id\", 1, 1) GLOB '[0-7]' AND \"driver_instance_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "error_code": { + "name": "error_code", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "error_details_json": { + "name": "error_details_json", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "error_retryable": { + "name": "error_retryable", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "id": { + "name": "id", + "type": "text CHECK (\"id\" = upper(\"id\") AND length(\"id\") = 26 AND substr(\"id\", 1, 1) GLOB '[0-7]' AND \"id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "runtime_id": { + "name": "runtime_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "session_id": { + "name": "session_id", + "type": "text CHECK (\"session_id\" = upper(\"session_id\") AND length(\"session_id\") = 26 AND substr(\"session_id\", 1, 1) GLOB '[0-7]' AND \"session_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "started_at": { + "name": "started_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status_changed_at": { + "name": "status_changed_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "status_event": { + "name": "status_event", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'run.queue'" + }, + "status_operation_id": { + "name": "status_operation_id", + "type": "text CHECK (\"status_operation_id\" = upper(\"status_operation_id\") AND length(\"status_operation_id\") = 26 AND substr(\"status_operation_id\", 1, 1) GLOB '[0-7]' AND \"status_operation_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status_seq": { + "name": "status_seq", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "status_source": { + "name": "status_source", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'system'" + }, + "trace_id": { + "name": "trace_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "trigger": { + "name": "trigger", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "session_run_driver_instance_idx": { + "name": "session_run_driver_instance_idx", + "columns": ["driver_instance_id", "created_at"], + "isUnique": false + }, + "session_run_active_driver_lease_idx": { + "name": "session_run_active_driver_lease_idx", + "columns": ["driver_instance_id"], + "isUnique": true, + "where": "\"session_run\".\"driver_instance_id\" IS NOT NULL AND \"session_run\".\"status\" IN ('queued', 'booting', 'running', 'waiting_input')" + }, + "session_run_session_created_at_idx": { + "name": "session_run_session_created_at_idx", + "columns": ["session_id", "created_at"], + "isUnique": false + }, + "session_run_session_status_idx": { + "name": "session_run_session_status_idx", + "columns": ["session_id", "status"], + "isUnique": false + } + }, + "foreignKeys": { + "session_run_session_id_session_id_fk": { + "name": "session_run_session_id_session_id_fk", + "tableFrom": "session_run", + "tableTo": "session", + "columnsFrom": ["session_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": { + "session_run_error_retryable_check": { + "name": "session_run_error_retryable_check", + "value": "\"session_run\".\"error_retryable\" IS NULL OR (\"session_run\".\"error_retryable\" IN (false, true) AND \"session_run\".\"error_code\" IS NOT NULL AND \"session_run\".\"error_details_json\" IS NOT NULL AND \"session_run\".\"error_message\" IS NOT NULL)" + }, + "session_run_status_check": { + "name": "session_run_status_check", + "value": "\"session_run\".\"status\" IN ('queued', 'booting', 'running', 'waiting_input', 'completed', 'failed', 'cancelled', 'expired')" + }, + "session_run_status_seq_check": { + "name": "session_run_status_seq_check", + "value": "\"session_run\".\"status_seq\" >= 0" + } + } + }, + "session_agent_task_snapshot": { + "name": "session_agent_task_snapshot", + "columns": { + "driver_instance_id": { + "name": "driver_instance_id", + "type": "text CHECK (\"driver_instance_id\" = upper(\"driver_instance_id\") AND length(\"driver_instance_id\") = 26 AND substr(\"driver_instance_id\", 1, 1) GLOB '[0-7]' AND \"driver_instance_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "run_id": { + "name": "run_id", + "type": "text CHECK (\"run_id\" = upper(\"run_id\") AND length(\"run_id\") = 26 AND substr(\"run_id\", 1, 1) GLOB '[0-7]' AND \"run_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "seq": { + "name": "seq", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "session_id": { + "name": "session_id", + "type": "text CHECK (\"session_id\" = upper(\"session_id\") AND length(\"session_id\") = 26 AND substr(\"session_id\", 1, 1) GLOB '[0-7]' AND \"session_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tasks_json": { + "name": "tasks_json", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "session_agent_task_snapshot_run_id_session_run_id_fk": { + "name": "session_agent_task_snapshot_run_id_session_run_id_fk", + "tableFrom": "session_agent_task_snapshot", + "tableTo": "session_run", + "columnsFrom": ["run_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "session_agent_task_snapshot_session_id_session_id_fk": { + "name": "session_agent_task_snapshot_session_id_session_id_fk", + "tableFrom": "session_agent_task_snapshot", + "tableTo": "session", + "columnsFrom": ["session_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "session_event": { + "name": "session_event", + "columns": { + "agent_id": { + "name": "agent_id", + "type": "text CHECK (\"agent_id\" = upper(\"agent_id\") AND length(\"agent_id\") = 26 AND substr(\"agent_id\", 1, 1) GLOB '[0-7]' AND \"agent_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "content_text": { + "name": "content_text", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "ended_at": { + "name": "ended_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "event_type": { + "name": "event_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "family": { + "name": "family", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "id": { + "name": "id", + "type": "text CHECK (\"id\" = upper(\"id\") AND length(\"id\") = 26 AND substr(\"id\", 1, 1) GLOB '[0-7]' AND \"id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "mcp_command_id": { + "name": "mcp_command_id", + "type": "text CHECK (\"mcp_command_id\" = upper(\"mcp_command_id\") AND length(\"mcp_command_id\") = 26 AND substr(\"mcp_command_id\", 1, 1) GLOB '[0-7]' AND \"mcp_command_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "occurred_at": { + "name": "occurred_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "process_status": { + "name": "process_status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "process_type": { + "name": "process_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "run_id": { + "name": "run_id", + "type": "text CHECK (\"run_id\" = upper(\"run_id\") AND length(\"run_id\") = 26 AND substr(\"run_id\", 1, 1) GLOB '[0-7]' AND \"run_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "semantic_hash": { + "name": "semantic_hash", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "seq": { + "name": "seq", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "session_id": { + "name": "session_id", + "type": "text CHECK (\"session_id\" = upper(\"session_id\") AND length(\"session_id\") = 26 AND substr(\"session_id\", 1, 1) GLOB '[0-7]' AND \"session_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "source_event_id": { + "name": "source_event_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "stream_id": { + "name": "stream_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "tool_call_id": { + "name": "tool_call_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "tool_input_delta_json": { + "name": "tool_input_delta_json", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "tool_input_json": { + "name": "tool_input_json", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "tool_name": { + "name": "tool_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "tool_output_delta_text": { + "name": "tool_output_delta_text", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "tool_output_text": { + "name": "tool_output_text", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "tool_parent_message_id": { + "name": "tool_parent_message_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "tool_result_message_id": { + "name": "tool_result_message_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "tool_status": { + "name": "tool_status", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "tokens": { + "name": "tokens", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "trace_id": { + "name": "trace_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "visibility": { + "name": "visibility", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "session_event_agent_family_created_idx": { + "name": "session_event_agent_family_created_idx", + "columns": ["agent_id", "family", "created_at", "id"], + "isUnique": false + }, + "session_event_agent_visibility_created_idx": { + "name": "session_event_agent_visibility_created_idx", + "columns": ["agent_id", "visibility", "created_at", "id"], + "isUnique": false + }, + "session_event_agent_created_idx": { + "name": "session_event_agent_created_idx", + "columns": ["agent_id", "created_at", "id"], + "isUnique": false + }, + "session_event_session_visibility_seq_idx": { + "name": "session_event_session_visibility_seq_idx", + "columns": ["session_id", "visibility", "seq"], + "isUnique": false + }, + "session_event_run_event_type_idx": { + "name": "session_event_run_event_type_idx", + "columns": ["run_id", "event_type"], + "isUnique": false + }, + "session_event_run_stream_process_seq_idx": { + "name": "session_event_run_stream_process_seq_idx", + "columns": ["run_id", "stream_id", "process_type", "seq"], + "isUnique": false + }, + "session_event_run_tool_call_seq_idx": { + "name": "session_event_run_tool_call_seq_idx", + "columns": ["run_id", "tool_call_id", "seq"], + "isUnique": false + }, + "session_event_session_seq_idx": { + "name": "session_event_session_seq_idx", + "columns": ["session_id", "seq"], + "isUnique": true + }, + "session_event_session_source_idx": { + "name": "session_event_session_source_idx", + "columns": ["session_id", "source_event_id"], + "isUnique": true + }, + "session_event_run_terminal_winner_idx": { + "name": "session_event_run_terminal_winner_idx", + "columns": ["session_id", "run_id"], + "isUnique": true, + "where": "\"session_event\".\"semantic_hash\" IS NOT NULL AND \"session_event\".\"run_id\" IS NOT NULL AND \"session_event\".\"event_type\" IN ('run.cancelled', 'run.completed', 'run.failed')" + }, + "session_event_mcp_terminal_winner_idx": { + "name": "session_event_mcp_terminal_winner_idx", + "columns": ["session_id", "mcp_command_id"], + "isUnique": true, + "where": "\"session_event\".\"mcp_command_id\" IS NOT NULL" + } + }, + "foreignKeys": { + "session_event_session_id_session_id_fk": { + "name": "session_event_session_id_session_id_fk", + "tableFrom": "session_event", + "tableTo": "session", + "columnsFrom": ["session_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": { + "session_event_mcp_command_check": { + "name": "session_event_mcp_command_check", + "value": "\"session_event\".\"mcp_command_id\" IS NULL OR (\"session_event\".\"event_type\" = 'tool.call.updated' AND \"session_event\".\"tool_status\" IS NOT NULL AND \"session_event\".\"tool_status\" IN ('completed', 'failed', 'cancelled'))" + }, + "session_event_semantic_hash_check": { + "name": "session_event_semantic_hash_check", + "value": "\"session_event\".\"semantic_hash\" IS NULL OR (length(\"session_event\".\"semantic_hash\") = 64 AND \"session_event\".\"semantic_hash\" = lower(\"session_event\".\"semantic_hash\") AND \"session_event\".\"semantic_hash\" NOT GLOB '*[^0-9a-f]*')" + }, + "session_event_tool_status_check": { + "name": "session_event_tool_status_check", + "value": "\"session_event\".\"tool_status\" IS NULL OR \"session_event\".\"tool_status\" IN ('running', 'completed', 'failed', 'cancelled')" + }, + "session_event_tool_input_kind_check": { + "name": "session_event_tool_input_kind_check", + "value": "\"session_event\".\"tool_input_delta_json\" IS NULL OR \"session_event\".\"tool_input_json\" IS NULL" + }, + "session_event_tool_output_kind_check": { + "name": "session_event_tool_output_kind_check", + "value": "\"session_event\".\"tool_output_delta_text\" IS NULL OR \"session_event\".\"tool_output_text\" IS NULL" + } + } + }, + "session_model_call": { + "name": "session_model_call", + "columns": { + "cache_creation_tokens": { + "name": "cache_creation_tokens", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "cache_read_tokens": { + "name": "cache_read_tokens", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "call_key": { + "name": "call_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "completed_at": { + "name": "completed_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "cost_currency": { + "name": "cost_currency", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "driver_instance_id": { + "name": "driver_instance_id", + "type": "text CHECK (\"driver_instance_id\" = upper(\"driver_instance_id\") AND length(\"driver_instance_id\") = 26 AND substr(\"driver_instance_id\", 1, 1) GLOB '[0-7]' AND \"driver_instance_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "error_code": { + "name": "error_code", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "id": { + "name": "id", + "type": "text CHECK (\"id\" = upper(\"id\") AND length(\"id\") = 26 AND substr(\"id\", 1, 1) GLOB '[0-7]' AND \"id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "input_tokens": { + "name": "input_tokens", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "metadata_json": { + "name": "metadata_json", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "native_call_id": { + "name": "native_call_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "output_tokens": { + "name": "output_tokens", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "session_id": { + "name": "session_id", + "type": "text CHECK (\"session_id\" = upper(\"session_id\") AND length(\"session_id\") = 26 AND substr(\"session_id\", 1, 1) GLOB '[0-7]' AND \"session_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "session_run_id": { + "name": "session_run_id", + "type": "text CHECK (\"session_run_id\" = upper(\"session_run_id\") AND length(\"session_run_id\") = 26 AND substr(\"session_run_id\", 1, 1) GLOB '[0-7]' AND \"session_run_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "started_at": { + "name": "started_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "total_cost_usd_micros": { + "name": "total_cost_usd_micros", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "trace_id": { + "name": "trace_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "session_model_call_run_created_idx": { + "name": "session_model_call_run_created_idx", + "columns": ["session_run_id", "created_at"], + "isUnique": false + }, + "session_model_call_session_created_idx": { + "name": "session_model_call_session_created_idx", + "columns": ["session_id", "created_at"], + "isUnique": false + }, + "session_model_call_run_key_idx": { + "name": "session_model_call_run_key_idx", + "columns": ["session_run_id", "call_key"], + "isUnique": true + }, + "session_model_call_native_idx": { + "name": "session_model_call_native_idx", + "columns": ["driver_instance_id", "native_call_id"], + "isUnique": true + } + }, + "foreignKeys": { + "session_model_call_session_id_session_id_fk": { + "name": "session_model_call_session_id_session_id_fk", + "tableFrom": "session_model_call", + "tableTo": "session", + "columnsFrom": ["session_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "session_model_call_session_run_id_session_run_id_fk": { + "name": "session_model_call_session_run_id_session_run_id_fk", + "tableFrom": "session_model_call", + "tableTo": "session_run", + "columnsFrom": ["session_run_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "session_permission_request": { + "name": "session_permission_request", + "columns": { + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "driver_instance_id": { + "name": "driver_instance_id", + "type": "text CHECK (\"driver_instance_id\" = upper(\"driver_instance_id\") AND length(\"driver_instance_id\") = 26 AND substr(\"driver_instance_id\", 1, 1) GLOB '[0-7]' AND \"driver_instance_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "raw_input": { + "name": "raw_input", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "request_id": { + "name": "request_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "run_id": { + "name": "run_id", + "type": "text CHECK (\"run_id\" = upper(\"run_id\") AND length(\"run_id\") = 26 AND substr(\"run_id\", 1, 1) GLOB '[0-7]' AND \"run_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "session_id": { + "name": "session_id", + "type": "text CHECK (\"session_id\" = upper(\"session_id\") AND length(\"session_id\") = 26 AND substr(\"session_id\", 1, 1) GLOB '[0-7]' AND \"session_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "tool_call_id": { + "name": "tool_call_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "tool_kind": { + "name": "tool_kind", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "session_permission_request_run_idx": { + "name": "session_permission_request_run_idx", + "columns": ["session_id", "run_id"], + "isUnique": false + } + }, + "foreignKeys": { + "session_permission_request_session_id_session_id_fk": { + "name": "session_permission_request_session_id_session_id_fk", + "tableFrom": "session_permission_request", + "tableTo": "session", + "columnsFrom": ["session_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "session_permission_request_session_id_request_id_pk": { + "columns": ["session_id", "request_id"], + "name": "session_permission_request_session_id_request_id_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "session_readiness_snapshot": { + "name": "session_readiness_snapshot", + "columns": { + "readiness_json": { + "name": "readiness_json", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "session_id": { + "name": "session_id", + "type": "text CHECK (\"session_id\" = upper(\"session_id\") AND length(\"session_id\") = 26 AND substr(\"session_id\", 1, 1) GLOB '[0-7]' AND \"session_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "session_readiness_snapshot_session_id_session_id_fk": { + "name": "session_readiness_snapshot_session_id_session_id_fk", + "tableFrom": "session_readiness_snapshot", + "tableTo": "session", + "columnsFrom": ["session_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "skill_snapshot_entry": { + "name": "skill_snapshot_entry", + "columns": { + "entry_kind": { + "name": "entry_kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "is_executable": { + "name": "is_executable", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "mime_type": { + "name": "mime_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "path": { + "name": "path", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "sha256": { + "name": "sha256", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "size": { + "name": "size", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "snapshot_id": { + "name": "snapshot_id", + "type": "text CHECK (\"snapshot_id\" = upper(\"snapshot_id\") AND length(\"snapshot_id\") = 26 AND substr(\"snapshot_id\", 1, 1) GLOB '[0-7]' AND \"snapshot_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "skill_snapshot_entry_snapshot_id_path_pk": { + "columns": ["snapshot_id", "path"], + "name": "skill_snapshot_entry_snapshot_id_path_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "skill_snapshot": { + "name": "skill_snapshot", + "columns": { + "author": { + "name": "author", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "blob_key": { + "name": "blob_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "blob_sha256": { + "name": "blob_sha256", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "blob_size": { + "name": "blob_size", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "id": { + "name": "id", + "type": "text CHECK (\"id\" = upper(\"id\") AND length(\"id\") = 26 AND substr(\"id\", 1, 1) GLOB '[0-7]' AND \"id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "app_id": { + "name": "app_id", + "type": "text CHECK (\"app_id\" = upper(\"app_id\") AND length(\"app_id\") = 26 AND substr(\"app_id\", 1, 1) GLOB '[0-7]' AND \"app_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "skill_markdown_path": { + "name": "skill_markdown_path", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "uncompressed_size": { + "name": "uncompressed_size", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "version": { + "name": "version", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "skill_snapshot_app_created_at_idx": { + "name": "skill_snapshot_app_created_at_idx", + "columns": ["app_id", "created_at"], + "isUnique": false + }, + "skill_snapshot_blob_sha256_idx": { + "name": "skill_snapshot_blob_sha256_idx", + "columns": ["app_id", "blob_sha256"], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "skill": { + "name": "skill", + "columns": { + "author": { + "name": "author", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "current_snapshot_id": { + "name": "current_snapshot_id", + "type": "text CHECK (\"current_snapshot_id\" = upper(\"current_snapshot_id\") AND length(\"current_snapshot_id\") = 26 AND substr(\"current_snapshot_id\", 1, 1) GLOB '[0-7]' AND \"current_snapshot_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "forked_from_owner_name": { + "name": "forked_from_owner_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "forked_from_skill_id": { + "name": "forked_from_skill_id", + "type": "text CHECK (\"forked_from_skill_id\" = upper(\"forked_from_skill_id\") AND length(\"forked_from_skill_id\") = 26 AND substr(\"forked_from_skill_id\", 1, 1) GLOB '[0-7]' AND \"forked_from_skill_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "forked_from_skill_name": { + "name": "forked_from_skill_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "id": { + "name": "id", + "type": "text CHECK (\"id\" = upper(\"id\") AND length(\"id\") = 26 AND substr(\"id\", 1, 1) GLOB '[0-7]' AND \"id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "owner_account_id": { + "name": "owner_account_id", + "type": "text CHECK (\"owner_account_id\" = upper(\"owner_account_id\") AND length(\"owner_account_id\") = 26 AND substr(\"owner_account_id\", 1, 1) GLOB '[0-7]' AND \"owner_account_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "app_id": { + "name": "app_id", + "type": "text CHECK (\"app_id\" = upper(\"app_id\") AND length(\"app_id\") = 26 AND substr(\"app_id\", 1, 1) GLOB '[0-7]' AND \"app_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "source_kind": { + "name": "source_kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "version": { + "name": "version", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "skill_app_updated_at_idx": { + "name": "skill_app_updated_at_idx", + "columns": ["app_id", "updated_at"], + "isUnique": false + }, + "skill_owner_account_updated_at_idx": { + "name": "skill_owner_account_updated_at_idx", + "columns": ["owner_account_id", "updated_at"], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "account": { + "name": "account", + "columns": { + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "email_verified": { + "name": "email_verified", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "id": { + "name": "id", + "type": "text CHECK (\"id\" = upper(\"id\") AND length(\"id\") = 26 AND substr(\"id\", 1, 1) GLOB '[0-7]' AND \"id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "image_url": { + "name": "image_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_active_organization_id": { + "name": "last_active_organization_id", + "type": "text CHECK (\"last_active_organization_id\" = upper(\"last_active_organization_id\") AND length(\"last_active_organization_id\") = 26 AND substr(\"last_active_organization_id\", 1, 1) GLOB '[0-7]' AND \"last_active_organization_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "system_agent_model": { + "name": "system_agent_model", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "account_email_idx": { + "name": "account_email_idx", + "columns": ["email"], + "isUnique": true + }, + "account_last_active_organization_idx": { + "name": "account_last_active_organization_idx", + "columns": ["last_active_organization_id"], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "usage_daily_rollup": { + "name": "usage_daily_rollup", + "columns": { + "actor_user_id": { + "name": "actor_user_id", + "type": "text CHECK (\"actor_user_id\" = upper(\"actor_user_id\") AND length(\"actor_user_id\") = 26 AND substr(\"actor_user_id\", 1, 1) GLOB '[0-7]' AND \"actor_user_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "agent_id": { + "name": "agent_id", + "type": "text CHECK (\"agent_id\" = upper(\"agent_id\") AND length(\"agent_id\") = 26 AND substr(\"agent_id\", 1, 1) GLOB '[0-7]' AND \"agent_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "agent_owner_user_id": { + "name": "agent_owner_user_id", + "type": "text CHECK (\"agent_owner_user_id\" = upper(\"agent_owner_user_id\") AND length(\"agent_owner_user_id\") = 26 AND substr(\"agent_owner_user_id\", 1, 1) GLOB '[0-7]' AND \"agent_owner_user_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "agent_publication_state_at_run": { + "name": "agent_publication_state_at_run", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "cache_creation_tokens": { + "name": "cache_creation_tokens", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "cache_read_tokens": { + "name": "cache_read_tokens", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "date": { + "name": "date", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "input_tokens": { + "name": "input_tokens", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "organization_id": { + "name": "organization_id", + "type": "text CHECK (\"organization_id\" = upper(\"organization_id\") AND length(\"organization_id\") = 26 AND substr(\"organization_id\", 1, 1) GLOB '[0-7]' AND \"organization_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "app_id": { + "name": "app_id", + "type": "text CHECK (\"app_id\" = upper(\"app_id\") AND length(\"app_id\") = 26 AND substr(\"app_id\", 1, 1) GLOB '[0-7]' AND \"app_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "output_tokens": { + "name": "output_tokens", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "request_count": { + "name": "request_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "run_purpose": { + "name": "run_purpose", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "total_cost_usd_micros": { + "name": "total_cost_usd_micros", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "unpriced_request_count": { + "name": "unpriced_request_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "usage_daily_rollup_app_date_idx": { + "name": "usage_daily_rollup_app_date_idx", + "columns": ["app_id", "date"], + "isUnique": false + }, + "usage_daily_rollup_organization_date_idx": { + "name": "usage_daily_rollup_organization_date_idx", + "columns": ["organization_id", "date"], + "isUnique": false + }, + "usage_daily_rollup_agent_date_idx": { + "name": "usage_daily_rollup_agent_date_idx", + "columns": ["agent_id", "date"], + "isUnique": false + }, + "usage_daily_rollup_actor_date_idx": { + "name": "usage_daily_rollup_actor_date_idx", + "columns": ["actor_user_id", "date"], + "isUnique": false + }, + "usage_daily_rollup_owner_date_idx": { + "name": "usage_daily_rollup_owner_date_idx", + "columns": ["agent_owner_user_id", "date"], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "usage_daily_rollup_organization_id_app_id_agent_id_actor_user_id_agent_owner_user_id_date_agent_publication_state_at_run_run_purpose_provider_model_pk": { + "columns": [ + "organization_id", + "app_id", + "agent_id", + "actor_user_id", + "agent_owner_user_id", + "date", + "agent_publication_state_at_run", + "run_purpose", + "provider", + "model" + ], + "name": "usage_daily_rollup_organization_id_app_id_agent_id_actor_user_id_agent_owner_user_id_date_agent_publication_state_at_run_run_purpose_provider_model_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "usage_event_rollup_receipt": { + "name": "usage_event_rollup_receipt", + "columns": { + "rolled_up_at": { + "name": "rolled_up_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "source_event_id": { + "name": "source_event_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "usage_event_rollup_receipt_rolled_up_at_idx": { + "name": "usage_event_rollup_receipt_rolled_up_at_idx", + "columns": ["rolled_up_at"], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "usage_event_rollup_receipt_source_source_event_id_pk": { + "columns": ["source", "source_event_id"], + "name": "usage_event_rollup_receipt_source_source_event_id_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "usage_event": { + "name": "usage_event", + "columns": { + "actor_user_id": { + "name": "actor_user_id", + "type": "text CHECK (\"actor_user_id\" = upper(\"actor_user_id\") AND length(\"actor_user_id\") = 26 AND substr(\"actor_user_id\", 1, 1) GLOB '[0-7]' AND \"actor_user_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "agent_id": { + "name": "agent_id", + "type": "text CHECK (\"agent_id\" = upper(\"agent_id\") AND length(\"agent_id\") = 26 AND substr(\"agent_id\", 1, 1) GLOB '[0-7]' AND \"agent_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "agent_owner_user_id": { + "name": "agent_owner_user_id", + "type": "text CHECK (\"agent_owner_user_id\" = upper(\"agent_owner_user_id\") AND length(\"agent_owner_user_id\") = 26 AND substr(\"agent_owner_user_id\", 1, 1) GLOB '[0-7]' AND \"agent_owner_user_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "agent_publication_state_at_run": { + "name": "agent_publication_state_at_run", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "agent_revision_id": { + "name": "agent_revision_id", + "type": "text CHECK (\"agent_revision_id\" = upper(\"agent_revision_id\") AND length(\"agent_revision_id\") = 26 AND substr(\"agent_revision_id\", 1, 1) GLOB '[0-7]' AND \"agent_revision_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "cache_creation_tokens": { + "name": "cache_creation_tokens", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "cache_read_tokens": { + "name": "cache_read_tokens", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "id": { + "name": "id", + "type": "text CHECK (\"id\" = upper(\"id\") AND length(\"id\") = 26 AND substr(\"id\", 1, 1) GLOB '[0-7]' AND \"id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "input_tokens": { + "name": "input_tokens", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "organization_id": { + "name": "organization_id", + "type": "text CHECK (\"organization_id\" = upper(\"organization_id\") AND length(\"organization_id\") = 26 AND substr(\"organization_id\", 1, 1) GLOB '[0-7]' AND \"organization_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "app_id": { + "name": "app_id", + "type": "text CHECK (\"app_id\" = upper(\"app_id\") AND length(\"app_id\") = 26 AND substr(\"app_id\", 1, 1) GLOB '[0-7]' AND \"app_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "output_tokens": { + "name": "output_tokens", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "price_snapshot_json": { + "name": "price_snapshot_json", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "pricing_status": { + "name": "pricing_status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "run_purpose": { + "name": "run_purpose", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "runtime_id": { + "name": "runtime_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "session_id": { + "name": "session_id", + "type": "text CHECK (\"session_id\" = upper(\"session_id\") AND length(\"session_id\") = 26 AND substr(\"session_id\", 1, 1) GLOB '[0-7]' AND \"session_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "session_run_id": { + "name": "session_run_id", + "type": "text CHECK (\"session_run_id\" = upper(\"session_run_id\") AND length(\"session_run_id\") = 26 AND substr(\"session_run_id\", 1, 1) GLOB '[0-7]' AND \"session_run_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "source_event_id": { + "name": "source_event_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "total_cost_usd_micros": { + "name": "total_cost_usd_micros", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "usage_contract": { + "name": "usage_contract", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "usage_event_app_created_idx": { + "name": "usage_event_app_created_idx", + "columns": ["app_id", "created_at"], + "isUnique": false + }, + "usage_event_organization_created_idx": { + "name": "usage_event_organization_created_idx", + "columns": ["organization_id", "created_at"], + "isUnique": false + }, + "usage_event_agent_created_idx": { + "name": "usage_event_agent_created_idx", + "columns": ["agent_id", "created_at"], + "isUnique": false + }, + "usage_event_actor_created_idx": { + "name": "usage_event_actor_created_idx", + "columns": ["actor_user_id", "created_at"], + "isUnique": false + }, + "usage_event_owner_created_idx": { + "name": "usage_event_owner_created_idx", + "columns": ["agent_owner_user_id", "created_at"], + "isUnique": false + }, + "usage_event_session_run_idx": { + "name": "usage_event_session_run_idx", + "columns": ["session_run_id"], + "isUnique": false + }, + "usage_event_source_event_idx": { + "name": "usage_event_source_event_idx", + "columns": ["source", "source_event_id"], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "vendor_credential": { + "name": "vendor_credential", + "columns": { + "api_base": { + "name": "api_base", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "api_key_secret_id": { + "name": "api_key_secret_id", + "type": "text CHECK (\"api_key_secret_id\" = upper(\"api_key_secret_id\") AND length(\"api_key_secret_id\") = 26 AND substr(\"api_key_secret_id\", 1, 1) GLOB '[0-7]' AND \"api_key_secret_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "id": { + "name": "id", + "type": "text CHECK (\"id\" = upper(\"id\") AND length(\"id\") = 26 AND substr(\"id\", 1, 1) GLOB '[0-7]' AND \"id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "is_default": { + "name": "is_default", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "models": { + "name": "models", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "app_id": { + "name": "app_id", + "type": "text CHECK (\"app_id\" = upper(\"app_id\") AND length(\"app_id\") = 26 AND substr(\"app_id\", 1, 1) GLOB '[0-7]' AND \"app_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "vendor_id": { + "name": "vendor_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "vendor_credential_app_vendor_idx": { + "name": "vendor_credential_app_vendor_idx", + "columns": ["app_id", "vendor_id"], + "isUnique": false + }, + "vendor_credential_app_vendor_name_idx": { + "name": "vendor_credential_app_vendor_name_idx", + "columns": ["app_id", "vendor_id", "name"], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + } + }, + "views": {}, + "enums": {}, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + }, + "internal": { + "indexes": { + "file_record_listing_idx": { + "columns": { + "lower(\"name\")": { + "isExpression": true + } + } + } + } + } +} diff --git a/pkgs/db/drizzle/meta/0015_snapshot.json b/pkgs/db/drizzle/meta/0015_snapshot.json new file mode 100644 index 00000000..c94e8366 --- /dev/null +++ b/pkgs/db/drizzle/meta/0015_snapshot.json @@ -0,0 +1,6821 @@ +{ + "version": "6", + "dialect": "sqlite", + "id": "66e7b9f7-1123-4ef2-ae35-9b7a24476711", + "prevId": "52fe8870-3794-4575-8107-6e113af1d104", + "tables": { + "agent_deployment_version": { + "name": "agent_deployment_version", + "columns": { + "agent_id": { + "name": "agent_id", + "type": "text CHECK (\"agent_id\" = upper(\"agent_id\") AND length(\"agent_id\") = 26 AND substr(\"agent_id\", 1, 1) GLOB '[0-7]' AND \"agent_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "config_json": { + "name": "config_json", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_by_account_id": { + "name": "created_by_account_id", + "type": "text CHECK (\"created_by_account_id\" = upper(\"created_by_account_id\") AND length(\"created_by_account_id\") = 26 AND substr(\"created_by_account_id\", 1, 1) GLOB '[0-7]' AND \"created_by_account_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "environment_id": { + "name": "environment_id", + "type": "text CHECK (\"environment_id\" = upper(\"environment_id\") AND length(\"environment_id\") = 26 AND substr(\"environment_id\", 1, 1) GLOB '[0-7]' AND \"environment_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "id": { + "name": "id", + "type": "text CHECK (\"id\" = upper(\"id\") AND length(\"id\") = 26 AND substr(\"id\", 1, 1) GLOB '[0-7]' AND \"id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "mcp_bindings_json": { + "name": "mcp_bindings_json", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "prompt": { + "name": "prompt", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "runtime_id": { + "name": "runtime_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "skills_json": { + "name": "skills_json", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "summary": { + "name": "summary", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "version_number": { + "name": "version_number", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "agent_deployment_version_agent_number_idx": { + "name": "agent_deployment_version_agent_number_idx", + "columns": ["agent_id", "version_number"], + "isUnique": true + }, + "agent_deployment_version_agent_created_idx": { + "name": "agent_deployment_version_agent_created_idx", + "columns": ["agent_id", "created_at"], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "agent_mcp_binding": { + "name": "agent_mcp_binding", + "columns": { + "agent_credential_id": { + "name": "agent_credential_id", + "type": "text CHECK (\"agent_credential_id\" = upper(\"agent_credential_id\") AND length(\"agent_credential_id\") = 26 AND substr(\"agent_credential_id\", 1, 1) GLOB '[0-7]' AND \"agent_credential_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "agent_id": { + "name": "agent_id", + "type": "text CHECK (\"agent_id\" = upper(\"agent_id\") AND length(\"agent_id\") = 26 AND substr(\"agent_id\", 1, 1) GLOB '[0-7]' AND \"agent_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "credential_mode": { + "name": "credential_mode", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'runtime_resolved'" + }, + "enabled": { + "name": "enabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "id": { + "name": "id", + "type": "text CHECK (\"id\" = upper(\"id\") AND length(\"id\") = 26 AND substr(\"id\", 1, 1) GLOB '[0-7]' AND \"id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "server_id": { + "name": "server_id", + "type": "text CHECK (\"server_id\" = upper(\"server_id\") AND length(\"server_id\") = 26 AND substr(\"server_id\", 1, 1) GLOB '[0-7]' AND \"server_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "agent_mcp_binding_agent_sort_idx": { + "name": "agent_mcp_binding_agent_sort_idx", + "columns": ["agent_id", "sort_order"], + "isUnique": true + }, + "agent_mcp_binding_server_idx": { + "name": "agent_mcp_binding_server_idx", + "columns": ["server_id"], + "isUnique": false + }, + "agent_mcp_binding_profile_server_idx": { + "name": "agent_mcp_binding_profile_server_idx", + "columns": ["agent_id", "server_id"], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": { + "agent_mcp_binding_agent_credential_shape_check": { + "name": "agent_mcp_binding_agent_credential_shape_check", + "value": "\n (\"agent_mcp_binding\".\"credential_mode\" = 'agent_bound' AND \"agent_mcp_binding\".\"agent_credential_id\" IS NOT NULL)\n OR (\"agent_mcp_binding\".\"credential_mode\" = 'runtime_resolved' AND \"agent_mcp_binding\".\"agent_credential_id\" IS NULL)\n " + } + } + }, + "agent_skill": { + "name": "agent_skill", + "columns": { + "agent_id": { + "name": "agent_id", + "type": "text CHECK (\"agent_id\" = upper(\"agent_id\") AND length(\"agent_id\") = 26 AND substr(\"agent_id\", 1, 1) GLOB '[0-7]' AND \"agent_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "skill_id": { + "name": "skill_id", + "type": "text CHECK (\"skill_id\" = upper(\"skill_id\") AND length(\"skill_id\") = 26 AND substr(\"skill_id\", 1, 1) GLOB '[0-7]' AND \"skill_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "agent_skill_agent_sort_idx": { + "name": "agent_skill_agent_sort_idx", + "columns": ["agent_id", "sort_order"], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "agent_skill_agent_id_skill_id_pk": { + "columns": ["agent_id", "skill_id"], + "name": "agent_skill_agent_id_skill_id_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "agent": { + "name": "agent", + "columns": { + "config_json": { + "name": "config_json", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "environment_id": { + "name": "environment_id", + "type": "text CHECK (\"environment_id\" = upper(\"environment_id\") AND length(\"environment_id\") = 26 AND substr(\"environment_id\", 1, 1) GLOB '[0-7]' AND \"environment_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "id": { + "name": "id", + "type": "text CHECK (\"id\" = upper(\"id\") AND length(\"id\") = 26 AND substr(\"id\", 1, 1) GLOB '[0-7]' AND \"id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'pet'" + }, + "live_deployment_version_id": { + "name": "live_deployment_version_id", + "type": "text CHECK (\"live_deployment_version_id\" = upper(\"live_deployment_version_id\") AND length(\"live_deployment_version_id\") = 26 AND substr(\"live_deployment_version_id\", 1, 1) GLOB '[0-7]' AND \"live_deployment_version_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "owner_account_id": { + "name": "owner_account_id", + "type": "text CHECK (\"owner_account_id\" = upper(\"owner_account_id\") AND length(\"owner_account_id\") = 26 AND substr(\"owner_account_id\", 1, 1) GLOB '[0-7]' AND \"owner_account_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "app_id": { + "name": "app_id", + "type": "text CHECK (\"app_id\" = upper(\"app_id\") AND length(\"app_id\") = 26 AND substr(\"app_id\", 1, 1) GLOB '[0-7]' AND \"app_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "prompt": { + "name": "prompt", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "runtime_id": { + "name": "runtime_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'draft'" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "visibility": { + "name": "visibility", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'private'" + } + }, + "indexes": { + "agent_app_owner_account_idx": { + "name": "agent_app_owner_account_idx", + "columns": ["app_id", "owner_account_id"], + "isUnique": false + }, + "agent_app_status_idx": { + "name": "agent_app_status_idx", + "columns": ["app_id", "status"], + "isUnique": false + }, + "agent_environment_idx": { + "name": "agent_environment_idx", + "columns": ["environment_id"], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": { + "agent_published_live_deployment_version_check": { + "name": "agent_published_live_deployment_version_check", + "value": "\"agent\".\"status\" <> 'published' OR \"agent\".\"live_deployment_version_id\" IS NOT NULL" + } + } + }, + "api_command": { + "name": "api_command", + "columns": { + "attempt_count": { + "name": "attempt_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "claim_expires_at": { + "name": "claim_expires_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "claim_owner": { + "name": "claim_owner", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "completed_at": { + "name": "completed_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "dedupe_key": { + "name": "dedupe_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "id": { + "name": "id", + "type": "text CHECK (\"id\" = upper(\"id\") AND length(\"id\") = 26 AND substr(\"id\", 1, 1) GLOB '[0-7]' AND \"id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "last_error_code": { + "name": "last_error_code", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_error_message": { + "name": "last_error_message", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "payload_json": { + "name": "payload_json", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "api_command_dedupe_idx": { + "name": "api_command_dedupe_idx", + "columns": ["dedupe_key"], + "isUnique": true + }, + "api_command_status_updated_idx": { + "name": "api_command_status_updated_idx", + "columns": ["status", "updated_at"], + "isUnique": false + }, + "api_command_claim_idx": { + "name": "api_command_claim_idx", + "columns": ["status", "claim_expires_at"], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "auth_account": { + "name": "auth_account", + "columns": { + "access_token": { + "name": "access_token", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "access_token_expires_at": { + "name": "access_token_expires_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "provider_account_id": { + "name": "provider_account_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "id_token": { + "name": "id_token", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "refresh_token": { + "name": "refresh_token", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "refresh_token_expires_at": { + "name": "refresh_token_expires_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "account_id": { + "name": "account_id", + "type": "text CHECK (\"account_id\" = upper(\"account_id\") AND length(\"account_id\") = 26 AND substr(\"account_id\", 1, 1) GLOB '[0-7]' AND \"account_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "auth_account_provider_account_idx": { + "name": "auth_account_provider_account_idx", + "columns": ["provider_id", "provider_account_id"], + "isUnique": true + }, + "auth_account_account_id_idx": { + "name": "auth_account_account_id_idx", + "columns": ["account_id"], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "auth_session": { + "name": "auth_session", + "columns": { + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "ip_address": { + "name": "ip_address", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "account_id": { + "name": "account_id", + "type": "text CHECK (\"account_id\" = upper(\"account_id\") AND length(\"account_id\") = 26 AND substr(\"account_id\", 1, 1) GLOB '[0-7]' AND \"account_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "auth_session_expires_at_idx": { + "name": "auth_session_expires_at_idx", + "columns": ["expires_at"], + "isUnique": false + }, + "auth_session_token_idx": { + "name": "auth_session_token_idx", + "columns": ["token"], + "isUnique": true + }, + "auth_session_account_id_idx": { + "name": "auth_session_account_id_idx", + "columns": ["account_id"], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "auth_verification": { + "name": "auth_verification", + "columns": { + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "identifier": { + "name": "identifier", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "auth_verification_expires_at_idx": { + "name": "auth_verification_expires_at_idx", + "columns": ["expires_at"], + "isUnique": false + }, + "auth_verification_identifier_idx": { + "name": "auth_verification_identifier_idx", + "columns": ["identifier"], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "cli_oauth_flow": { + "name": "cli_oauth_flow", + "columns": { + "account_id": { + "name": "account_id", + "type": "text CHECK (\"account_id\" = upper(\"account_id\") AND length(\"account_id\") = 26 AND substr(\"account_id\", 1, 1) GLOB '[0-7]' AND \"account_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "authorized_at": { + "name": "authorized_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "completed_at": { + "name": "completed_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "device_code_hash": { + "name": "device_code_hash", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "hostname": { + "name": "hostname", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "id": { + "name": "id", + "type": "text CHECK (\"id\" = upper(\"id\") AND length(\"id\") = 26 AND substr(\"id\", 1, 1) GLOB '[0-7]' AND \"id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_code": { + "name": "user_code", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "cli_oauth_flow_status_expires_idx": { + "name": "cli_oauth_flow_status_expires_idx", + "columns": ["status", "expires_at"], + "isUnique": false + }, + "cli_oauth_flow_device_code_hash_idx": { + "name": "cli_oauth_flow_device_code_hash_idx", + "columns": ["device_code_hash"], + "isUnique": true + }, + "cli_oauth_flow_user_code_idx": { + "name": "cli_oauth_flow_user_code_idx", + "columns": ["user_code"], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "personal_access_token": { + "name": "personal_access_token", + "columns": { + "account_id": { + "name": "account_id", + "type": "text CHECK (\"account_id\" = upper(\"account_id\") AND length(\"account_id\") = 26 AND substr(\"account_id\", 1, 1) GLOB '[0-7]' AND \"account_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "id": { + "name": "id", + "type": "text CHECK (\"id\" = upper(\"id\") AND length(\"id\") = 26 AND substr(\"id\", 1, 1) GLOB '[0-7]' AND \"id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "last_used_at": { + "name": "last_used_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "revoked_at": { + "name": "revoked_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "token_hash": { + "name": "token_hash", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "personal_access_token_account_created_idx": { + "name": "personal_access_token_account_created_idx", + "columns": ["account_id", "created_at"], + "isUnique": false + }, + "personal_access_token_hash_idx": { + "name": "personal_access_token_hash_idx", + "columns": ["token_hash"], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "email_log": { + "name": "email_log", + "columns": { + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "recipient_domain": { + "name": "recipient_domain", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "recipient_masked": { + "name": "recipient_masked", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "subject": { + "name": "subject", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "email_log_created_at_idx": { + "name": "email_log_created_at_idx", + "columns": ["created_at"], + "isUnique": false + }, + "email_log_type_status_idx": { + "name": "email_log_type_status_idx", + "columns": ["type", "status"], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "environment_revision": { + "name": "environment_revision", + "columns": { + "allow_mcp_servers": { + "name": "allow_mcp_servers", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "allow_package_managers": { + "name": "allow_package_managers", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "allowed_hosts_json": { + "name": "allowed_hosts_json", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_by_account_id": { + "name": "created_by_account_id", + "type": "text CHECK (\"created_by_account_id\" = upper(\"created_by_account_id\") AND length(\"created_by_account_id\") = 26 AND substr(\"created_by_account_id\", 1, 1) GLOB '[0-7]' AND \"created_by_account_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "env_vars_json": { + "name": "env_vars_json", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "environment_id": { + "name": "environment_id", + "type": "text CHECK (\"environment_id\" = upper(\"environment_id\") AND length(\"environment_id\") = 26 AND substr(\"environment_id\", 1, 1) GLOB '[0-7]' AND \"environment_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "id": { + "name": "id", + "type": "text CHECK (\"id\" = upper(\"id\") AND length(\"id\") = 26 AND substr(\"id\", 1, 1) GLOB '[0-7]' AND \"id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "network_policy": { + "name": "network_policy", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "packages_json": { + "name": "packages_json", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "app_id": { + "name": "app_id", + "type": "text CHECK (\"app_id\" = upper(\"app_id\") AND length(\"app_id\") = 26 AND substr(\"app_id\", 1, 1) GLOB '[0-7]' AND \"app_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "setup_script": { + "name": "setup_script", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "environment_revision_environment_created_at_idx": { + "name": "environment_revision_environment_created_at_idx", + "columns": ["environment_id", "created_at"], + "isUnique": false + }, + "environment_revision_app_created_at_idx": { + "name": "environment_revision_app_created_at_idx", + "columns": ["app_id", "created_at"], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": { + "environment_revision_network_policy_check": { + "name": "environment_revision_network_policy_check", + "value": "\"environment_revision\".\"network_policy\" IN ('full', 'limited')" + } + } + }, + "environment": { + "name": "environment", + "columns": { + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "current_revision_id": { + "name": "current_revision_id", + "type": "text CHECK (\"current_revision_id\" = upper(\"current_revision_id\") AND length(\"current_revision_id\") = 26 AND substr(\"current_revision_id\", 1, 1) GLOB '[0-7]' AND \"current_revision_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "forked_from_environment_id": { + "name": "forked_from_environment_id", + "type": "text CHECK (\"forked_from_environment_id\" = upper(\"forked_from_environment_id\") AND length(\"forked_from_environment_id\") = 26 AND substr(\"forked_from_environment_id\", 1, 1) GLOB '[0-7]' AND \"forked_from_environment_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "forked_from_environment_name": { + "name": "forked_from_environment_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "forked_from_owner_name": { + "name": "forked_from_owner_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "id": { + "name": "id", + "type": "text CHECK (\"id\" = upper(\"id\") AND length(\"id\") = 26 AND substr(\"id\", 1, 1) GLOB '[0-7]' AND \"id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "owner_account_id": { + "name": "owner_account_id", + "type": "text CHECK (\"owner_account_id\" = upper(\"owner_account_id\") AND length(\"owner_account_id\") = 26 AND substr(\"owner_account_id\", 1, 1) GLOB '[0-7]' AND \"owner_account_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "app_id": { + "name": "app_id", + "type": "text CHECK (\"app_id\" = upper(\"app_id\") AND length(\"app_id\") = 26 AND substr(\"app_id\", 1, 1) GLOB '[0-7]' AND \"app_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "environment_app_updated_at_idx": { + "name": "environment_app_updated_at_idx", + "columns": ["app_id", "updated_at"], + "isUnique": false + }, + "environment_owner_updated_at_idx": { + "name": "environment_owner_updated_at_idx", + "columns": ["owner_account_id", "updated_at"], + "isUnique": false + }, + "environment_owner_name_idx": { + "name": "environment_owner_name_idx", + "columns": ["app_id", "owner_account_id", "name"], + "isUnique": true, + "where": "\"environment\".\"owner_account_id\" IS NOT NULL" + }, + "environment_system_default_idx": { + "name": "environment_system_default_idx", + "columns": ["app_id"], + "isUnique": true, + "where": "\"environment\".\"owner_account_id\" IS NULL" + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "file_record": { + "name": "file_record", + "columns": { + "committed": { + "name": "committed", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_by_account_id": { + "name": "created_by_account_id", + "type": "text CHECK (\"created_by_account_id\" = upper(\"created_by_account_id\") AND length(\"created_by_account_id\") = 26 AND substr(\"created_by_account_id\", 1, 1) GLOB '[0-7]' AND \"created_by_account_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "etag": { + "name": "etag", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "id": { + "name": "id", + "type": "text CHECK (\"id\" = upper(\"id\") AND length(\"id\") = 26 AND substr(\"id\", 1, 1) GLOB '[0-7]' AND \"id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "mime_type": { + "name": "mime_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "object_key": { + "name": "object_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "owner_id": { + "name": "owner_id", + "type": "text CHECK (\"owner_id\" = upper(\"owner_id\") AND length(\"owner_id\") = 26 AND substr(\"owner_id\", 1, 1) GLOB '[0-7]' AND \"owner_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "owner_kind": { + "name": "owner_kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "parent_path": { + "name": "parent_path", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "path": { + "name": "path", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "purpose": { + "name": "purpose", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "scope_id": { + "name": "scope_id", + "type": "text CHECK (\"scope_id\" = upper(\"scope_id\") AND length(\"scope_id\") = 26 AND substr(\"scope_id\", 1, 1) GLOB '[0-7]' AND \"scope_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "scope_kind": { + "name": "scope_kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "session_kind": { + "name": "session_kind", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "size": { + "name": "size", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "version": { + "name": "version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "file_record_object_key_idx": { + "name": "file_record_object_key_idx", + "columns": ["object_key"], + "isUnique": true + }, + "file_record_unscoped_parent_path_name_status_idx": { + "name": "file_record_unscoped_parent_path_name_status_idx", + "columns": ["scope_kind", "parent_path", "name", "status"], + "isUnique": true, + "where": "\"file_record\".\"scope_id\" IS NULL" + }, + "file_record_scoped_parent_path_name_status_idx": { + "name": "file_record_scoped_parent_path_name_status_idx", + "columns": ["scope_kind", "scope_id", "parent_path", "name", "status"], + "isUnique": true + }, + "file_record_unscoped_pending_path_idx": { + "name": "file_record_unscoped_pending_path_idx", + "columns": ["scope_kind", "path"], + "isUnique": true, + "where": "\"file_record\".\"status\" = 'pending' AND \"file_record\".\"scope_id\" IS NULL" + }, + "file_record_scoped_pending_path_idx": { + "name": "file_record_scoped_pending_path_idx", + "columns": ["scope_kind", "scope_id", "path"], + "isUnique": true, + "where": "\"file_record\".\"status\" = 'pending' AND \"file_record\".\"scope_id\" IS NOT NULL" + }, + "file_record_unscoped_ready_path_idx": { + "name": "file_record_unscoped_ready_path_idx", + "columns": ["scope_kind", "path"], + "isUnique": true, + "where": "\"file_record\".\"status\" = 'ready' AND \"file_record\".\"scope_id\" IS NULL" + }, + "file_record_scoped_ready_path_idx": { + "name": "file_record_scoped_ready_path_idx", + "columns": ["scope_kind", "scope_id", "path"], + "isUnique": true, + "where": "\"file_record\".\"status\" = 'ready' AND \"file_record\".\"scope_id\" IS NOT NULL" + }, + "file_record_governance_idx": { + "name": "file_record_governance_idx", + "columns": ["purpose", "owner_kind", "owner_id", "status", "expires_at"], + "isUnique": false + }, + "file_record_listing_idx": { + "name": "file_record_listing_idx", + "columns": ["scope_kind", "scope_id", "parent_path", "status", "lower(\"name\")"], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "file_upload": { + "name": "file_upload", + "columns": { + "content_type": { + "name": "content_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_by_account_id": { + "name": "created_by_account_id", + "type": "text CHECK (\"created_by_account_id\" = upper(\"created_by_account_id\") AND length(\"created_by_account_id\") = 26 AND substr(\"created_by_account_id\", 1, 1) GLOB '[0-7]' AND \"created_by_account_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expected_size": { + "name": "expected_size", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "file_id": { + "name": "file_id", + "type": "text CHECK (\"file_id\" = upper(\"file_id\") AND length(\"file_id\") = 26 AND substr(\"file_id\", 1, 1) GLOB '[0-7]' AND \"file_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "id": { + "name": "id", + "type": "text CHECK (\"id\" = upper(\"id\") AND length(\"id\") = 26 AND substr(\"id\", 1, 1) GLOB '[0-7]' AND \"id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "if_match_etag": { + "name": "if_match_etag", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "multipart_upload_id": { + "name": "multipart_upload_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "overwrite": { + "name": "overwrite", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "part_size": { + "name": "part_size", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "scope_id": { + "name": "scope_id", + "type": "text CHECK (\"scope_id\" = upper(\"scope_id\") AND length(\"scope_id\") = 26 AND substr(\"scope_id\", 1, 1) GLOB '[0-7]' AND \"scope_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "scope_kind": { + "name": "scope_kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "strategy": { + "name": "strategy", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "file_upload_file_id_idx": { + "name": "file_upload_file_id_idx", + "columns": ["file_id"], + "isUnique": true + }, + "file_upload_status_expires_idx": { + "name": "file_upload_status_expires_idx", + "columns": ["status", "expires_at"], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "file_version": { + "name": "file_version", + "columns": { + "committed": { + "name": "committed", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "committed_at": { + "name": "committed_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_by_account_id": { + "name": "created_by_account_id", + "type": "text CHECK (\"created_by_account_id\" = upper(\"created_by_account_id\") AND length(\"created_by_account_id\") = 26 AND substr(\"created_by_account_id\", 1, 1) GLOB '[0-7]' AND \"created_by_account_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "file_id": { + "name": "file_id", + "type": "text CHECK (\"file_id\" = upper(\"file_id\") AND length(\"file_id\") = 26 AND substr(\"file_id\", 1, 1) GLOB '[0-7]' AND \"file_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "id": { + "name": "id", + "type": "text CHECK (\"id\" = upper(\"id\") AND length(\"id\") = 26 AND substr(\"id\", 1, 1) GLOB '[0-7]' AND \"id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "mime_type": { + "name": "mime_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "object_key": { + "name": "object_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "path": { + "name": "path", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "reason": { + "name": "reason", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "scope_id": { + "name": "scope_id", + "type": "text CHECK (\"scope_id\" = upper(\"scope_id\") AND length(\"scope_id\") = 26 AND substr(\"scope_id\", 1, 1) GLOB '[0-7]' AND \"scope_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "scope_kind": { + "name": "scope_kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "size": { + "name": "size", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "source_etag": { + "name": "source_etag", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "source_object_key": { + "name": "source_object_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "version": { + "name": "version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "file_version_object_key_idx": { + "name": "file_version_object_key_idx", + "columns": ["object_key"], + "isUnique": true + }, + "file_version_scope_path_created_idx": { + "name": "file_version_scope_path_created_idx", + "columns": ["scope_kind", "scope_id", "path", "created_at"], + "isUnique": false + }, + "file_version_file_created_idx": { + "name": "file_version_file_created_idx", + "columns": ["file_id", "created_at"], + "isUnique": false + }, + "file_version_pending_idx": { + "name": "file_version_pending_idx", + "columns": ["committed", "created_at"], + "isUnique": false, + "where": "\"file_version\".\"committed\" = 0" + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "mcp_credential": { + "name": "mcp_credential", + "columns": { + "account_id": { + "name": "account_id", + "type": "text CHECK (\"account_id\" = upper(\"account_id\") AND length(\"account_id\") = 26 AND substr(\"account_id\", 1, 1) GLOB '[0-7]' AND \"account_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "agent_id": { + "name": "agent_id", + "type": "text CHECK (\"agent_id\" = upper(\"agent_id\") AND length(\"agent_id\") = 26 AND substr(\"agent_id\", 1, 1) GLOB '[0-7]' AND \"agent_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "auth_type": { + "name": "auth_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "id": { + "name": "id", + "type": "text CHECK (\"id\" = upper(\"id\") AND length(\"id\") = 26 AND substr(\"id\", 1, 1) GLOB '[0-7]' AND \"id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "last_refreshed_at": { + "name": "last_refreshed_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "oauth_client_id": { + "name": "oauth_client_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "oauth_client_secret_secret_id": { + "name": "oauth_client_secret_secret_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "app_id": { + "name": "app_id", + "type": "text CHECK (\"app_id\" = upper(\"app_id\") AND length(\"app_id\") = 26 AND substr(\"app_id\", 1, 1) GLOB '[0-7]' AND \"app_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "refresh_secret_id": { + "name": "refresh_secret_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "scope_values_json": { + "name": "scope_values_json", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "secret_id": { + "name": "secret_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "server_id": { + "name": "server_id", + "type": "text CHECK (\"server_id\" = upper(\"server_id\") AND length(\"server_id\") = 26 AND substr(\"server_id\", 1, 1) GLOB '[0-7]' AND \"server_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "subject_label": { + "name": "subject_label", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "mcp_credential_server_scope_status_idx": { + "name": "mcp_credential_server_scope_status_idx", + "columns": ["server_id", "scope", "status"], + "isUnique": false + }, + "mcp_credential_app_scope_status_idx": { + "name": "mcp_credential_app_scope_status_idx", + "columns": ["app_id", "scope", "status"], + "isUnique": false + }, + "mcp_credential_app_scope_idx": { + "name": "mcp_credential_app_scope_idx", + "columns": ["server_id", "scope"], + "isUnique": true, + "where": "\"mcp_credential\".\"scope\" = 'app'" + }, + "mcp_credential_agent_scope_idx": { + "name": "mcp_credential_agent_scope_idx", + "columns": ["server_id", "agent_id", "scope"], + "isUnique": true, + "where": "\"mcp_credential\".\"scope\" = 'agent' AND \"mcp_credential\".\"agent_id\" IS NOT NULL" + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": { + "mcp_credential_scope_shape_check": { + "name": "mcp_credential_scope_shape_check", + "value": "\n (\"mcp_credential\".\"scope\" = 'app' AND \"mcp_credential\".\"account_id\" IS NULL AND \"mcp_credential\".\"agent_id\" IS NULL)\n OR (\"mcp_credential\".\"scope\" = 'agent' AND \"mcp_credential\".\"account_id\" IS NULL AND \"mcp_credential\".\"agent_id\" IS NOT NULL)\n " + }, + "mcp_credential_scope_values_json_check": { + "name": "mcp_credential_scope_values_json_check", + "value": "\n \"mcp_credential\".\"scope_values_json\" IS NULL\n OR (json_valid(\"mcp_credential\".\"scope_values_json\") AND json_type(\"mcp_credential\".\"scope_values_json\") = 'array')\n " + }, + "mcp_credential_bearer_shape_check": { + "name": "mcp_credential_bearer_shape_check", + "value": "\n \"mcp_credential\".\"auth_type\" != 'bearer'\n OR (\n \"mcp_credential\".\"oauth_client_id\" IS NULL\n AND \"mcp_credential\".\"oauth_client_secret_secret_id\" IS NULL\n AND \"mcp_credential\".\"refresh_secret_id\" IS NULL\n )\n " + } + } + }, + "mcp_oauth_flow": { + "name": "mcp_oauth_flow", + "columns": { + "authorization_endpoint": { + "name": "authorization_endpoint", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "cleanup_after": { + "name": "cleanup_after", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "code_verifier": { + "name": "code_verifier", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "completed_at": { + "name": "completed_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "id": { + "name": "id", + "type": "text CHECK (\"id\" = upper(\"id\") AND length(\"id\") = 26 AND substr(\"id\", 1, 1) GLOB '[0-7]' AND \"id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "initiator_account_id": { + "name": "initiator_account_id", + "type": "text CHECK (\"initiator_account_id\" = upper(\"initiator_account_id\") AND length(\"initiator_account_id\") = 26 AND substr(\"initiator_account_id\", 1, 1) GLOB '[0-7]' AND \"initiator_account_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "oauth_client_id": { + "name": "oauth_client_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "oauth_client_secret_secret_id": { + "name": "oauth_client_secret_secret_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "app_id": { + "name": "app_id", + "type": "text CHECK (\"app_id\" = upper(\"app_id\") AND length(\"app_id\") = 26 AND substr(\"app_id\", 1, 1) GLOB '[0-7]' AND \"app_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "registration_endpoint": { + "name": "registration_endpoint", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "return_url": { + "name": "return_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "scope_values_json": { + "name": "scope_values_json", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "server_id": { + "name": "server_id", + "type": "text CHECK (\"server_id\" = upper(\"server_id\") AND length(\"server_id\") = 26 AND substr(\"server_id\", 1, 1) GLOB '[0-7]' AND \"server_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "subject_label": { + "name": "subject_label", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "token_endpoint": { + "name": "token_endpoint", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "mcp_oauth_flow_status_cleanup_after_idx": { + "name": "mcp_oauth_flow_status_cleanup_after_idx", + "columns": ["status", "cleanup_after"], + "isUnique": false + }, + "mcp_oauth_flow_expires_at_idx": { + "name": "mcp_oauth_flow_expires_at_idx", + "columns": ["expires_at"], + "isUnique": false + }, + "mcp_oauth_flow_server_account_idx": { + "name": "mcp_oauth_flow_server_account_idx", + "columns": ["server_id", "initiator_account_id"], + "isUnique": false + }, + "mcp_oauth_flow_app_server_account_idx": { + "name": "mcp_oauth_flow_app_server_account_idx", + "columns": ["app_id", "server_id", "initiator_account_id"], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": { + "mcp_oauth_flow_scope_values_json_check": { + "name": "mcp_oauth_flow_scope_values_json_check", + "value": "\n \"mcp_oauth_flow\".\"scope_values_json\" IS NULL\n OR (json_valid(\"mcp_oauth_flow\".\"scope_values_json\") AND json_type(\"mcp_oauth_flow\".\"scope_values_json\") = 'array')\n " + } + } + }, + "mcp_server": { + "name": "mcp_server", + "columns": { + "auth_type": { + "name": "auth_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "byo_client_id": { + "name": "byo_client_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "byo_client_secret_secret_id": { + "name": "byo_client_secret_secret_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "credential_scope": { + "name": "credential_scope", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "enabled": { + "name": "enabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "icon_url": { + "name": "icon_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "id": { + "name": "id", + "type": "text CHECK (\"id\" = upper(\"id\") AND length(\"id\") = 26 AND substr(\"id\", 1, 1) GLOB '[0-7]' AND \"id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "oauth_metadata_json": { + "name": "oauth_metadata_json", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "owner_account_id": { + "name": "owner_account_id", + "type": "text CHECK (\"owner_account_id\" = upper(\"owner_account_id\") AND length(\"owner_account_id\") = 26 AND substr(\"owner_account_id\", 1, 1) GLOB '[0-7]' AND \"owner_account_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "app_id": { + "name": "app_id", + "type": "text CHECK (\"app_id\" = upper(\"app_id\") AND length(\"app_id\") = 26 AND substr(\"app_id\", 1, 1) GLOB '[0-7]' AND \"app_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "mcp_server_app_enabled_idx": { + "name": "mcp_server_app_enabled_idx", + "columns": ["app_id", "enabled"], + "isUnique": false + }, + "mcp_server_owner_app_idx": { + "name": "mcp_server_owner_app_idx", + "columns": ["owner_account_id", "app_id"], + "isUnique": false + }, + "mcp_server_app_url_idx": { + "name": "mcp_server_app_url_idx", + "columns": ["app_id", "url"], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": { + "mcp_server_source_scope_check": { + "name": "mcp_server_source_scope_check", + "value": "\"mcp_server\".\"source\" = 'app' AND \"mcp_server\".\"credential_scope\" = 'app'" + } + } + }, + "vault_secret": { + "name": "vault_secret", + "columns": { + "algorithm": { + "name": "algorithm", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'AES-GCM'" + }, + "ciphertext": { + "name": "ciphertext", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "ciphertext_iv": { + "name": "ciphertext_iv", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "id": { + "name": "id", + "type": "text CHECK (\"id\" = upper(\"id\") AND length(\"id\") = 26 AND substr(\"id\", 1, 1) GLOB '[0-7]' AND \"id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "wrapped_dek": { + "name": "wrapped_dek", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "wrapped_dek_iv": { + "name": "wrapped_dek_iv", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "vault_secret_kind_created_at_idx": { + "name": "vault_secret_kind_created_at_idx", + "columns": ["kind", "created_at"], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "organization": { + "name": "organization", + "columns": { + "avatar_url": { + "name": "avatar_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "creator_account_id": { + "name": "creator_account_id", + "type": "text CHECK (\"creator_account_id\" = upper(\"creator_account_id\") AND length(\"creator_account_id\") = 26 AND substr(\"creator_account_id\", 1, 1) GLOB '[0-7]' AND \"creator_account_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "id": { + "name": "id", + "type": "text CHECK (\"id\" = upper(\"id\") AND length(\"id\") = 26 AND substr(\"id\", 1, 1) GLOB '[0-7]' AND \"id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "organization_creator_account_idx": { + "name": "organization_creator_account_idx", + "columns": ["creator_account_id"], + "isUnique": true, + "where": "\"organization\".\"creator_account_id\" IS NOT NULL" + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "app_deployment_run": { + "name": "app_deployment_run", + "columns": { + "app_id": { + "name": "app_id", + "type": "text CHECK (\"app_id\" = upper(\"app_id\") AND length(\"app_id\") = 26 AND substr(\"app_id\", 1, 1) GLOB '[0-7]' AND \"app_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "deployment_id": { + "name": "deployment_id", + "type": "text CHECK (\"deployment_id\" = upper(\"deployment_id\") AND length(\"deployment_id\") = 26 AND substr(\"deployment_id\", 1, 1) GLOB '[0-7]' AND \"deployment_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "error_code": { + "name": "error_code", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "external_deployment_id": { + "name": "external_deployment_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "external_project_id": { + "name": "external_project_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "external_version_id": { + "name": "external_version_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "generated_wrangler_config_json": { + "name": "generated_wrangler_config_json", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "id": { + "name": "id", + "type": "text CHECK (\"id\" = upper(\"id\") AND length(\"id\") = 26 AND substr(\"id\", 1, 1) GLOB '[0-7]' AND \"id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "mosoo_config_json": { + "name": "mosoo_config_json", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "plan_json": { + "name": "plan_json", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "source_branch": { + "name": "source_branch", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "source_commit_sha": { + "name": "source_commit_sha", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "target_kind": { + "name": "target_kind", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "target_project_name": { + "name": "target_project_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "target_script_name": { + "name": "target_script_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "app_deployment_run_app_id_idx": { + "name": "app_deployment_run_app_id_idx", + "columns": ["app_id", "id"], + "isUnique": false + }, + "app_deployment_run_deployment_id_idx": { + "name": "app_deployment_run_deployment_id_idx", + "columns": ["deployment_id", "id"], + "isUnique": false + }, + "app_deployment_run_active_app_idx": { + "name": "app_deployment_run_active_app_idx", + "columns": ["app_id"], + "isUnique": true, + "where": "\"app_deployment_run\".\"status\" IN ('queued', 'preparing', 'building', 'submitting', 'submitted', 'activating')" + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": { + "app_deployment_run_status_check": { + "name": "app_deployment_run_status_check", + "value": "\"app_deployment_run\".\"status\" IN ('queued', 'preparing', 'building', 'submitting', 'submitted', 'activating', 'success', 'failed')" + }, + "app_deployment_run_target_kind_check": { + "name": "app_deployment_run_target_kind_check", + "value": "\"app_deployment_run\".\"target_kind\" IS NULL OR \"app_deployment_run\".\"target_kind\" IN ('cloudflare_pages', 'cloudflare_worker')" + } + } + }, + "app_deployment_secret": { + "name": "app_deployment_secret", + "columns": { + "app_id": { + "name": "app_id", + "type": "text CHECK (\"app_id\" = upper(\"app_id\") AND length(\"app_id\") = 26 AND substr(\"app_id\", 1, 1) GLOB '[0-7]' AND \"app_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "vault_secret_id": { + "name": "vault_secret_id", + "type": "text CHECK (\"vault_secret_id\" = upper(\"vault_secret_id\") AND length(\"vault_secret_id\") = 26 AND substr(\"vault_secret_id\", 1, 1) GLOB '[0-7]' AND \"vault_secret_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "app_deployment_secret_app_name_idx": { + "name": "app_deployment_secret_app_name_idx", + "columns": ["app_id", "name"], + "isUnique": true + }, + "app_deployment_secret_vault_secret_idx": { + "name": "app_deployment_secret_vault_secret_idx", + "columns": ["vault_secret_id"], + "isUnique": true + } + }, + "foreignKeys": { + "app_deployment_secret_vault_secret_id_vault_secret_id_fk": { + "name": "app_deployment_secret_vault_secret_id_vault_secret_id_fk", + "tableFrom": "app_deployment_secret", + "tableTo": "vault_secret", + "columnsFrom": ["vault_secret_id"], + "columnsTo": ["id"], + "onDelete": "restrict", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "app_deployment": { + "name": "app_deployment", + "columns": { + "app_id": { + "name": "app_id", + "type": "text CHECK (\"app_id\" = upper(\"app_id\") AND length(\"app_id\") = 26 AND substr(\"app_id\", 1, 1) GLOB '[0-7]' AND \"app_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "default_branch": { + "name": "default_branch", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "deleted_at": { + "name": "deleted_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "id": { + "name": "id", + "type": "text CHECK (\"id\" = upper(\"id\") AND length(\"id\") = 26 AND substr(\"id\", 1, 1) GLOB '[0-7]' AND \"id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "last_successful_url": { + "name": "last_successful_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "latest_run_id": { + "name": "latest_run_id", + "type": "text CHECK (\"latest_run_id\" = upper(\"latest_run_id\") AND length(\"latest_run_id\") = 26 AND substr(\"latest_run_id\", 1, 1) GLOB '[0-7]' AND \"latest_run_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "mosoo_subdomain": { + "name": "mosoo_subdomain", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "owner_account_id": { + "name": "owner_account_id", + "type": "text CHECK (\"owner_account_id\" = upper(\"owner_account_id\") AND length(\"owner_account_id\") = 26 AND substr(\"owner_account_id\", 1, 1) GLOB '[0-7]' AND \"owner_account_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "repo_name": { + "name": "repo_name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "repo_owner": { + "name": "repo_owner", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "repo_url": { + "name": "repo_url", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "source_kind": { + "name": "source_kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "app_deployment_active_app_idx": { + "name": "app_deployment_active_app_idx", + "columns": ["app_id"], + "isUnique": true, + "where": "\"app_deployment\".\"deleted_at\" IS NULL" + }, + "app_deployment_active_subdomain_idx": { + "name": "app_deployment_active_subdomain_idx", + "columns": ["mosoo_subdomain"], + "isUnique": true, + "where": "\"app_deployment\".\"deleted_at\" IS NULL" + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": { + "app_deployment_source_kind_check": { + "name": "app_deployment_source_kind_check", + "value": "\"app_deployment\".\"source_kind\" IN ('github_public')" + } + } + }, + "app": { + "name": "app", + "columns": { + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "default_environment_id": { + "name": "default_environment_id", + "type": "text CHECK (\"default_environment_id\" = upper(\"default_environment_id\") AND length(\"default_environment_id\") = 26 AND substr(\"default_environment_id\", 1, 1) GLOB '[0-7]' AND \"default_environment_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "id": { + "name": "id", + "type": "text CHECK (\"id\" = upper(\"id\") AND length(\"id\") = 26 AND substr(\"id\", 1, 1) GLOB '[0-7]' AND \"id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "organization_id": { + "name": "organization_id", + "type": "text CHECK (\"organization_id\" = upper(\"organization_id\") AND length(\"organization_id\") = 26 AND substr(\"organization_id\", 1, 1) GLOB '[0-7]' AND \"organization_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "owner_account_id": { + "name": "owner_account_id", + "type": "text CHECK (\"owner_account_id\" = upper(\"owner_account_id\") AND length(\"owner_account_id\") = 26 AND substr(\"owner_account_id\", 1, 1) GLOB '[0-7]' AND \"owner_account_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "bound_agent_call_idempotency_key": { + "name": "bound_agent_call_idempotency_key", + "columns": { + "body_hash": { + "name": "body_hash", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "id": { + "name": "id", + "type": "text CHECK (\"id\" = upper(\"id\") AND length(\"id\") = 26 AND substr(\"id\", 1, 1) GLOB '[0-7]' AND \"id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "idempotency_key": { + "name": "idempotency_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "run_id": { + "name": "run_id", + "type": "text CHECK (\"run_id\" = upper(\"run_id\") AND length(\"run_id\") = 26 AND substr(\"run_id\", 1, 1) GLOB '[0-7]' AND \"run_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "session_id": { + "name": "session_id", + "type": "text CHECK (\"session_id\" = upper(\"session_id\") AND length(\"session_id\") = 26 AND substr(\"session_id\", 1, 1) GLOB '[0-7]' AND \"session_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "subject_hash": { + "name": "subject_hash", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "bound_agent_call_idempotency_subject_key_idx": { + "name": "bound_agent_call_idempotency_subject_key_idx", + "columns": ["subject_hash", "idempotency_key"], + "isUnique": true + }, + "bound_agent_call_idempotency_updated_idx": { + "name": "bound_agent_call_idempotency_updated_idx", + "columns": ["updated_at"], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "public_api_idempotency_key": { + "name": "public_api_idempotency_key", + "columns": { + "body_hash": { + "name": "body_hash", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "id": { + "name": "id", + "type": "text CHECK (\"id\" = upper(\"id\") AND length(\"id\") = 26 AND substr(\"id\", 1, 1) GLOB '[0-7]' AND \"id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "idempotency_key": { + "name": "idempotency_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "method": { + "name": "method", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "response_json": { + "name": "response_json", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "response_status": { + "name": "response_status", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "route": { + "name": "route", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "token_id": { + "name": "token_id", + "type": "text CHECK (\"token_id\" = upper(\"token_id\") AND length(\"token_id\") = 26 AND substr(\"token_id\", 1, 1) GLOB '[0-7]' AND \"token_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "public_api_idempotency_token_key_idx": { + "name": "public_api_idempotency_token_key_idx", + "columns": ["token_id", "idempotency_key"], + "isUnique": true + }, + "public_api_idempotency_updated_idx": { + "name": "public_api_idempotency_updated_idx", + "columns": ["updated_at"], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "public_api_rate_limit_window": { + "name": "public_api_rate_limit_window", + "columns": { + "bucket_key": { + "name": "bucket_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "request_count": { + "name": "request_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "shard": { + "name": "shard", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "window_start": { + "name": "window_start", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "public_api_rate_limit_window_updated_idx": { + "name": "public_api_rate_limit_window_updated_idx", + "columns": ["updated_at"], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "public_api_rate_limit_window_bucket_key_window_start_shard_pk": { + "columns": ["bucket_key", "window_start", "shard"], + "name": "public_api_rate_limit_window_bucket_key_window_start_shard_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "driver_command": { + "name": "driver_command", + "columns": { + "acked_at": { + "name": "acked_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "completed_at": { + "name": "completed_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "delivery_connection_id": { + "name": "delivery_connection_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "driver_generation": { + "name": "driver_generation", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "driver_instance_id": { + "name": "driver_instance_id", + "type": "text CHECK (\"driver_instance_id\" = upper(\"driver_instance_id\") AND length(\"driver_instance_id\") = 26 AND substr(\"driver_instance_id\", 1, 1) GLOB '[0-7]' AND \"driver_instance_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "error_json": { + "name": "error_json", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "id": { + "name": "id", + "type": "text CHECK (\"id\" = upper(\"id\") AND length(\"id\") = 26 AND substr(\"id\", 1, 1) GLOB '[0-7]' AND \"id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "issued_at": { + "name": "issued_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "payload_json": { + "name": "payload_json", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "result_json": { + "name": "result_json", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "seq": { + "name": "seq", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "driver_command_instance_seq_idx": { + "name": "driver_command_instance_seq_idx", + "columns": ["driver_instance_id", "seq"], + "isUnique": true + }, + "driver_command_instance_status_idx": { + "name": "driver_command_instance_status_idx", + "columns": ["driver_instance_id", "status", "expires_at"], + "isUnique": false + } + }, + "foreignKeys": { + "driver_command_driver_instance_id_driver_instance_id_fk": { + "name": "driver_command_driver_instance_id_driver_instance_id_fk", + "tableFrom": "driver_command", + "tableTo": "driver_instance", + "columnsFrom": ["driver_instance_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": { + "driver_command_generation_check": { + "name": "driver_command_generation_check", + "value": "\"driver_command\".\"driver_generation\" IS NULL OR (typeof(\"driver_command\".\"driver_generation\") = 'integer' AND \"driver_command\".\"driver_generation\" BETWEEN 0 AND 9007199254740991)" + }, + "driver_command_nonterminal_generation_check": { + "name": "driver_command_nonterminal_generation_check", + "value": "\"driver_command\".\"status\" IN ('completed', 'failed', 'expired', 'cancelled') OR \"driver_command\".\"driver_generation\" IS NOT NULL" + } + } + }, + "driver_instance_mcp_grant": { + "name": "driver_instance_mcp_grant", + "columns": { + "auth_type": { + "name": "auth_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "authorization_state": { + "name": "authorization_state", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "can_invalidate": { + "name": "can_invalidate", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "can_refresh": { + "name": "can_refresh", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "credential_id": { + "name": "credential_id", + "type": "text CHECK (\"credential_id\" = upper(\"credential_id\") AND length(\"credential_id\") = 26 AND substr(\"credential_id\", 1, 1) GLOB '[0-7]' AND \"credential_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "driver_instance_id": { + "name": "driver_instance_id", + "type": "text CHECK (\"driver_instance_id\" = upper(\"driver_instance_id\") AND length(\"driver_instance_id\") = 26 AND substr(\"driver_instance_id\", 1, 1) GLOB '[0-7]' AND \"driver_instance_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "app_id": { + "name": "app_id", + "type": "text CHECK (\"app_id\" = upper(\"app_id\") AND length(\"app_id\") = 26 AND substr(\"app_id\", 1, 1) GLOB '[0-7]' AND \"app_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "server_id": { + "name": "server_id", + "type": "text CHECK (\"server_id\" = upper(\"server_id\") AND length(\"server_id\") = 26 AND substr(\"server_id\", 1, 1) GLOB '[0-7]' AND \"server_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "driver_instance_mcp_grant_instance_server_idx": { + "name": "driver_instance_mcp_grant_instance_server_idx", + "columns": ["driver_instance_id", "server_id"], + "isUnique": true + }, + "driver_instance_mcp_grant_instance_credential_idx": { + "name": "driver_instance_mcp_grant_instance_credential_idx", + "columns": ["driver_instance_id", "credential_id"], + "isUnique": false + } + }, + "foreignKeys": { + "driver_instance_mcp_grant_driver_instance_id_driver_instance_id_fk": { + "name": "driver_instance_mcp_grant_driver_instance_id_driver_instance_id_fk", + "tableFrom": "driver_instance_mcp_grant", + "tableTo": "driver_instance", + "columnsFrom": ["driver_instance_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "driver_instance": { + "name": "driver_instance", + "columns": { + "boot_token_expires_at": { + "name": "boot_token_expires_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "boot_token_hash": { + "name": "boot_token_hash", + "type": "blob", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "boot_token_used_at": { + "name": "boot_token_used_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "close_code": { + "name": "close_code", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "close_reason": { + "name": "close_reason", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "connection_id": { + "name": "connection_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "command_seq_cursor": { + "name": "command_seq_cursor", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "driver_pid": { + "name": "driver_pid", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "driver_started_at": { + "name": "driver_started_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "driver_version": { + "name": "driver_version", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "heartbeat_count": { + "name": "heartbeat_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "generation": { + "name": "generation", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "id": { + "name": "id", + "type": "text CHECK (\"id\" = upper(\"id\") AND length(\"id\") = 26 AND substr(\"id\", 1, 1) GLOB '[0-7]' AND \"id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "last_heartbeat_at": { + "name": "last_heartbeat_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "process_id": { + "name": "process_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "protocol": { + "name": "protocol", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "protocol_version": { + "name": "protocol_version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "restart_count": { + "name": "restart_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "runtime": { + "name": "runtime", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "sandbox_id": { + "name": "sandbox_id", + "type": "text CHECK (\"sandbox_id\" = upper(\"sandbox_id\") AND length(\"sandbox_id\") = 26 AND substr(\"sandbox_id\", 1, 1) GLOB '[0-7]' AND \"sandbox_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "sandbox_session_id": { + "name": "sandbox_session_id", + "type": "text CHECK (\"sandbox_session_id\" = upper(\"sandbox_session_id\") AND length(\"sandbox_session_id\") = 26 AND substr(\"sandbox_session_id\", 1, 1) GLOB '[0-7]' AND \"sandbox_session_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status_changed_at": { + "name": "status_changed_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "status_event": { + "name": "status_event", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'driver.provision'" + }, + "status_operation_id": { + "name": "status_operation_id", + "type": "text CHECK (\"status_operation_id\" = upper(\"status_operation_id\") AND length(\"status_operation_id\") = 26 AND substr(\"status_operation_id\", 1, 1) GLOB '[0-7]' AND \"status_operation_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status_seq": { + "name": "status_seq", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "status_source": { + "name": "status_source", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'system'" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "driver_instance_completed_idx": { + "name": "driver_instance_completed_idx", + "columns": ["expires_at", "status"], + "isUnique": false + }, + "driver_instance_connection_idx": { + "name": "driver_instance_connection_idx", + "columns": ["connection_id"], + "isUnique": true, + "where": "\"driver_instance\".\"connection_id\" IS NOT NULL" + }, + "driver_instance_boot_token_expiry_idx": { + "name": "driver_instance_boot_token_expiry_idx", + "columns": ["status", "boot_token_expires_at"], + "isUnique": false, + "where": "\"driver_instance\".\"boot_token_used_at\" IS NULL" + }, + "driver_instance_boot_token_hash_idx": { + "name": "driver_instance_boot_token_hash_idx", + "columns": ["boot_token_hash"], + "isUnique": true + }, + "driver_instance_sandbox_session_idx": { + "name": "driver_instance_sandbox_session_idx", + "columns": ["sandbox_id", "sandbox_session_id", "status", "updated_at"], + "isUnique": false + }, + "driver_instance_live_sandbox_session_idx": { + "name": "driver_instance_live_sandbox_session_idx", + "columns": ["sandbox_id", "sandbox_session_id"], + "isUnique": true, + "where": "\"driver_instance\".\"status\" IN ('provisioning', 'connecting', 'ready', 'stopping')" + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": { + "driver_instance_status_check": { + "name": "driver_instance_status_check", + "value": "\"driver_instance\".\"status\" IN ('provisioning', 'connecting', 'ready', 'stopping', 'stopped', 'failed')" + }, + "driver_instance_status_seq_check": { + "name": "driver_instance_status_seq_check", + "value": "\"driver_instance\".\"status_seq\" >= 0" + } + } + }, + "external_tool_effect_attempt": { + "name": "external_tool_effect_attempt", + "columns": { + "attempt": { + "name": "attempt", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "claim_token": { + "name": "claim_token", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "completed_at": { + "name": "completed_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "effect_id": { + "name": "effect_id", + "type": "text CHECK (\"effect_id\" = upper(\"effect_id\") AND length(\"effect_id\") = 26 AND substr(\"effect_id\", 1, 1) GLOB '[0-7]' AND \"effect_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "provider_receipt_json": { + "name": "provider_receipt_json", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "result_json": { + "name": "result_json", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "external_tool_effect_attempt_status_idx": { + "name": "external_tool_effect_attempt_status_idx", + "columns": ["status", "created_at"], + "isUnique": false + } + }, + "foreignKeys": { + "external_tool_effect_attempt_effect_id_external_tool_effect_id_fk": { + "name": "external_tool_effect_attempt_effect_id_external_tool_effect_id_fk", + "tableFrom": "external_tool_effect_attempt", + "tableTo": "external_tool_effect", + "columnsFrom": ["effect_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "external_tool_effect_attempt_effect_id_attempt_pk": { + "columns": ["effect_id", "attempt"], + "name": "external_tool_effect_attempt_effect_id_attempt_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": { + "external_tool_effect_attempt_status_check": { + "name": "external_tool_effect_attempt_status_check", + "value": "\"external_tool_effect_attempt\".\"status\" IN ('claimed', 'succeeded', 'unknown')" + }, + "external_tool_effect_attempt_claim_token_uuid_check": { + "name": "external_tool_effect_attempt_claim_token_uuid_check", + "value": "length(\"external_tool_effect_attempt\".\"claim_token\") = 36 AND length(replace(\"external_tool_effect_attempt\".\"claim_token\", '-', '')) = 32 AND \"external_tool_effect_attempt\".\"claim_token\" = lower(\"external_tool_effect_attempt\".\"claim_token\") AND substr(\"external_tool_effect_attempt\".\"claim_token\", 9, 1) = '-' AND substr(\"external_tool_effect_attempt\".\"claim_token\", 14, 1) = '-' AND substr(\"external_tool_effect_attempt\".\"claim_token\", 15, 1) = '4' AND substr(\"external_tool_effect_attempt\".\"claim_token\", 19, 1) = '-' AND substr(\"external_tool_effect_attempt\".\"claim_token\", 20, 1) GLOB '[89ab]' AND substr(\"external_tool_effect_attempt\".\"claim_token\", 24, 1) = '-' AND replace(\"external_tool_effect_attempt\".\"claim_token\", '-', '') NOT GLOB '*[^0-9a-f]*'" + } + } + }, + "external_tool_effect": { + "name": "external_tool_effect", + "columns": { + "attempt_count": { + "name": "attempt_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "claim_token": { + "name": "claim_token", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "command_id": { + "name": "command_id", + "type": "text CHECK (\"command_id\" = upper(\"command_id\") AND length(\"command_id\") = 26 AND substr(\"command_id\", 1, 1) GLOB '[0-7]' AND \"command_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "driver_instance_id": { + "name": "driver_instance_id", + "type": "text CHECK (\"driver_instance_id\" = upper(\"driver_instance_id\") AND length(\"driver_instance_id\") = 26 AND substr(\"driver_instance_id\", 1, 1) GLOB '[0-7]' AND \"driver_instance_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "id": { + "name": "id", + "type": "text CHECK (\"id\" = upper(\"id\") AND length(\"id\") = 26 AND substr(\"id\", 1, 1) GLOB '[0-7]' AND \"id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "idempotency_key": { + "name": "idempotency_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "provider_receipt_json": { + "name": "provider_receipt_json", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "result_json": { + "name": "result_json", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "server_id": { + "name": "server_id", + "type": "text CHECK (\"server_id\" = upper(\"server_id\") AND length(\"server_id\") = 26 AND substr(\"server_id\", 1, 1) GLOB '[0-7]' AND \"server_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "session_run_id": { + "name": "session_run_id", + "type": "text CHECK (\"session_run_id\" = upper(\"session_run_id\") AND length(\"session_run_id\") = 26 AND substr(\"session_run_id\", 1, 1) GLOB '[0-7]' AND \"session_run_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "tool_name": { + "name": "tool_name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "external_tool_effect_command_idx": { + "name": "external_tool_effect_command_idx", + "columns": ["command_id"], + "isUnique": true + }, + "external_tool_effect_idempotency_key_idx": { + "name": "external_tool_effect_idempotency_key_idx", + "columns": ["idempotency_key"], + "isUnique": true + }, + "external_tool_effect_run_status_idx": { + "name": "external_tool_effect_run_status_idx", + "columns": ["session_run_id", "status", "id"], + "isUnique": false + }, + "external_tool_effect_driver_status_idx": { + "name": "external_tool_effect_driver_status_idx", + "columns": ["driver_instance_id", "status"], + "isUnique": false + } + }, + "foreignKeys": { + "external_tool_effect_command_id_driver_command_id_fk": { + "name": "external_tool_effect_command_id_driver_command_id_fk", + "tableFrom": "external_tool_effect", + "tableTo": "driver_command", + "columnsFrom": ["command_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "external_tool_effect_driver_instance_id_driver_instance_id_fk": { + "name": "external_tool_effect_driver_instance_id_driver_instance_id_fk", + "tableFrom": "external_tool_effect", + "tableTo": "driver_instance", + "columnsFrom": ["driver_instance_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "external_tool_effect_session_run_id_session_run_id_fk": { + "name": "external_tool_effect_session_run_id_session_run_id_fk", + "tableFrom": "external_tool_effect", + "tableTo": "session_run", + "columnsFrom": ["session_run_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": { + "external_tool_effect_status_check": { + "name": "external_tool_effect_status_check", + "value": "\"external_tool_effect\".\"status\" IN ('intent', 'claimed', 'succeeded', 'unknown')" + }, + "external_tool_effect_claim_token_uuid_check": { + "name": "external_tool_effect_claim_token_uuid_check", + "value": "\"external_tool_effect\".\"claim_token\" IS NULL OR (length(\"external_tool_effect\".\"claim_token\") = 36 AND length(replace(\"external_tool_effect\".\"claim_token\", '-', '')) = 32 AND \"external_tool_effect\".\"claim_token\" = lower(\"external_tool_effect\".\"claim_token\") AND substr(\"external_tool_effect\".\"claim_token\", 9, 1) = '-' AND substr(\"external_tool_effect\".\"claim_token\", 14, 1) = '-' AND substr(\"external_tool_effect\".\"claim_token\", 15, 1) = '4' AND substr(\"external_tool_effect\".\"claim_token\", 19, 1) = '-' AND substr(\"external_tool_effect\".\"claim_token\", 20, 1) GLOB '[89ab]' AND substr(\"external_tool_effect\".\"claim_token\", 24, 1) = '-' AND replace(\"external_tool_effect\".\"claim_token\", '-', '') NOT GLOB '*[^0-9a-f]*')" + } + } + }, + "native_resume_ref": { + "name": "native_resume_ref", + "columns": { + "committed_session_run_id": { + "name": "committed_session_run_id", + "type": "text CHECK (\"committed_session_run_id\" = upper(\"committed_session_run_id\") AND length(\"committed_session_run_id\") = 26 AND substr(\"committed_session_run_id\", 1, 1) GLOB '[0-7]' AND \"committed_session_run_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "committed_value": { + "name": "committed_value", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "observed_driver_instance_id": { + "name": "observed_driver_instance_id", + "type": "text CHECK (\"observed_driver_instance_id\" = upper(\"observed_driver_instance_id\") AND length(\"observed_driver_instance_id\") = 26 AND substr(\"observed_driver_instance_id\", 1, 1) GLOB '[0-7]' AND \"observed_driver_instance_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "observed_session_run_id": { + "name": "observed_session_run_id", + "type": "text CHECK (\"observed_session_run_id\" = upper(\"observed_session_run_id\") AND length(\"observed_session_run_id\") = 26 AND substr(\"observed_session_run_id\", 1, 1) GLOB '[0-7]' AND \"observed_session_run_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "runtime_id": { + "name": "runtime_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "session_id": { + "name": "session_id", + "type": "text CHECK (\"session_id\" = upper(\"session_id\") AND length(\"session_id\") = 26 AND substr(\"session_id\", 1, 1) GLOB '[0-7]' AND \"session_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "native_resume_ref_runtime_updated_idx": { + "name": "native_resume_ref_runtime_updated_idx", + "columns": ["runtime_id", "updated_at"], + "isUnique": false + } + }, + "foreignKeys": { + "native_resume_ref_session_id_session_id_fk": { + "name": "native_resume_ref_session_id_session_id_fk", + "tableFrom": "native_resume_ref", + "tableTo": "session", + "columnsFrom": ["session_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "sandbox_backup": { + "name": "sandbox_backup", + "columns": { + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "dir": { + "name": "dir", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "id": { + "name": "id", + "type": "text CHECK (\"id\" = upper(\"id\") AND length(\"id\") = 26 AND substr(\"id\", 1, 1) GLOB '[0-7]' AND \"id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "keep": { + "name": "keep", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "sandbox_id": { + "name": "sandbox_id", + "type": "text CHECK (\"sandbox_id\" = upper(\"sandbox_id\") AND length(\"sandbox_id\") = 26 AND substr(\"sandbox_id\", 1, 1) GLOB '[0-7]' AND \"sandbox_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "session_run_id": { + "name": "session_run_id", + "type": "text CHECK (\"session_run_id\" = upper(\"session_run_id\") AND length(\"session_run_id\") = 26 AND substr(\"session_run_id\", 1, 1) GLOB '[0-7]' AND \"session_run_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "ttl_seconds": { + "name": "ttl_seconds", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "sandbox_backup_sandbox_status_created_idx": { + "name": "sandbox_backup_sandbox_status_created_idx", + "columns": ["sandbox_id", "status", "created_at"], + "isUnique": false + }, + "sandbox_backup_terminal_checkpoint_idx": { + "name": "sandbox_backup_terminal_checkpoint_idx", + "columns": ["sandbox_id", "dir", "session_run_id"], + "isUnique": true, + "where": "\"sandbox_backup\".\"session_run_id\" IS NOT NULL AND \"sandbox_backup\".\"status\" = 'ready'" + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "sandbox_session": { + "name": "sandbox_session", + "columns": { + "cloudflare_session_id": { + "name": "cloudflare_session_id", + "type": "text CHECK (\"cloudflare_session_id\" = upper(\"cloudflare_session_id\") AND length(\"cloudflare_session_id\") = 26 AND substr(\"cloudflare_session_id\", 1, 1) GLOB '[0-7]' AND \"cloudflare_session_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "cwd": { + "name": "cwd", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "origin_json": { + "name": "origin_json", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "sandbox_id": { + "name": "sandbox_id", + "type": "text CHECK (\"sandbox_id\" = upper(\"sandbox_id\") AND length(\"sandbox_id\") = 26 AND substr(\"sandbox_id\", 1, 1) GLOB '[0-7]' AND \"sandbox_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "session_id": { + "name": "session_id", + "type": "text CHECK (\"session_id\" = upper(\"session_id\") AND length(\"session_id\") = 26 AND substr(\"session_id\", 1, 1) GLOB '[0-7]' AND \"session_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "sandbox_session_sandbox_status_idx": { + "name": "sandbox_session_sandbox_status_idx", + "columns": ["sandbox_id", "status", "updated_at"], + "isUnique": false + }, + "sandbox_session_cloudflare_session_idx": { + "name": "sandbox_session_cloudflare_session_idx", + "columns": ["cloudflare_session_id"], + "isUnique": true + } + }, + "foreignKeys": { + "sandbox_session_session_id_session_id_fk": { + "name": "sandbox_session_session_id_session_id_fk", + "tableFrom": "sandbox_session", + "tableTo": "session", + "columnsFrom": ["session_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "sandbox": { + "name": "sandbox", + "columns": { + "agent_id": { + "name": "agent_id", + "type": "text CHECK (\"agent_id\" = upper(\"agent_id\") AND length(\"agent_id\") = 26 AND substr(\"agent_id\", 1, 1) GLOB '[0-7]' AND \"agent_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "app_id": { + "name": "app_id", + "type": "text CHECK (\"app_id\" = upper(\"app_id\") AND length(\"app_id\") = 26 AND substr(\"app_id\", 1, 1) GLOB '[0-7]' AND \"app_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "bind_mount_ready": { + "name": "bind_mount_ready", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "claim_expires_at": { + "name": "claim_expires_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "claim_owner": { + "name": "claim_owner", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "global_mounts_json": { + "name": "global_mounts_json", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'[]'" + }, + "id": { + "name": "id", + "type": "text CHECK (\"id\" = upper(\"id\") AND length(\"id\") = 26 AND substr(\"id\", 1, 1) GLOB '[0-7]' AND \"id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "inactive_deadline_at": { + "name": "inactive_deadline_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "last_backup_id": { + "name": "last_backup_id", + "type": "text CHECK (\"last_backup_id\" = upper(\"last_backup_id\") AND length(\"last_backup_id\") = 26 AND substr(\"last_backup_id\", 1, 1) GLOB '[0-7]' AND \"last_backup_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_error_code": { + "name": "last_error_code", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_restore_backup_id": { + "name": "last_restore_backup_id", + "type": "text CHECK (\"last_restore_backup_id\" = upper(\"last_restore_backup_id\") AND length(\"last_restore_backup_id\") = 26 AND substr(\"last_restore_backup_id\", 1, 1) GLOB '[0-7]' AND \"last_restore_backup_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "owner_account_id": { + "name": "owner_account_id", + "type": "text CHECK (\"owner_account_id\" = upper(\"owner_account_id\") AND length(\"owner_account_id\") = 26 AND substr(\"owner_account_id\", 1, 1) GLOB '[0-7]' AND \"owner_account_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status_changed_at": { + "name": "status_changed_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "status_event": { + "name": "status_event", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'runtime_subject.cold'" + }, + "status_operation_id": { + "name": "status_operation_id", + "type": "text CHECK (\"status_operation_id\" = upper(\"status_operation_id\") AND length(\"status_operation_id\") = 26 AND substr(\"status_operation_id\", 1, 1) GLOB '[0-7]' AND \"status_operation_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status_seq": { + "name": "status_seq", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "status_source": { + "name": "status_source", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'system'" + }, + "subject_id": { + "name": "subject_id", + "type": "text CHECK (\"subject_id\" = upper(\"subject_id\") AND length(\"subject_id\") = 26 AND substr(\"subject_id\", 1, 1) GLOB '[0-7]' AND \"subject_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "subject_kind": { + "name": "subject_kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "sandbox_subject_idx": { + "name": "sandbox_subject_idx", + "columns": ["kind", "subject_kind", "subject_id"], + "isUnique": true + }, + "sandbox_status_deadline_idx": { + "name": "sandbox_status_deadline_idx", + "columns": ["status", "inactive_deadline_at", "updated_at"], + "isUnique": false + }, + "sandbox_claim_idx": { + "name": "sandbox_claim_idx", + "columns": ["claim_expires_at", "claim_owner"], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": { + "sandbox_status_check": { + "name": "sandbox_status_check", + "value": "\"sandbox\".\"status\" IN ('cold', 'restoring', 'active', 'backing_up', 'destroying', 'error')" + }, + "sandbox_status_seq_check": { + "name": "sandbox_status_seq_check", + "value": "\"sandbox\".\"status_seq\" >= 0" + } + } + }, + "session_message": { + "name": "session_message", + "columns": { + "content_text": { + "name": "content_text", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_by_account_id": { + "name": "created_by_account_id", + "type": "text CHECK (\"created_by_account_id\" = upper(\"created_by_account_id\") AND length(\"created_by_account_id\") = 26 AND substr(\"created_by_account_id\", 1, 1) GLOB '[0-7]' AND \"created_by_account_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "id": { + "name": "id", + "type": "text CHECK (\"id\" = upper(\"id\") AND length(\"id\") = 26 AND substr(\"id\", 1, 1) GLOB '[0-7]' AND \"id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "plan_json": { + "name": "plan_json", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "projection_format": { + "name": "projection_format", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'materialized'" + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "segments_json": { + "name": "segments_json", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "seq": { + "name": "seq", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "session_id": { + "name": "session_id", + "type": "text CHECK (\"session_id\" = upper(\"session_id\") AND length(\"session_id\") = 26 AND substr(\"session_id\", 1, 1) GLOB '[0-7]' AND \"session_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "session_run_id": { + "name": "session_run_id", + "type": "text CHECK (\"session_run_id\" = upper(\"session_run_id\") AND length(\"session_run_id\") = 26 AND substr(\"session_run_id\", 1, 1) GLOB '[0-7]' AND \"session_run_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "session_message_session_seq_idx": { + "name": "session_message_session_seq_idx", + "columns": ["session_id", "seq"], + "isUnique": true + }, + "session_message_run_idx": { + "name": "session_message_run_idx", + "columns": ["session_run_id"], + "isUnique": false + } + }, + "foreignKeys": { + "session_message_session_id_session_id_fk": { + "name": "session_message_session_id_session_id_fk", + "tableFrom": "session_message", + "tableTo": "session", + "columnsFrom": ["session_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": { + "session_message_projection_format_check": { + "name": "session_message_projection_format_check", + "value": "\"session_message\".\"projection_format\" IN ('materialized', 'event_stream_v3')" + }, + "session_message_event_stream_v3_check": { + "name": "session_message_event_stream_v3_check", + "value": "\"session_message\".\"projection_format\" <> 'event_stream_v3' OR (\"session_message\".\"role\" = 'assistant' AND \"session_message\".\"session_run_id\" IS NOT NULL AND \"session_message\".\"content_text\" = '' AND \"session_message\".\"plan_json\" IS NULL AND \"session_message\".\"segments_json\" IS NULL)" + } + } + }, + "session": { + "name": "session", + "columns": { + "agent_id": { + "name": "agent_id", + "type": "text CHECK (\"agent_id\" = upper(\"agent_id\") AND length(\"agent_id\") = 26 AND substr(\"agent_id\", 1, 1) GLOB '[0-7]' AND \"agent_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "archived_at": { + "name": "archived_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "cleanup_operation_kind": { + "name": "cleanup_operation_kind", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "end_user_id": { + "name": "end_user_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "attributed_user_id": { + "name": "attributed_user_id", + "type": "text CHECK (\"attributed_user_id\" = upper(\"attributed_user_id\") AND length(\"attributed_user_id\") = 26 AND substr(\"attributed_user_id\", 1, 1) GLOB '[0-7]' AND \"attributed_user_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "creator_account_id": { + "name": "creator_account_id", + "type": "text CHECK (\"creator_account_id\" = upper(\"creator_account_id\") AND length(\"creator_account_id\") = 26 AND substr(\"creator_account_id\", 1, 1) GLOB '[0-7]' AND \"creator_account_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "deployment_version_id": { + "name": "deployment_version_id", + "type": "text CHECK (\"deployment_version_id\" = upper(\"deployment_version_id\") AND length(\"deployment_version_id\") = 26 AND substr(\"deployment_version_id\", 1, 1) GLOB '[0-7]' AND \"deployment_version_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "deployment_version_number": { + "name": "deployment_version_number", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "id": { + "name": "id", + "type": "text CHECK (\"id\" = upper(\"id\") AND length(\"id\") = 26 AND substr(\"id\", 1, 1) GLOB '[0-7]' AND \"id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "last_message_at": { + "name": "last_message_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_run_id": { + "name": "last_run_id", + "type": "text CHECK (\"last_run_id\" = upper(\"last_run_id\") AND length(\"last_run_id\") = 26 AND substr(\"last_run_id\", 1, 1) GLOB '[0-7]' AND \"last_run_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "message_seq_cursor": { + "name": "message_seq_cursor", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "metadata_json": { + "name": "metadata_json", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'{}'" + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "app_id": { + "name": "app_id", + "type": "text CHECK (\"app_id\" = upper(\"app_id\") AND length(\"app_id\") = 26 AND substr(\"app_id\", 1, 1) GLOB '[0-7]' AND \"app_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "renamed": { + "name": "renamed", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "runtime_id": { + "name": "runtime_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status_operation_id": { + "name": "status_operation_id", + "type": "text CHECK (\"status_operation_id\" = upper(\"status_operation_id\") AND length(\"status_operation_id\") = 26 AND substr(\"status_operation_id\", 1, 1) GLOB '[0-7]' AND \"status_operation_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status_seq": { + "name": "status_seq", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "runtime_event_seq_cursor": { + "name": "runtime_event_seq_cursor", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "runtime_provisioning_heartbeat_at": { + "name": "runtime_provisioning_heartbeat_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "runtime_provisioning_operation_id": { + "name": "runtime_provisioning_operation_id", + "type": "text CHECK (\"runtime_provisioning_operation_id\" = upper(\"runtime_provisioning_operation_id\") AND length(\"runtime_provisioning_operation_id\") = 26 AND substr(\"runtime_provisioning_operation_id\", 1, 1) GLOB '[0-7]' AND \"runtime_provisioning_operation_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "runtime_provisioning_run_id": { + "name": "runtime_provisioning_run_id", + "type": "text CHECK (\"runtime_provisioning_run_id\" = upper(\"runtime_provisioning_run_id\") AND length(\"runtime_provisioning_run_id\") = 26 AND substr(\"runtime_provisioning_run_id\", 1, 1) GLOB '[0-7]' AND \"runtime_provisioning_run_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "runtime_provisioning_sandbox_id": { + "name": "runtime_provisioning_sandbox_id", + "type": "text CHECK (\"runtime_provisioning_sandbox_id\" = upper(\"runtime_provisioning_sandbox_id\") AND length(\"runtime_provisioning_sandbox_id\") = 26 AND substr(\"runtime_provisioning_sandbox_id\", 1, 1) GLOB '[0-7]' AND \"runtime_provisioning_sandbox_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'preview'" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "workspace_checkpoint_required": { + "name": "workspace_checkpoint_required", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + } + }, + "indexes": { + "session_agent_updated_idx": { + "name": "session_agent_updated_idx", + "columns": ["agent_id", "updated_at", "id"], + "isUnique": false + }, + "session_app_creator_archived_updated_idx": { + "name": "session_app_creator_archived_updated_idx", + "columns": ["app_id", "creator_account_id", "archived_at", "updated_at", "id"], + "isUnique": false + }, + "session_app_attributed_archived_updated_idx": { + "name": "session_app_attributed_archived_updated_idx", + "columns": ["app_id", "attributed_user_id", "archived_at", "updated_at", "id"], + "isUnique": false + }, + "session_app_creator_type_archived_updated_idx": { + "name": "session_app_creator_type_archived_updated_idx", + "columns": ["app_id", "creator_account_id", "type", "archived_at", "updated_at", "id"], + "isUnique": false + }, + "session_app_attributed_type_archived_updated_idx": { + "name": "session_app_attributed_type_archived_updated_idx", + "columns": ["app_id", "attributed_user_id", "type", "archived_at", "updated_at", "id"], + "isUnique": false + }, + "session_status_operation_updated_idx": { + "name": "session_status_operation_updated_idx", + "columns": ["status", "status_operation_id", "updated_at"], + "isUnique": false + }, + "session_cleanup_operation_updated_idx": { + "name": "session_cleanup_operation_updated_idx", + "columns": ["cleanup_operation_kind", "status", "updated_at", "id"], + "isUnique": false + }, + "session_runtime_provisioning_heartbeat_idx": { + "name": "session_runtime_provisioning_heartbeat_idx", + "columns": ["runtime_provisioning_heartbeat_at", "id"], + "isUnique": false + }, + "session_status_updated_idx": { + "name": "session_status_updated_idx", + "columns": ["status", "updated_at", "id"], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": { + "session_cleanup_operation_kind_check": { + "name": "session_cleanup_operation_kind_check", + "value": "\"session\".\"cleanup_operation_kind\" IS NULL OR (\"session\".\"cleanup_operation_kind\" IN ('archive', 'delete') AND \"session\".\"archived_at\" IS NOT NULL AND \"session\".\"status\" IN ('IDLE', 'RESCHEDULING') AND (\"session\".\"status_operation_id\" IS NOT NULL OR (\"session\".\"cleanup_operation_kind\" = 'archive' AND \"session\".\"status\" = 'IDLE')))" + }, + "session_runtime_provisioning_lease_check": { + "name": "session_runtime_provisioning_lease_check", + "value": "(\"session\".\"runtime_provisioning_operation_id\" IS NULL AND \"session\".\"runtime_provisioning_run_id\" IS NULL AND \"session\".\"runtime_provisioning_sandbox_id\" IS NULL AND \"session\".\"runtime_provisioning_heartbeat_at\" IS NULL) OR (\"session\".\"runtime_provisioning_operation_id\" IS NOT NULL AND \"session\".\"runtime_provisioning_sandbox_id\" IS NOT NULL AND \"session\".\"runtime_provisioning_heartbeat_at\" IS NOT NULL AND typeof(\"session\".\"runtime_provisioning_heartbeat_at\") = 'integer' AND \"session\".\"runtime_provisioning_heartbeat_at\" >= 0 AND \"session\".\"archived_at\" IS NULL AND \"session\".\"cleanup_operation_kind\" IS NULL AND \"session\".\"status_operation_id\" IS NULL)" + }, + "session_status_check": { + "name": "session_status_check", + "value": "\"session\".\"status\" IN ('IDLE', 'RUNNING', 'RESCHEDULING', 'TERMINATED')" + }, + "session_status_seq_check": { + "name": "session_status_seq_check", + "value": "\"session\".\"status_seq\" >= 0" + } + } + }, + "session_execution_snapshot": { + "name": "session_execution_snapshot", + "columns": { + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "plan_json": { + "name": "plan_json", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "session_id": { + "name": "session_id", + "type": "text CHECK (\"session_id\" = upper(\"session_id\") AND length(\"session_id\") = 26 AND substr(\"session_id\", 1, 1) GLOB '[0-7]' AND \"session_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": true, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "session_execution_snapshot_session_id_session_id_fk": { + "name": "session_execution_snapshot_session_id_session_id_fk", + "tableFrom": "session_execution_snapshot", + "tableTo": "session", + "columnsFrom": ["session_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "session_run_skill": { + "name": "session_run_skill", + "columns": { + "blob_sha256": { + "name": "blob_sha256", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "materialization_status": { + "name": "materialization_status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "mount_path": { + "name": "mount_path", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "resolution_mode": { + "name": "resolution_mode", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "session_run_id": { + "name": "session_run_id", + "type": "text CHECK (\"session_run_id\" = upper(\"session_run_id\") AND length(\"session_run_id\") = 26 AND substr(\"session_run_id\", 1, 1) GLOB '[0-7]' AND \"session_run_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "skill_id": { + "name": "skill_id", + "type": "text CHECK (\"skill_id\" = upper(\"skill_id\") AND length(\"skill_id\") = 26 AND substr(\"skill_id\", 1, 1) GLOB '[0-7]' AND \"skill_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "skill_name": { + "name": "skill_name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "snapshot_id": { + "name": "snapshot_id", + "type": "text CHECK (\"snapshot_id\" = upper(\"snapshot_id\") AND length(\"snapshot_id\") = 26 AND substr(\"snapshot_id\", 1, 1) GLOB '[0-7]' AND \"snapshot_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "warning_code": { + "name": "warning_code", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "session_run_skill_run_resolution_idx": { + "name": "session_run_skill_run_resolution_idx", + "columns": ["session_run_id", "resolution_mode"], + "isUnique": false + } + }, + "foreignKeys": { + "session_run_skill_session_run_id_session_run_id_fk": { + "name": "session_run_skill_session_run_id_session_run_id_fk", + "tableFrom": "session_run_skill", + "tableTo": "session_run", + "columnsFrom": ["session_run_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "session_run_skill_session_run_id_skill_id_pk": { + "columns": ["session_run_id", "skill_id"], + "name": "session_run_skill_session_run_id_skill_id_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "session_run": { + "name": "session_run", + "columns": { + "agent_id": { + "name": "agent_id", + "type": "text CHECK (\"agent_id\" = upper(\"agent_id\") AND length(\"agent_id\") = 26 AND substr(\"agent_id\", 1, 1) GLOB '[0-7]' AND \"agent_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "bound_capability_agent_id": { + "name": "bound_capability_agent_id", + "type": "text CHECK (\"bound_capability_agent_id\" = upper(\"bound_capability_agent_id\") AND length(\"bound_capability_agent_id\") = 26 AND substr(\"bound_capability_agent_id\", 1, 1) GLOB '[0-7]' AND \"bound_capability_agent_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "bound_capability_app_id": { + "name": "bound_capability_app_id", + "type": "text CHECK (\"bound_capability_app_id\" = upper(\"bound_capability_app_id\") AND length(\"bound_capability_app_id\") = 26 AND substr(\"bound_capability_app_id\", 1, 1) GLOB '[0-7]' AND \"bound_capability_app_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "bound_capability_binding_env": { + "name": "bound_capability_binding_env", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "bound_capability_binding_name": { + "name": "bound_capability_binding_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "bound_capability_deployment_id": { + "name": "bound_capability_deployment_id", + "type": "text CHECK (\"bound_capability_deployment_id\" = upper(\"bound_capability_deployment_id\") AND length(\"bound_capability_deployment_id\") = 26 AND substr(\"bound_capability_deployment_id\", 1, 1) GLOB '[0-7]' AND \"bound_capability_deployment_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "bound_capability_deployment_run_id": { + "name": "bound_capability_deployment_run_id", + "type": "text CHECK (\"bound_capability_deployment_run_id\" = upper(\"bound_capability_deployment_run_id\") AND length(\"bound_capability_deployment_run_id\") = 26 AND substr(\"bound_capability_deployment_run_id\", 1, 1) GLOB '[0-7]' AND \"bound_capability_deployment_run_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "completed_at": { + "name": "completed_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_by_account_id": { + "name": "created_by_account_id", + "type": "text CHECK (\"created_by_account_id\" = upper(\"created_by_account_id\") AND length(\"created_by_account_id\") = 26 AND substr(\"created_by_account_id\", 1, 1) GLOB '[0-7]' AND \"created_by_account_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "deployment_version_id": { + "name": "deployment_version_id", + "type": "text CHECK (\"deployment_version_id\" = upper(\"deployment_version_id\") AND length(\"deployment_version_id\") = 26 AND substr(\"deployment_version_id\", 1, 1) GLOB '[0-7]' AND \"deployment_version_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "deployment_version_number": { + "name": "deployment_version_number", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "driver_instance_id": { + "name": "driver_instance_id", + "type": "text CHECK (\"driver_instance_id\" = upper(\"driver_instance_id\") AND length(\"driver_instance_id\") = 26 AND substr(\"driver_instance_id\", 1, 1) GLOB '[0-7]' AND \"driver_instance_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "error_code": { + "name": "error_code", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "error_details_json": { + "name": "error_details_json", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "error_retryable": { + "name": "error_retryable", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "id": { + "name": "id", + "type": "text CHECK (\"id\" = upper(\"id\") AND length(\"id\") = 26 AND substr(\"id\", 1, 1) GLOB '[0-7]' AND \"id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "runtime_id": { + "name": "runtime_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "session_id": { + "name": "session_id", + "type": "text CHECK (\"session_id\" = upper(\"session_id\") AND length(\"session_id\") = 26 AND substr(\"session_id\", 1, 1) GLOB '[0-7]' AND \"session_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "started_at": { + "name": "started_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status_changed_at": { + "name": "status_changed_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "status_event": { + "name": "status_event", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'run.queue'" + }, + "status_operation_id": { + "name": "status_operation_id", + "type": "text CHECK (\"status_operation_id\" = upper(\"status_operation_id\") AND length(\"status_operation_id\") = 26 AND substr(\"status_operation_id\", 1, 1) GLOB '[0-7]' AND \"status_operation_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status_seq": { + "name": "status_seq", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "status_source": { + "name": "status_source", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'system'" + }, + "trace_id": { + "name": "trace_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "trigger": { + "name": "trigger", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "session_run_driver_instance_idx": { + "name": "session_run_driver_instance_idx", + "columns": ["driver_instance_id", "created_at"], + "isUnique": false + }, + "session_run_active_driver_lease_idx": { + "name": "session_run_active_driver_lease_idx", + "columns": ["driver_instance_id"], + "isUnique": true, + "where": "\"session_run\".\"driver_instance_id\" IS NOT NULL AND \"session_run\".\"status\" IN ('queued', 'booting', 'running', 'waiting_input')" + }, + "session_run_session_created_at_idx": { + "name": "session_run_session_created_at_idx", + "columns": ["session_id", "created_at"], + "isUnique": false + }, + "session_run_session_status_idx": { + "name": "session_run_session_status_idx", + "columns": ["session_id", "status"], + "isUnique": false + } + }, + "foreignKeys": { + "session_run_session_id_session_id_fk": { + "name": "session_run_session_id_session_id_fk", + "tableFrom": "session_run", + "tableTo": "session", + "columnsFrom": ["session_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": { + "session_run_error_retryable_check": { + "name": "session_run_error_retryable_check", + "value": "\"session_run\".\"error_retryable\" IS NULL OR (\"session_run\".\"error_retryable\" IN (false, true) AND \"session_run\".\"error_code\" IS NOT NULL AND \"session_run\".\"error_details_json\" IS NOT NULL AND \"session_run\".\"error_message\" IS NOT NULL)" + }, + "session_run_status_check": { + "name": "session_run_status_check", + "value": "\"session_run\".\"status\" IN ('queued', 'booting', 'running', 'waiting_input', 'completed', 'failed', 'cancelled', 'expired')" + }, + "session_run_status_seq_check": { + "name": "session_run_status_seq_check", + "value": "\"session_run\".\"status_seq\" >= 0" + } + } + }, + "session_agent_task_snapshot": { + "name": "session_agent_task_snapshot", + "columns": { + "driver_instance_id": { + "name": "driver_instance_id", + "type": "text CHECK (\"driver_instance_id\" = upper(\"driver_instance_id\") AND length(\"driver_instance_id\") = 26 AND substr(\"driver_instance_id\", 1, 1) GLOB '[0-7]' AND \"driver_instance_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "run_id": { + "name": "run_id", + "type": "text CHECK (\"run_id\" = upper(\"run_id\") AND length(\"run_id\") = 26 AND substr(\"run_id\", 1, 1) GLOB '[0-7]' AND \"run_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "seq": { + "name": "seq", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "session_id": { + "name": "session_id", + "type": "text CHECK (\"session_id\" = upper(\"session_id\") AND length(\"session_id\") = 26 AND substr(\"session_id\", 1, 1) GLOB '[0-7]' AND \"session_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tasks_json": { + "name": "tasks_json", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "session_agent_task_snapshot_run_id_session_run_id_fk": { + "name": "session_agent_task_snapshot_run_id_session_run_id_fk", + "tableFrom": "session_agent_task_snapshot", + "tableTo": "session_run", + "columnsFrom": ["run_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "session_agent_task_snapshot_session_id_session_id_fk": { + "name": "session_agent_task_snapshot_session_id_session_id_fk", + "tableFrom": "session_agent_task_snapshot", + "tableTo": "session", + "columnsFrom": ["session_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "session_event": { + "name": "session_event", + "columns": { + "agent_id": { + "name": "agent_id", + "type": "text CHECK (\"agent_id\" = upper(\"agent_id\") AND length(\"agent_id\") = 26 AND substr(\"agent_id\", 1, 1) GLOB '[0-7]' AND \"agent_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "content_text": { + "name": "content_text", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "ended_at": { + "name": "ended_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "event_type": { + "name": "event_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "family": { + "name": "family", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "id": { + "name": "id", + "type": "text CHECK (\"id\" = upper(\"id\") AND length(\"id\") = 26 AND substr(\"id\", 1, 1) GLOB '[0-7]' AND \"id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "mcp_command_id": { + "name": "mcp_command_id", + "type": "text CHECK (\"mcp_command_id\" = upper(\"mcp_command_id\") AND length(\"mcp_command_id\") = 26 AND substr(\"mcp_command_id\", 1, 1) GLOB '[0-7]' AND \"mcp_command_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "occurred_at": { + "name": "occurred_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "process_status": { + "name": "process_status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "process_type": { + "name": "process_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "run_id": { + "name": "run_id", + "type": "text CHECK (\"run_id\" = upper(\"run_id\") AND length(\"run_id\") = 26 AND substr(\"run_id\", 1, 1) GLOB '[0-7]' AND \"run_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "semantic_hash": { + "name": "semantic_hash", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "seq": { + "name": "seq", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "session_id": { + "name": "session_id", + "type": "text CHECK (\"session_id\" = upper(\"session_id\") AND length(\"session_id\") = 26 AND substr(\"session_id\", 1, 1) GLOB '[0-7]' AND \"session_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "source_event_id": { + "name": "source_event_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "stream_id": { + "name": "stream_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "tool_call_id": { + "name": "tool_call_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "tool_input_delta_json": { + "name": "tool_input_delta_json", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "tool_input_json": { + "name": "tool_input_json", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "tool_name": { + "name": "tool_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "tool_output_delta_text": { + "name": "tool_output_delta_text", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "tool_output_text": { + "name": "tool_output_text", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "tool_parent_message_id": { + "name": "tool_parent_message_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "tool_result_message_id": { + "name": "tool_result_message_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "tool_status": { + "name": "tool_status", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "tokens": { + "name": "tokens", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "trace_id": { + "name": "trace_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "visibility": { + "name": "visibility", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "session_event_agent_family_created_idx": { + "name": "session_event_agent_family_created_idx", + "columns": ["agent_id", "family", "created_at", "id"], + "isUnique": false + }, + "session_event_agent_visibility_created_idx": { + "name": "session_event_agent_visibility_created_idx", + "columns": ["agent_id", "visibility", "created_at", "id"], + "isUnique": false + }, + "session_event_agent_created_idx": { + "name": "session_event_agent_created_idx", + "columns": ["agent_id", "created_at", "id"], + "isUnique": false + }, + "session_event_session_visibility_seq_idx": { + "name": "session_event_session_visibility_seq_idx", + "columns": ["session_id", "visibility", "seq"], + "isUnique": false + }, + "session_event_run_event_type_idx": { + "name": "session_event_run_event_type_idx", + "columns": ["run_id", "event_type"], + "isUnique": false + }, + "session_event_run_stream_process_seq_idx": { + "name": "session_event_run_stream_process_seq_idx", + "columns": ["run_id", "stream_id", "process_type", "seq"], + "isUnique": false + }, + "session_event_run_tool_call_seq_idx": { + "name": "session_event_run_tool_call_seq_idx", + "columns": ["run_id", "tool_call_id", "seq"], + "isUnique": false + }, + "session_event_session_seq_idx": { + "name": "session_event_session_seq_idx", + "columns": ["session_id", "seq"], + "isUnique": true + }, + "session_event_session_source_idx": { + "name": "session_event_session_source_idx", + "columns": ["session_id", "source_event_id"], + "isUnique": true + }, + "session_event_run_terminal_winner_idx": { + "name": "session_event_run_terminal_winner_idx", + "columns": ["session_id", "run_id"], + "isUnique": true, + "where": "\"session_event\".\"semantic_hash\" IS NOT NULL AND \"session_event\".\"run_id\" IS NOT NULL AND \"session_event\".\"event_type\" IN ('run.cancelled', 'run.completed', 'run.failed')" + }, + "session_event_mcp_terminal_winner_idx": { + "name": "session_event_mcp_terminal_winner_idx", + "columns": ["session_id", "mcp_command_id"], + "isUnique": true, + "where": "\"session_event\".\"mcp_command_id\" IS NOT NULL" + } + }, + "foreignKeys": { + "session_event_session_id_session_id_fk": { + "name": "session_event_session_id_session_id_fk", + "tableFrom": "session_event", + "tableTo": "session", + "columnsFrom": ["session_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": { + "session_event_mcp_command_check": { + "name": "session_event_mcp_command_check", + "value": "\"session_event\".\"mcp_command_id\" IS NULL OR (\"session_event\".\"event_type\" = 'tool.call.updated' AND \"session_event\".\"tool_status\" IS NOT NULL AND \"session_event\".\"tool_status\" IN ('completed', 'failed', 'cancelled'))" + }, + "session_event_semantic_hash_check": { + "name": "session_event_semantic_hash_check", + "value": "\"session_event\".\"semantic_hash\" IS NULL OR (length(\"session_event\".\"semantic_hash\") = 64 AND \"session_event\".\"semantic_hash\" = lower(\"session_event\".\"semantic_hash\") AND \"session_event\".\"semantic_hash\" NOT GLOB '*[^0-9a-f]*')" + }, + "session_event_tool_input_kind_check": { + "name": "session_event_tool_input_kind_check", + "value": "\"session_event\".\"tool_input_delta_json\" IS NULL OR \"session_event\".\"tool_input_json\" IS NULL" + }, + "session_event_tool_output_kind_check": { + "name": "session_event_tool_output_kind_check", + "value": "\"session_event\".\"tool_output_delta_text\" IS NULL OR \"session_event\".\"tool_output_text\" IS NULL" + }, + "session_event_tool_status_check": { + "name": "session_event_tool_status_check", + "value": "\"session_event\".\"tool_status\" IS NULL OR \"session_event\".\"tool_status\" IN ('running', 'completed', 'failed', 'cancelled')" + } + } + }, + "session_model_call": { + "name": "session_model_call", + "columns": { + "cache_creation_tokens": { + "name": "cache_creation_tokens", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "cache_read_tokens": { + "name": "cache_read_tokens", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "call_key": { + "name": "call_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "completed_at": { + "name": "completed_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "cost_currency": { + "name": "cost_currency", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "driver_instance_id": { + "name": "driver_instance_id", + "type": "text CHECK (\"driver_instance_id\" = upper(\"driver_instance_id\") AND length(\"driver_instance_id\") = 26 AND substr(\"driver_instance_id\", 1, 1) GLOB '[0-7]' AND \"driver_instance_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "error_code": { + "name": "error_code", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "id": { + "name": "id", + "type": "text CHECK (\"id\" = upper(\"id\") AND length(\"id\") = 26 AND substr(\"id\", 1, 1) GLOB '[0-7]' AND \"id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "input_tokens": { + "name": "input_tokens", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "metadata_json": { + "name": "metadata_json", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "native_call_id": { + "name": "native_call_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "output_tokens": { + "name": "output_tokens", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "session_id": { + "name": "session_id", + "type": "text CHECK (\"session_id\" = upper(\"session_id\") AND length(\"session_id\") = 26 AND substr(\"session_id\", 1, 1) GLOB '[0-7]' AND \"session_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "session_run_id": { + "name": "session_run_id", + "type": "text CHECK (\"session_run_id\" = upper(\"session_run_id\") AND length(\"session_run_id\") = 26 AND substr(\"session_run_id\", 1, 1) GLOB '[0-7]' AND \"session_run_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "started_at": { + "name": "started_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "total_cost_usd_micros": { + "name": "total_cost_usd_micros", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "trace_id": { + "name": "trace_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "session_model_call_run_created_idx": { + "name": "session_model_call_run_created_idx", + "columns": ["session_run_id", "created_at"], + "isUnique": false + }, + "session_model_call_session_created_idx": { + "name": "session_model_call_session_created_idx", + "columns": ["session_id", "created_at"], + "isUnique": false + }, + "session_model_call_run_key_idx": { + "name": "session_model_call_run_key_idx", + "columns": ["session_run_id", "call_key"], + "isUnique": true + }, + "session_model_call_native_idx": { + "name": "session_model_call_native_idx", + "columns": ["driver_instance_id", "native_call_id"], + "isUnique": true + } + }, + "foreignKeys": { + "session_model_call_session_id_session_id_fk": { + "name": "session_model_call_session_id_session_id_fk", + "tableFrom": "session_model_call", + "tableTo": "session", + "columnsFrom": ["session_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "session_model_call_session_run_id_session_run_id_fk": { + "name": "session_model_call_session_run_id_session_run_id_fk", + "tableFrom": "session_model_call", + "tableTo": "session_run", + "columnsFrom": ["session_run_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "session_permission_request": { + "name": "session_permission_request", + "columns": { + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "driver_instance_id": { + "name": "driver_instance_id", + "type": "text CHECK (\"driver_instance_id\" = upper(\"driver_instance_id\") AND length(\"driver_instance_id\") = 26 AND substr(\"driver_instance_id\", 1, 1) GLOB '[0-7]' AND \"driver_instance_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "raw_input": { + "name": "raw_input", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "request_id": { + "name": "request_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "run_id": { + "name": "run_id", + "type": "text CHECK (\"run_id\" = upper(\"run_id\") AND length(\"run_id\") = 26 AND substr(\"run_id\", 1, 1) GLOB '[0-7]' AND \"run_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "session_id": { + "name": "session_id", + "type": "text CHECK (\"session_id\" = upper(\"session_id\") AND length(\"session_id\") = 26 AND substr(\"session_id\", 1, 1) GLOB '[0-7]' AND \"session_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "tool_call_id": { + "name": "tool_call_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "tool_kind": { + "name": "tool_kind", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "session_permission_request_run_idx": { + "name": "session_permission_request_run_idx", + "columns": ["session_id", "run_id"], + "isUnique": false + } + }, + "foreignKeys": { + "session_permission_request_session_id_session_id_fk": { + "name": "session_permission_request_session_id_session_id_fk", + "tableFrom": "session_permission_request", + "tableTo": "session", + "columnsFrom": ["session_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "session_permission_request_session_id_request_id_pk": { + "columns": ["session_id", "request_id"], + "name": "session_permission_request_session_id_request_id_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "session_readiness_snapshot": { + "name": "session_readiness_snapshot", + "columns": { + "readiness_json": { + "name": "readiness_json", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "session_id": { + "name": "session_id", + "type": "text CHECK (\"session_id\" = upper(\"session_id\") AND length(\"session_id\") = 26 AND substr(\"session_id\", 1, 1) GLOB '[0-7]' AND \"session_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "session_readiness_snapshot_session_id_session_id_fk": { + "name": "session_readiness_snapshot_session_id_session_id_fk", + "tableFrom": "session_readiness_snapshot", + "tableTo": "session", + "columnsFrom": ["session_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "skill_snapshot_entry": { + "name": "skill_snapshot_entry", + "columns": { + "entry_kind": { + "name": "entry_kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "is_executable": { + "name": "is_executable", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "mime_type": { + "name": "mime_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "path": { + "name": "path", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "sha256": { + "name": "sha256", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "size": { + "name": "size", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "snapshot_id": { + "name": "snapshot_id", + "type": "text CHECK (\"snapshot_id\" = upper(\"snapshot_id\") AND length(\"snapshot_id\") = 26 AND substr(\"snapshot_id\", 1, 1) GLOB '[0-7]' AND \"snapshot_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "skill_snapshot_entry_snapshot_id_path_pk": { + "columns": ["snapshot_id", "path"], + "name": "skill_snapshot_entry_snapshot_id_path_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "skill_snapshot": { + "name": "skill_snapshot", + "columns": { + "author": { + "name": "author", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "blob_key": { + "name": "blob_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "blob_sha256": { + "name": "blob_sha256", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "blob_size": { + "name": "blob_size", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "id": { + "name": "id", + "type": "text CHECK (\"id\" = upper(\"id\") AND length(\"id\") = 26 AND substr(\"id\", 1, 1) GLOB '[0-7]' AND \"id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "app_id": { + "name": "app_id", + "type": "text CHECK (\"app_id\" = upper(\"app_id\") AND length(\"app_id\") = 26 AND substr(\"app_id\", 1, 1) GLOB '[0-7]' AND \"app_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "skill_markdown_path": { + "name": "skill_markdown_path", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "uncompressed_size": { + "name": "uncompressed_size", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "version": { + "name": "version", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "skill_snapshot_app_created_at_idx": { + "name": "skill_snapshot_app_created_at_idx", + "columns": ["app_id", "created_at"], + "isUnique": false + }, + "skill_snapshot_blob_sha256_idx": { + "name": "skill_snapshot_blob_sha256_idx", + "columns": ["app_id", "blob_sha256"], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "skill": { + "name": "skill", + "columns": { + "author": { + "name": "author", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "current_snapshot_id": { + "name": "current_snapshot_id", + "type": "text CHECK (\"current_snapshot_id\" = upper(\"current_snapshot_id\") AND length(\"current_snapshot_id\") = 26 AND substr(\"current_snapshot_id\", 1, 1) GLOB '[0-7]' AND \"current_snapshot_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "forked_from_owner_name": { + "name": "forked_from_owner_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "forked_from_skill_id": { + "name": "forked_from_skill_id", + "type": "text CHECK (\"forked_from_skill_id\" = upper(\"forked_from_skill_id\") AND length(\"forked_from_skill_id\") = 26 AND substr(\"forked_from_skill_id\", 1, 1) GLOB '[0-7]' AND \"forked_from_skill_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "forked_from_skill_name": { + "name": "forked_from_skill_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "id": { + "name": "id", + "type": "text CHECK (\"id\" = upper(\"id\") AND length(\"id\") = 26 AND substr(\"id\", 1, 1) GLOB '[0-7]' AND \"id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "owner_account_id": { + "name": "owner_account_id", + "type": "text CHECK (\"owner_account_id\" = upper(\"owner_account_id\") AND length(\"owner_account_id\") = 26 AND substr(\"owner_account_id\", 1, 1) GLOB '[0-7]' AND \"owner_account_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "app_id": { + "name": "app_id", + "type": "text CHECK (\"app_id\" = upper(\"app_id\") AND length(\"app_id\") = 26 AND substr(\"app_id\", 1, 1) GLOB '[0-7]' AND \"app_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "source_kind": { + "name": "source_kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "version": { + "name": "version", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "skill_app_updated_at_idx": { + "name": "skill_app_updated_at_idx", + "columns": ["app_id", "updated_at"], + "isUnique": false + }, + "skill_owner_account_updated_at_idx": { + "name": "skill_owner_account_updated_at_idx", + "columns": ["owner_account_id", "updated_at"], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "account": { + "name": "account", + "columns": { + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "email_verified": { + "name": "email_verified", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "id": { + "name": "id", + "type": "text CHECK (\"id\" = upper(\"id\") AND length(\"id\") = 26 AND substr(\"id\", 1, 1) GLOB '[0-7]' AND \"id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "image_url": { + "name": "image_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_active_organization_id": { + "name": "last_active_organization_id", + "type": "text CHECK (\"last_active_organization_id\" = upper(\"last_active_organization_id\") AND length(\"last_active_organization_id\") = 26 AND substr(\"last_active_organization_id\", 1, 1) GLOB '[0-7]' AND \"last_active_organization_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "system_agent_model": { + "name": "system_agent_model", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "account_email_idx": { + "name": "account_email_idx", + "columns": ["email"], + "isUnique": true + }, + "account_last_active_organization_idx": { + "name": "account_last_active_organization_idx", + "columns": ["last_active_organization_id"], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "usage_daily_rollup": { + "name": "usage_daily_rollup", + "columns": { + "actor_user_id": { + "name": "actor_user_id", + "type": "text CHECK (\"actor_user_id\" = upper(\"actor_user_id\") AND length(\"actor_user_id\") = 26 AND substr(\"actor_user_id\", 1, 1) GLOB '[0-7]' AND \"actor_user_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "agent_id": { + "name": "agent_id", + "type": "text CHECK (\"agent_id\" = upper(\"agent_id\") AND length(\"agent_id\") = 26 AND substr(\"agent_id\", 1, 1) GLOB '[0-7]' AND \"agent_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "agent_owner_user_id": { + "name": "agent_owner_user_id", + "type": "text CHECK (\"agent_owner_user_id\" = upper(\"agent_owner_user_id\") AND length(\"agent_owner_user_id\") = 26 AND substr(\"agent_owner_user_id\", 1, 1) GLOB '[0-7]' AND \"agent_owner_user_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "agent_publication_state_at_run": { + "name": "agent_publication_state_at_run", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "cache_creation_tokens": { + "name": "cache_creation_tokens", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "cache_read_tokens": { + "name": "cache_read_tokens", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "date": { + "name": "date", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "input_tokens": { + "name": "input_tokens", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "organization_id": { + "name": "organization_id", + "type": "text CHECK (\"organization_id\" = upper(\"organization_id\") AND length(\"organization_id\") = 26 AND substr(\"organization_id\", 1, 1) GLOB '[0-7]' AND \"organization_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "app_id": { + "name": "app_id", + "type": "text CHECK (\"app_id\" = upper(\"app_id\") AND length(\"app_id\") = 26 AND substr(\"app_id\", 1, 1) GLOB '[0-7]' AND \"app_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "output_tokens": { + "name": "output_tokens", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "request_count": { + "name": "request_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "run_purpose": { + "name": "run_purpose", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "total_cost_usd_micros": { + "name": "total_cost_usd_micros", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "unpriced_request_count": { + "name": "unpriced_request_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "usage_daily_rollup_app_date_idx": { + "name": "usage_daily_rollup_app_date_idx", + "columns": ["app_id", "date"], + "isUnique": false + }, + "usage_daily_rollup_organization_date_idx": { + "name": "usage_daily_rollup_organization_date_idx", + "columns": ["organization_id", "date"], + "isUnique": false + }, + "usage_daily_rollup_agent_date_idx": { + "name": "usage_daily_rollup_agent_date_idx", + "columns": ["agent_id", "date"], + "isUnique": false + }, + "usage_daily_rollup_actor_date_idx": { + "name": "usage_daily_rollup_actor_date_idx", + "columns": ["actor_user_id", "date"], + "isUnique": false + }, + "usage_daily_rollup_owner_date_idx": { + "name": "usage_daily_rollup_owner_date_idx", + "columns": ["agent_owner_user_id", "date"], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "usage_daily_rollup_organization_id_app_id_agent_id_actor_user_id_agent_owner_user_id_date_agent_publication_state_at_run_run_purpose_provider_model_pk": { + "columns": [ + "organization_id", + "app_id", + "agent_id", + "actor_user_id", + "agent_owner_user_id", + "date", + "agent_publication_state_at_run", + "run_purpose", + "provider", + "model" + ], + "name": "usage_daily_rollup_organization_id_app_id_agent_id_actor_user_id_agent_owner_user_id_date_agent_publication_state_at_run_run_purpose_provider_model_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "usage_event_rollup_receipt": { + "name": "usage_event_rollup_receipt", + "columns": { + "rolled_up_at": { + "name": "rolled_up_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "source_event_id": { + "name": "source_event_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "usage_event_rollup_receipt_rolled_up_at_idx": { + "name": "usage_event_rollup_receipt_rolled_up_at_idx", + "columns": ["rolled_up_at"], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "usage_event_rollup_receipt_source_source_event_id_pk": { + "columns": ["source", "source_event_id"], + "name": "usage_event_rollup_receipt_source_source_event_id_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "usage_event": { + "name": "usage_event", + "columns": { + "actor_user_id": { + "name": "actor_user_id", + "type": "text CHECK (\"actor_user_id\" = upper(\"actor_user_id\") AND length(\"actor_user_id\") = 26 AND substr(\"actor_user_id\", 1, 1) GLOB '[0-7]' AND \"actor_user_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "agent_id": { + "name": "agent_id", + "type": "text CHECK (\"agent_id\" = upper(\"agent_id\") AND length(\"agent_id\") = 26 AND substr(\"agent_id\", 1, 1) GLOB '[0-7]' AND \"agent_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "agent_owner_user_id": { + "name": "agent_owner_user_id", + "type": "text CHECK (\"agent_owner_user_id\" = upper(\"agent_owner_user_id\") AND length(\"agent_owner_user_id\") = 26 AND substr(\"agent_owner_user_id\", 1, 1) GLOB '[0-7]' AND \"agent_owner_user_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "agent_publication_state_at_run": { + "name": "agent_publication_state_at_run", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "agent_revision_id": { + "name": "agent_revision_id", + "type": "text CHECK (\"agent_revision_id\" = upper(\"agent_revision_id\") AND length(\"agent_revision_id\") = 26 AND substr(\"agent_revision_id\", 1, 1) GLOB '[0-7]' AND \"agent_revision_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "cache_creation_tokens": { + "name": "cache_creation_tokens", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "cache_read_tokens": { + "name": "cache_read_tokens", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "id": { + "name": "id", + "type": "text CHECK (\"id\" = upper(\"id\") AND length(\"id\") = 26 AND substr(\"id\", 1, 1) GLOB '[0-7]' AND \"id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "input_tokens": { + "name": "input_tokens", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "organization_id": { + "name": "organization_id", + "type": "text CHECK (\"organization_id\" = upper(\"organization_id\") AND length(\"organization_id\") = 26 AND substr(\"organization_id\", 1, 1) GLOB '[0-7]' AND \"organization_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "app_id": { + "name": "app_id", + "type": "text CHECK (\"app_id\" = upper(\"app_id\") AND length(\"app_id\") = 26 AND substr(\"app_id\", 1, 1) GLOB '[0-7]' AND \"app_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "output_tokens": { + "name": "output_tokens", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "price_snapshot_json": { + "name": "price_snapshot_json", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "pricing_status": { + "name": "pricing_status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "run_purpose": { + "name": "run_purpose", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "runtime_id": { + "name": "runtime_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "session_id": { + "name": "session_id", + "type": "text CHECK (\"session_id\" = upper(\"session_id\") AND length(\"session_id\") = 26 AND substr(\"session_id\", 1, 1) GLOB '[0-7]' AND \"session_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "session_run_id": { + "name": "session_run_id", + "type": "text CHECK (\"session_run_id\" = upper(\"session_run_id\") AND length(\"session_run_id\") = 26 AND substr(\"session_run_id\", 1, 1) GLOB '[0-7]' AND \"session_run_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "source_event_id": { + "name": "source_event_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "total_cost_usd_micros": { + "name": "total_cost_usd_micros", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "usage_contract": { + "name": "usage_contract", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "usage_event_app_created_idx": { + "name": "usage_event_app_created_idx", + "columns": ["app_id", "created_at"], + "isUnique": false + }, + "usage_event_organization_created_idx": { + "name": "usage_event_organization_created_idx", + "columns": ["organization_id", "created_at"], + "isUnique": false + }, + "usage_event_agent_created_idx": { + "name": "usage_event_agent_created_idx", + "columns": ["agent_id", "created_at"], + "isUnique": false + }, + "usage_event_actor_created_idx": { + "name": "usage_event_actor_created_idx", + "columns": ["actor_user_id", "created_at"], + "isUnique": false + }, + "usage_event_owner_created_idx": { + "name": "usage_event_owner_created_idx", + "columns": ["agent_owner_user_id", "created_at"], + "isUnique": false + }, + "usage_event_session_run_idx": { + "name": "usage_event_session_run_idx", + "columns": ["session_run_id"], + "isUnique": false + }, + "usage_event_source_event_idx": { + "name": "usage_event_source_event_idx", + "columns": ["source", "source_event_id"], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "vendor_credential": { + "name": "vendor_credential", + "columns": { + "api_base": { + "name": "api_base", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "api_key_secret_id": { + "name": "api_key_secret_id", + "type": "text CHECK (\"api_key_secret_id\" = upper(\"api_key_secret_id\") AND length(\"api_key_secret_id\") = 26 AND substr(\"api_key_secret_id\", 1, 1) GLOB '[0-7]' AND \"api_key_secret_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "id": { + "name": "id", + "type": "text CHECK (\"id\" = upper(\"id\") AND length(\"id\") = 26 AND substr(\"id\", 1, 1) GLOB '[0-7]' AND \"id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "is_default": { + "name": "is_default", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "models": { + "name": "models", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "app_id": { + "name": "app_id", + "type": "text CHECK (\"app_id\" = upper(\"app_id\") AND length(\"app_id\") = 26 AND substr(\"app_id\", 1, 1) GLOB '[0-7]' AND \"app_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "vendor_id": { + "name": "vendor_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "vendor_credential_app_vendor_idx": { + "name": "vendor_credential_app_vendor_idx", + "columns": ["app_id", "vendor_id"], + "isUnique": false + }, + "vendor_credential_app_vendor_name_idx": { + "name": "vendor_credential_app_vendor_name_idx", + "columns": ["app_id", "vendor_id", "name"], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + } + }, + "views": {}, + "enums": {}, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + }, + "internal": { + "indexes": { + "file_record_listing_idx": { + "columns": { + "lower(\"name\")": { + "isExpression": true + } + } + } + } + } +} diff --git a/pkgs/db/drizzle/meta/0016_snapshot.json b/pkgs/db/drizzle/meta/0016_snapshot.json new file mode 100644 index 00000000..cf4d144d --- /dev/null +++ b/pkgs/db/drizzle/meta/0016_snapshot.json @@ -0,0 +1,7194 @@ +{ + "version": "6", + "dialect": "sqlite", + "id": "0e5397c2-d7b5-4fff-bc06-7bff73b243cc", + "prevId": "66e7b9f7-1123-4ef2-ae35-9b7a24476711", + "tables": { + "agent_deployment_version": { + "name": "agent_deployment_version", + "columns": { + "agent_id": { + "name": "agent_id", + "type": "text CHECK (\"agent_id\" = upper(\"agent_id\") AND length(\"agent_id\") = 26 AND substr(\"agent_id\", 1, 1) GLOB '[0-7]' AND \"agent_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "config_json": { + "name": "config_json", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_by_account_id": { + "name": "created_by_account_id", + "type": "text CHECK (\"created_by_account_id\" = upper(\"created_by_account_id\") AND length(\"created_by_account_id\") = 26 AND substr(\"created_by_account_id\", 1, 1) GLOB '[0-7]' AND \"created_by_account_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "environment_id": { + "name": "environment_id", + "type": "text CHECK (\"environment_id\" = upper(\"environment_id\") AND length(\"environment_id\") = 26 AND substr(\"environment_id\", 1, 1) GLOB '[0-7]' AND \"environment_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "id": { + "name": "id", + "type": "text CHECK (\"id\" = upper(\"id\") AND length(\"id\") = 26 AND substr(\"id\", 1, 1) GLOB '[0-7]' AND \"id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "mcp_bindings_json": { + "name": "mcp_bindings_json", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "prompt": { + "name": "prompt", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "runtime_id": { + "name": "runtime_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "skills_json": { + "name": "skills_json", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "summary": { + "name": "summary", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "version_number": { + "name": "version_number", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "agent_deployment_version_agent_number_idx": { + "name": "agent_deployment_version_agent_number_idx", + "columns": ["agent_id", "version_number"], + "isUnique": true + }, + "agent_deployment_version_agent_created_idx": { + "name": "agent_deployment_version_agent_created_idx", + "columns": ["agent_id", "created_at"], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "agent_mcp_binding": { + "name": "agent_mcp_binding", + "columns": { + "agent_credential_id": { + "name": "agent_credential_id", + "type": "text CHECK (\"agent_credential_id\" = upper(\"agent_credential_id\") AND length(\"agent_credential_id\") = 26 AND substr(\"agent_credential_id\", 1, 1) GLOB '[0-7]' AND \"agent_credential_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "agent_id": { + "name": "agent_id", + "type": "text CHECK (\"agent_id\" = upper(\"agent_id\") AND length(\"agent_id\") = 26 AND substr(\"agent_id\", 1, 1) GLOB '[0-7]' AND \"agent_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "credential_mode": { + "name": "credential_mode", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'runtime_resolved'" + }, + "enabled": { + "name": "enabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "id": { + "name": "id", + "type": "text CHECK (\"id\" = upper(\"id\") AND length(\"id\") = 26 AND substr(\"id\", 1, 1) GLOB '[0-7]' AND \"id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "server_id": { + "name": "server_id", + "type": "text CHECK (\"server_id\" = upper(\"server_id\") AND length(\"server_id\") = 26 AND substr(\"server_id\", 1, 1) GLOB '[0-7]' AND \"server_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "agent_mcp_binding_agent_sort_idx": { + "name": "agent_mcp_binding_agent_sort_idx", + "columns": ["agent_id", "sort_order"], + "isUnique": true + }, + "agent_mcp_binding_server_idx": { + "name": "agent_mcp_binding_server_idx", + "columns": ["server_id"], + "isUnique": false + }, + "agent_mcp_binding_profile_server_idx": { + "name": "agent_mcp_binding_profile_server_idx", + "columns": ["agent_id", "server_id"], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": { + "agent_mcp_binding_agent_credential_shape_check": { + "name": "agent_mcp_binding_agent_credential_shape_check", + "value": "\n (\"agent_mcp_binding\".\"credential_mode\" = 'agent_bound' AND \"agent_mcp_binding\".\"agent_credential_id\" IS NOT NULL)\n OR (\"agent_mcp_binding\".\"credential_mode\" = 'runtime_resolved' AND \"agent_mcp_binding\".\"agent_credential_id\" IS NULL)\n " + } + } + }, + "agent_skill": { + "name": "agent_skill", + "columns": { + "agent_id": { + "name": "agent_id", + "type": "text CHECK (\"agent_id\" = upper(\"agent_id\") AND length(\"agent_id\") = 26 AND substr(\"agent_id\", 1, 1) GLOB '[0-7]' AND \"agent_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "skill_id": { + "name": "skill_id", + "type": "text CHECK (\"skill_id\" = upper(\"skill_id\") AND length(\"skill_id\") = 26 AND substr(\"skill_id\", 1, 1) GLOB '[0-7]' AND \"skill_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "agent_skill_agent_sort_idx": { + "name": "agent_skill_agent_sort_idx", + "columns": ["agent_id", "sort_order"], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "agent_skill_agent_id_skill_id_pk": { + "columns": ["agent_id", "skill_id"], + "name": "agent_skill_agent_id_skill_id_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "agent": { + "name": "agent", + "columns": { + "config_json": { + "name": "config_json", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "environment_id": { + "name": "environment_id", + "type": "text CHECK (\"environment_id\" = upper(\"environment_id\") AND length(\"environment_id\") = 26 AND substr(\"environment_id\", 1, 1) GLOB '[0-7]' AND \"environment_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "id": { + "name": "id", + "type": "text CHECK (\"id\" = upper(\"id\") AND length(\"id\") = 26 AND substr(\"id\", 1, 1) GLOB '[0-7]' AND \"id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'pet'" + }, + "live_deployment_version_id": { + "name": "live_deployment_version_id", + "type": "text CHECK (\"live_deployment_version_id\" = upper(\"live_deployment_version_id\") AND length(\"live_deployment_version_id\") = 26 AND substr(\"live_deployment_version_id\", 1, 1) GLOB '[0-7]' AND \"live_deployment_version_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "owner_account_id": { + "name": "owner_account_id", + "type": "text CHECK (\"owner_account_id\" = upper(\"owner_account_id\") AND length(\"owner_account_id\") = 26 AND substr(\"owner_account_id\", 1, 1) GLOB '[0-7]' AND \"owner_account_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "app_id": { + "name": "app_id", + "type": "text CHECK (\"app_id\" = upper(\"app_id\") AND length(\"app_id\") = 26 AND substr(\"app_id\", 1, 1) GLOB '[0-7]' AND \"app_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "prompt": { + "name": "prompt", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "runtime_id": { + "name": "runtime_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'draft'" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "visibility": { + "name": "visibility", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'private'" + } + }, + "indexes": { + "agent_app_owner_account_idx": { + "name": "agent_app_owner_account_idx", + "columns": ["app_id", "owner_account_id"], + "isUnique": false + }, + "agent_app_status_idx": { + "name": "agent_app_status_idx", + "columns": ["app_id", "status"], + "isUnique": false + }, + "agent_environment_idx": { + "name": "agent_environment_idx", + "columns": ["environment_id"], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": { + "agent_published_live_deployment_version_check": { + "name": "agent_published_live_deployment_version_check", + "value": "\"agent\".\"status\" <> 'published' OR \"agent\".\"live_deployment_version_id\" IS NOT NULL" + } + } + }, + "api_command": { + "name": "api_command", + "columns": { + "attempt_count": { + "name": "attempt_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "claim_expires_at": { + "name": "claim_expires_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "claim_owner": { + "name": "claim_owner", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "completed_at": { + "name": "completed_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "dedupe_key": { + "name": "dedupe_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "id": { + "name": "id", + "type": "text CHECK (\"id\" = upper(\"id\") AND length(\"id\") = 26 AND substr(\"id\", 1, 1) GLOB '[0-7]' AND \"id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "last_error_code": { + "name": "last_error_code", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_error_message": { + "name": "last_error_message", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "payload_json": { + "name": "payload_json", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "api_command_dedupe_idx": { + "name": "api_command_dedupe_idx", + "columns": ["dedupe_key"], + "isUnique": true + }, + "api_command_status_updated_idx": { + "name": "api_command_status_updated_idx", + "columns": ["status", "updated_at"], + "isUnique": false + }, + "api_command_claim_idx": { + "name": "api_command_claim_idx", + "columns": ["status", "claim_expires_at"], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "auth_account": { + "name": "auth_account", + "columns": { + "access_token": { + "name": "access_token", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "access_token_expires_at": { + "name": "access_token_expires_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "provider_account_id": { + "name": "provider_account_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "id_token": { + "name": "id_token", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "refresh_token": { + "name": "refresh_token", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "refresh_token_expires_at": { + "name": "refresh_token_expires_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "account_id": { + "name": "account_id", + "type": "text CHECK (\"account_id\" = upper(\"account_id\") AND length(\"account_id\") = 26 AND substr(\"account_id\", 1, 1) GLOB '[0-7]' AND \"account_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "auth_account_provider_account_idx": { + "name": "auth_account_provider_account_idx", + "columns": ["provider_id", "provider_account_id"], + "isUnique": true + }, + "auth_account_account_id_idx": { + "name": "auth_account_account_id_idx", + "columns": ["account_id"], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "auth_session": { + "name": "auth_session", + "columns": { + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "ip_address": { + "name": "ip_address", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "account_id": { + "name": "account_id", + "type": "text CHECK (\"account_id\" = upper(\"account_id\") AND length(\"account_id\") = 26 AND substr(\"account_id\", 1, 1) GLOB '[0-7]' AND \"account_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "auth_session_expires_at_idx": { + "name": "auth_session_expires_at_idx", + "columns": ["expires_at"], + "isUnique": false + }, + "auth_session_token_idx": { + "name": "auth_session_token_idx", + "columns": ["token"], + "isUnique": true + }, + "auth_session_account_id_idx": { + "name": "auth_session_account_id_idx", + "columns": ["account_id"], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "auth_verification": { + "name": "auth_verification", + "columns": { + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "identifier": { + "name": "identifier", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "auth_verification_expires_at_idx": { + "name": "auth_verification_expires_at_idx", + "columns": ["expires_at"], + "isUnique": false + }, + "auth_verification_identifier_idx": { + "name": "auth_verification_identifier_idx", + "columns": ["identifier"], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "cli_oauth_flow": { + "name": "cli_oauth_flow", + "columns": { + "account_id": { + "name": "account_id", + "type": "text CHECK (\"account_id\" = upper(\"account_id\") AND length(\"account_id\") = 26 AND substr(\"account_id\", 1, 1) GLOB '[0-7]' AND \"account_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "authorized_at": { + "name": "authorized_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "completed_at": { + "name": "completed_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "device_code_hash": { + "name": "device_code_hash", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "hostname": { + "name": "hostname", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "id": { + "name": "id", + "type": "text CHECK (\"id\" = upper(\"id\") AND length(\"id\") = 26 AND substr(\"id\", 1, 1) GLOB '[0-7]' AND \"id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_code": { + "name": "user_code", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "cli_oauth_flow_status_expires_idx": { + "name": "cli_oauth_flow_status_expires_idx", + "columns": ["status", "expires_at"], + "isUnique": false + }, + "cli_oauth_flow_device_code_hash_idx": { + "name": "cli_oauth_flow_device_code_hash_idx", + "columns": ["device_code_hash"], + "isUnique": true + }, + "cli_oauth_flow_user_code_idx": { + "name": "cli_oauth_flow_user_code_idx", + "columns": ["user_code"], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "personal_access_token": { + "name": "personal_access_token", + "columns": { + "account_id": { + "name": "account_id", + "type": "text CHECK (\"account_id\" = upper(\"account_id\") AND length(\"account_id\") = 26 AND substr(\"account_id\", 1, 1) GLOB '[0-7]' AND \"account_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "id": { + "name": "id", + "type": "text CHECK (\"id\" = upper(\"id\") AND length(\"id\") = 26 AND substr(\"id\", 1, 1) GLOB '[0-7]' AND \"id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "last_used_at": { + "name": "last_used_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "revoked_at": { + "name": "revoked_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "token_hash": { + "name": "token_hash", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "personal_access_token_account_created_idx": { + "name": "personal_access_token_account_created_idx", + "columns": ["account_id", "created_at"], + "isUnique": false + }, + "personal_access_token_hash_idx": { + "name": "personal_access_token_hash_idx", + "columns": ["token_hash"], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "email_log": { + "name": "email_log", + "columns": { + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "recipient_domain": { + "name": "recipient_domain", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "recipient_masked": { + "name": "recipient_masked", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "subject": { + "name": "subject", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "email_log_created_at_idx": { + "name": "email_log_created_at_idx", + "columns": ["created_at"], + "isUnique": false + }, + "email_log_type_status_idx": { + "name": "email_log_type_status_idx", + "columns": ["type", "status"], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "environment_revision": { + "name": "environment_revision", + "columns": { + "allow_mcp_servers": { + "name": "allow_mcp_servers", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "allow_package_managers": { + "name": "allow_package_managers", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "allowed_hosts_json": { + "name": "allowed_hosts_json", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_by_account_id": { + "name": "created_by_account_id", + "type": "text CHECK (\"created_by_account_id\" = upper(\"created_by_account_id\") AND length(\"created_by_account_id\") = 26 AND substr(\"created_by_account_id\", 1, 1) GLOB '[0-7]' AND \"created_by_account_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "env_vars_json": { + "name": "env_vars_json", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "environment_id": { + "name": "environment_id", + "type": "text CHECK (\"environment_id\" = upper(\"environment_id\") AND length(\"environment_id\") = 26 AND substr(\"environment_id\", 1, 1) GLOB '[0-7]' AND \"environment_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "id": { + "name": "id", + "type": "text CHECK (\"id\" = upper(\"id\") AND length(\"id\") = 26 AND substr(\"id\", 1, 1) GLOB '[0-7]' AND \"id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "network_policy": { + "name": "network_policy", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "packages_json": { + "name": "packages_json", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "app_id": { + "name": "app_id", + "type": "text CHECK (\"app_id\" = upper(\"app_id\") AND length(\"app_id\") = 26 AND substr(\"app_id\", 1, 1) GLOB '[0-7]' AND \"app_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "setup_script": { + "name": "setup_script", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "environment_revision_environment_created_at_idx": { + "name": "environment_revision_environment_created_at_idx", + "columns": ["environment_id", "created_at"], + "isUnique": false + }, + "environment_revision_app_created_at_idx": { + "name": "environment_revision_app_created_at_idx", + "columns": ["app_id", "created_at"], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": { + "environment_revision_network_policy_check": { + "name": "environment_revision_network_policy_check", + "value": "\"environment_revision\".\"network_policy\" IN ('full', 'limited')" + } + } + }, + "environment": { + "name": "environment", + "columns": { + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "current_revision_id": { + "name": "current_revision_id", + "type": "text CHECK (\"current_revision_id\" = upper(\"current_revision_id\") AND length(\"current_revision_id\") = 26 AND substr(\"current_revision_id\", 1, 1) GLOB '[0-7]' AND \"current_revision_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "forked_from_environment_id": { + "name": "forked_from_environment_id", + "type": "text CHECK (\"forked_from_environment_id\" = upper(\"forked_from_environment_id\") AND length(\"forked_from_environment_id\") = 26 AND substr(\"forked_from_environment_id\", 1, 1) GLOB '[0-7]' AND \"forked_from_environment_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "forked_from_environment_name": { + "name": "forked_from_environment_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "forked_from_owner_name": { + "name": "forked_from_owner_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "id": { + "name": "id", + "type": "text CHECK (\"id\" = upper(\"id\") AND length(\"id\") = 26 AND substr(\"id\", 1, 1) GLOB '[0-7]' AND \"id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "owner_account_id": { + "name": "owner_account_id", + "type": "text CHECK (\"owner_account_id\" = upper(\"owner_account_id\") AND length(\"owner_account_id\") = 26 AND substr(\"owner_account_id\", 1, 1) GLOB '[0-7]' AND \"owner_account_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "app_id": { + "name": "app_id", + "type": "text CHECK (\"app_id\" = upper(\"app_id\") AND length(\"app_id\") = 26 AND substr(\"app_id\", 1, 1) GLOB '[0-7]' AND \"app_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "environment_app_updated_at_idx": { + "name": "environment_app_updated_at_idx", + "columns": ["app_id", "updated_at"], + "isUnique": false + }, + "environment_owner_updated_at_idx": { + "name": "environment_owner_updated_at_idx", + "columns": ["owner_account_id", "updated_at"], + "isUnique": false + }, + "environment_owner_name_idx": { + "name": "environment_owner_name_idx", + "columns": ["app_id", "owner_account_id", "name"], + "isUnique": true, + "where": "\"environment\".\"owner_account_id\" IS NOT NULL" + }, + "environment_system_default_idx": { + "name": "environment_system_default_idx", + "columns": ["app_id"], + "isUnique": true, + "where": "\"environment\".\"owner_account_id\" IS NULL" + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "file_record": { + "name": "file_record", + "columns": { + "committed": { + "name": "committed", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_by_account_id": { + "name": "created_by_account_id", + "type": "text CHECK (\"created_by_account_id\" = upper(\"created_by_account_id\") AND length(\"created_by_account_id\") = 26 AND substr(\"created_by_account_id\", 1, 1) GLOB '[0-7]' AND \"created_by_account_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "etag": { + "name": "etag", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "id": { + "name": "id", + "type": "text CHECK (\"id\" = upper(\"id\") AND length(\"id\") = 26 AND substr(\"id\", 1, 1) GLOB '[0-7]' AND \"id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "mime_type": { + "name": "mime_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "object_key": { + "name": "object_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "owner_id": { + "name": "owner_id", + "type": "text CHECK (\"owner_id\" = upper(\"owner_id\") AND length(\"owner_id\") = 26 AND substr(\"owner_id\", 1, 1) GLOB '[0-7]' AND \"owner_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "owner_kind": { + "name": "owner_kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "parent_path": { + "name": "parent_path", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "path": { + "name": "path", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "purpose": { + "name": "purpose", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "runtime_event_seq": { + "name": "runtime_event_seq", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "scope_id": { + "name": "scope_id", + "type": "text CHECK (\"scope_id\" = upper(\"scope_id\") AND length(\"scope_id\") = 26 AND substr(\"scope_id\", 1, 1) GLOB '[0-7]' AND \"scope_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "scope_kind": { + "name": "scope_kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "session_kind": { + "name": "session_kind", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "size": { + "name": "size", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "version": { + "name": "version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "file_record_runtime_event_seq_idx": { + "name": "file_record_runtime_event_seq_idx", + "columns": ["scope_id", "runtime_event_seq"], + "isUnique": false + }, + "file_record_object_key_idx": { + "name": "file_record_object_key_idx", + "columns": ["object_key"], + "isUnique": true + }, + "file_record_unscoped_parent_path_name_status_idx": { + "name": "file_record_unscoped_parent_path_name_status_idx", + "columns": ["scope_kind", "parent_path", "name", "status"], + "isUnique": true, + "where": "\"file_record\".\"scope_id\" IS NULL" + }, + "file_record_scoped_parent_path_name_status_idx": { + "name": "file_record_scoped_parent_path_name_status_idx", + "columns": ["scope_kind", "scope_id", "parent_path", "name", "status"], + "isUnique": true + }, + "file_record_unscoped_pending_path_idx": { + "name": "file_record_unscoped_pending_path_idx", + "columns": ["scope_kind", "path"], + "isUnique": true, + "where": "\"file_record\".\"status\" = 'pending' AND \"file_record\".\"scope_id\" IS NULL" + }, + "file_record_scoped_pending_path_idx": { + "name": "file_record_scoped_pending_path_idx", + "columns": ["scope_kind", "scope_id", "path"], + "isUnique": true, + "where": "\"file_record\".\"status\" = 'pending' AND \"file_record\".\"scope_id\" IS NOT NULL" + }, + "file_record_unscoped_ready_path_idx": { + "name": "file_record_unscoped_ready_path_idx", + "columns": ["scope_kind", "path"], + "isUnique": true, + "where": "\"file_record\".\"status\" = 'ready' AND \"file_record\".\"scope_id\" IS NULL" + }, + "file_record_scoped_ready_path_idx": { + "name": "file_record_scoped_ready_path_idx", + "columns": ["scope_kind", "scope_id", "path"], + "isUnique": true, + "where": "\"file_record\".\"status\" = 'ready' AND \"file_record\".\"scope_id\" IS NOT NULL" + }, + "file_record_governance_idx": { + "name": "file_record_governance_idx", + "columns": ["purpose", "owner_kind", "owner_id", "status", "expires_at"], + "isUnique": false + }, + "file_record_listing_idx": { + "name": "file_record_listing_idx", + "columns": ["scope_kind", "scope_id", "parent_path", "status", "lower(\"name\")"], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": { + "file_record_runtime_event_seq_check": { + "name": "file_record_runtime_event_seq_check", + "value": "\"file_record\".\"runtime_event_seq\" IS NULL OR \"file_record\".\"runtime_event_seq\" >= 0" + } + } + }, + "file_upload": { + "name": "file_upload", + "columns": { + "content_type": { + "name": "content_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_by_account_id": { + "name": "created_by_account_id", + "type": "text CHECK (\"created_by_account_id\" = upper(\"created_by_account_id\") AND length(\"created_by_account_id\") = 26 AND substr(\"created_by_account_id\", 1, 1) GLOB '[0-7]' AND \"created_by_account_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expected_size": { + "name": "expected_size", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "file_id": { + "name": "file_id", + "type": "text CHECK (\"file_id\" = upper(\"file_id\") AND length(\"file_id\") = 26 AND substr(\"file_id\", 1, 1) GLOB '[0-7]' AND \"file_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "id": { + "name": "id", + "type": "text CHECK (\"id\" = upper(\"id\") AND length(\"id\") = 26 AND substr(\"id\", 1, 1) GLOB '[0-7]' AND \"id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "if_match_etag": { + "name": "if_match_etag", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "multipart_upload_id": { + "name": "multipart_upload_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "overwrite": { + "name": "overwrite", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "part_size": { + "name": "part_size", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "scope_id": { + "name": "scope_id", + "type": "text CHECK (\"scope_id\" = upper(\"scope_id\") AND length(\"scope_id\") = 26 AND substr(\"scope_id\", 1, 1) GLOB '[0-7]' AND \"scope_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "scope_kind": { + "name": "scope_kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "strategy": { + "name": "strategy", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "file_upload_file_id_idx": { + "name": "file_upload_file_id_idx", + "columns": ["file_id"], + "isUnique": true + }, + "file_upload_status_expires_idx": { + "name": "file_upload_status_expires_idx", + "columns": ["status", "expires_at"], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "file_version": { + "name": "file_version", + "columns": { + "committed": { + "name": "committed", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "committed_at": { + "name": "committed_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_by_account_id": { + "name": "created_by_account_id", + "type": "text CHECK (\"created_by_account_id\" = upper(\"created_by_account_id\") AND length(\"created_by_account_id\") = 26 AND substr(\"created_by_account_id\", 1, 1) GLOB '[0-7]' AND \"created_by_account_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "file_id": { + "name": "file_id", + "type": "text CHECK (\"file_id\" = upper(\"file_id\") AND length(\"file_id\") = 26 AND substr(\"file_id\", 1, 1) GLOB '[0-7]' AND \"file_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "id": { + "name": "id", + "type": "text CHECK (\"id\" = upper(\"id\") AND length(\"id\") = 26 AND substr(\"id\", 1, 1) GLOB '[0-7]' AND \"id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "mime_type": { + "name": "mime_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "object_key": { + "name": "object_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "path": { + "name": "path", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "reason": { + "name": "reason", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "scope_id": { + "name": "scope_id", + "type": "text CHECK (\"scope_id\" = upper(\"scope_id\") AND length(\"scope_id\") = 26 AND substr(\"scope_id\", 1, 1) GLOB '[0-7]' AND \"scope_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "scope_kind": { + "name": "scope_kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "size": { + "name": "size", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "source_etag": { + "name": "source_etag", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "source_object_key": { + "name": "source_object_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "version": { + "name": "version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "file_version_object_key_idx": { + "name": "file_version_object_key_idx", + "columns": ["object_key"], + "isUnique": true + }, + "file_version_scope_path_created_idx": { + "name": "file_version_scope_path_created_idx", + "columns": ["scope_kind", "scope_id", "path", "created_at"], + "isUnique": false + }, + "file_version_file_created_idx": { + "name": "file_version_file_created_idx", + "columns": ["file_id", "created_at"], + "isUnique": false + }, + "file_version_pending_idx": { + "name": "file_version_pending_idx", + "columns": ["committed", "created_at"], + "isUnique": false, + "where": "\"file_version\".\"committed\" = 0" + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "runtime_artifact_attempt": { + "name": "runtime_artifact_attempt", + "columns": { + "accepted_event_id": { + "name": "accepted_event_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_by_account_id": { + "name": "created_by_account_id", + "type": "text CHECK (\"created_by_account_id\" = upper(\"created_by_account_id\") AND length(\"created_by_account_id\") = 26 AND substr(\"created_by_account_id\", 1, 1) GLOB '[0-7]' AND \"created_by_account_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "delete_after": { + "name": "delete_after", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "driver_connection_id": { + "name": "driver_connection_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "driver_generation": { + "name": "driver_generation", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "driver_instance_id": { + "name": "driver_instance_id", + "type": "text CHECK (\"driver_instance_id\" = upper(\"driver_instance_id\") AND length(\"driver_instance_id\") = 26 AND substr(\"driver_instance_id\", 1, 1) GLOB '[0-7]' AND \"driver_instance_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "event_type": { + "name": "event_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "manifest_json": { + "name": "manifest_json", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "manifest_sha256": { + "name": "manifest_sha256", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "owned_object_keys_json": { + "name": "owned_object_keys_json", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'[]'" + }, + "run_id": { + "name": "run_id", + "type": "text CHECK (\"run_id\" = upper(\"run_id\") AND length(\"run_id\") = 26 AND substr(\"run_id\", 1, 1) GLOB '[0-7]' AND \"run_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "semantic_hash": { + "name": "semantic_hash", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "session_id": { + "name": "session_id", + "type": "text CHECK (\"session_id\" = upper(\"session_id\") AND length(\"session_id\") = 26 AND substr(\"session_id\", 1, 1) GLOB '[0-7]' AND \"session_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "source_event_id": { + "name": "source_event_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "runtime_artifact_attempt_accepted_event_idx": { + "name": "runtime_artifact_attempt_accepted_event_idx", + "columns": ["accepted_event_id"], + "isUnique": true, + "where": "\"runtime_artifact_attempt\".\"accepted_event_id\" IS NOT NULL" + }, + "runtime_artifact_attempt_cleanup_idx": { + "name": "runtime_artifact_attempt_cleanup_idx", + "columns": ["status", "expires_at", "updated_at", "id"], + "isUnique": false + }, + "runtime_artifact_attempt_session_status_idx": { + "name": "runtime_artifact_attempt_session_status_idx", + "columns": ["session_id", "status", "id"], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": { + "runtime_artifact_attempt_manifest_check": { + "name": "runtime_artifact_attempt_manifest_check", + "value": "(\"runtime_artifact_attempt\".\"manifest_json\" IS NULL AND \"runtime_artifact_attempt\".\"manifest_sha256\" IS NULL) OR (\"runtime_artifact_attempt\".\"manifest_json\" IS NOT NULL AND json_valid(\"runtime_artifact_attempt\".\"manifest_json\") = 1 AND json_extract(\"runtime_artifact_attempt\".\"manifest_json\", '$.version') IS 1 AND json_type(\"runtime_artifact_attempt\".\"manifest_json\", '$.captureStatus') IS 'text' AND json_extract(\"runtime_artifact_attempt\".\"manifest_json\", '$.captureStatus') IN ('complete', 'omitted_file_limit', 'omitted_runtime_unavailable', 'omitted_size_limit', 'omitted_source_changed', 'omitted_source_missing') AND json_type(\"runtime_artifact_attempt\".\"manifest_json\", '$.mode') IS 'text' AND json_extract(\"runtime_artifact_attempt\".\"manifest_json\", '$.mode') IN ('delta', 'snapshot') AND (json_extract(\"runtime_artifact_attempt\".\"manifest_json\", '$.captureStatus') = 'complete' OR json_array_length(\"runtime_artifact_attempt\".\"manifest_json\", '$.files') = 0) AND json_extract(\"runtime_artifact_attempt\".\"manifest_json\", '$.sourceEventId') IS \"runtime_artifact_attempt\".\"source_event_id\" AND json_extract(\"runtime_artifact_attempt\".\"manifest_json\", '$.semanticHash') IS \"runtime_artifact_attempt\".\"semantic_hash\" AND json_type(\"runtime_artifact_attempt\".\"manifest_json\", '$.files') IS 'array' AND \"runtime_artifact_attempt\".\"manifest_sha256\" IS NOT NULL AND length(\"runtime_artifact_attempt\".\"manifest_sha256\") = 64 AND \"runtime_artifact_attempt\".\"manifest_sha256\" = lower(\"runtime_artifact_attempt\".\"manifest_sha256\") AND \"runtime_artifact_attempt\".\"manifest_sha256\" NOT GLOB '*[^0-9a-f]*')" + }, + "runtime_artifact_attempt_owned_keys_check": { + "name": "runtime_artifact_attempt_owned_keys_check", + "value": "json_valid(\"runtime_artifact_attempt\".\"owned_object_keys_json\") = 1 AND json_type(\"runtime_artifact_attempt\".\"owned_object_keys_json\") IS 'array'" + }, + "runtime_artifact_attempt_semantic_hash_check": { + "name": "runtime_artifact_attempt_semantic_hash_check", + "value": "length(\"runtime_artifact_attempt\".\"semantic_hash\") = 64 AND \"runtime_artifact_attempt\".\"semantic_hash\" = lower(\"runtime_artifact_attempt\".\"semantic_hash\") AND \"runtime_artifact_attempt\".\"semantic_hash\" NOT GLOB '*[^0-9a-f]*'" + }, + "runtime_artifact_attempt_status_check": { + "name": "runtime_artifact_attempt_status_check", + "value": "(\"runtime_artifact_attempt\".\"status\" = 'staging' AND \"runtime_artifact_attempt\".\"manifest_json\" IS NULL AND \"runtime_artifact_attempt\".\"accepted_event_id\" IS NULL AND \"runtime_artifact_attempt\".\"expires_at\" IS NOT NULL AND \"runtime_artifact_attempt\".\"delete_after\" IS NULL) OR (\"runtime_artifact_attempt\".\"status\" = 'staged' AND \"runtime_artifact_attempt\".\"manifest_json\" IS NOT NULL AND \"runtime_artifact_attempt\".\"accepted_event_id\" IS NULL AND \"runtime_artifact_attempt\".\"expires_at\" IS NOT NULL AND \"runtime_artifact_attempt\".\"delete_after\" IS NULL) OR (\"runtime_artifact_attempt\".\"status\" = 'accepted' AND \"runtime_artifact_attempt\".\"manifest_json\" IS NOT NULL AND \"runtime_artifact_attempt\".\"accepted_event_id\" IS NOT NULL AND \"runtime_artifact_attempt\".\"expires_at\" IS NULL AND \"runtime_artifact_attempt\".\"delete_after\" IS NULL AND json_array_length(\"runtime_artifact_attempt\".\"owned_object_keys_json\") = 0) OR (\"runtime_artifact_attempt\".\"status\" = 'deleting' AND \"runtime_artifact_attempt\".\"accepted_event_id\" IS NULL AND \"runtime_artifact_attempt\".\"delete_after\" IS NOT NULL)" + }, + "runtime_artifact_attempt_time_check": { + "name": "runtime_artifact_attempt_time_check", + "value": "\"runtime_artifact_attempt\".\"driver_generation\" >= 0 AND (\"runtime_artifact_attempt\".\"expires_at\" IS NULL OR \"runtime_artifact_attempt\".\"expires_at\" >= \"runtime_artifact_attempt\".\"created_at\") AND (\"runtime_artifact_attempt\".\"delete_after\" IS NULL OR \"runtime_artifact_attempt\".\"delete_after\" >= \"runtime_artifact_attempt\".\"created_at\") AND \"runtime_artifact_attempt\".\"updated_at\" >= \"runtime_artifact_attempt\".\"created_at\"" + } + } + }, + "session_artifact_head": { + "name": "session_artifact_head", + "columns": { + "file_id": { + "name": "file_id", + "type": "text CHECK (\"file_id\" = upper(\"file_id\") AND length(\"file_id\") = 26 AND substr(\"file_id\", 1, 1) GLOB '[0-7]' AND \"file_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "runtime_event_seq": { + "name": "runtime_event_seq", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "session_id": { + "name": "session_id", + "type": "text CHECK (\"session_id\" = upper(\"session_id\") AND length(\"session_id\") = 26 AND substr(\"session_id\", 1, 1) GLOB '[0-7]' AND \"session_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "source_event_id": { + "name": "source_event_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "source_path": { + "name": "source_path", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "session_artifact_head_session_path_idx": { + "name": "session_artifact_head_session_path_idx", + "columns": ["session_id", "source_path"], + "isUnique": true + }, + "session_artifact_head_session_seq_idx": { + "name": "session_artifact_head_session_seq_idx", + "columns": ["session_id", "runtime_event_seq", "source_path"], + "isUnique": false + } + }, + "foreignKeys": { + "session_artifact_head_session_id_session_id_fk": { + "name": "session_artifact_head_session_id_session_id_fk", + "tableFrom": "session_artifact_head", + "tableTo": "session", + "columnsFrom": ["session_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": { + "session_artifact_head_path_check": { + "name": "session_artifact_head_path_check", + "value": "length(\"session_artifact_head\".\"source_path\") > 8 AND substr(\"session_artifact_head\".\"source_path\", 1, 8) = 'outputs/' AND instr(\"session_artifact_head\".\"source_path\", char(0)) = 0 AND instr(\"session_artifact_head\".\"source_path\", '\\') = 0 AND \"session_artifact_head\".\"source_path\" NOT LIKE '%//%' AND \"session_artifact_head\".\"source_path\" NOT LIKE '%/./%' AND \"session_artifact_head\".\"source_path\" NOT LIKE '%/.' AND \"session_artifact_head\".\"source_path\" NOT LIKE '%/../%' AND \"session_artifact_head\".\"source_path\" NOT LIKE '%/..'" + }, + "session_artifact_head_seq_check": { + "name": "session_artifact_head_seq_check", + "value": "\"session_artifact_head\".\"runtime_event_seq\" >= 0 AND \"session_artifact_head\".\"updated_at\" >= 0" + } + } + }, + "mcp_credential": { + "name": "mcp_credential", + "columns": { + "account_id": { + "name": "account_id", + "type": "text CHECK (\"account_id\" = upper(\"account_id\") AND length(\"account_id\") = 26 AND substr(\"account_id\", 1, 1) GLOB '[0-7]' AND \"account_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "agent_id": { + "name": "agent_id", + "type": "text CHECK (\"agent_id\" = upper(\"agent_id\") AND length(\"agent_id\") = 26 AND substr(\"agent_id\", 1, 1) GLOB '[0-7]' AND \"agent_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "auth_type": { + "name": "auth_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "id": { + "name": "id", + "type": "text CHECK (\"id\" = upper(\"id\") AND length(\"id\") = 26 AND substr(\"id\", 1, 1) GLOB '[0-7]' AND \"id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "last_refreshed_at": { + "name": "last_refreshed_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "oauth_client_id": { + "name": "oauth_client_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "oauth_client_secret_secret_id": { + "name": "oauth_client_secret_secret_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "app_id": { + "name": "app_id", + "type": "text CHECK (\"app_id\" = upper(\"app_id\") AND length(\"app_id\") = 26 AND substr(\"app_id\", 1, 1) GLOB '[0-7]' AND \"app_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "refresh_secret_id": { + "name": "refresh_secret_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "scope_values_json": { + "name": "scope_values_json", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "secret_id": { + "name": "secret_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "server_id": { + "name": "server_id", + "type": "text CHECK (\"server_id\" = upper(\"server_id\") AND length(\"server_id\") = 26 AND substr(\"server_id\", 1, 1) GLOB '[0-7]' AND \"server_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "subject_label": { + "name": "subject_label", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "mcp_credential_server_scope_status_idx": { + "name": "mcp_credential_server_scope_status_idx", + "columns": ["server_id", "scope", "status"], + "isUnique": false + }, + "mcp_credential_app_scope_status_idx": { + "name": "mcp_credential_app_scope_status_idx", + "columns": ["app_id", "scope", "status"], + "isUnique": false + }, + "mcp_credential_app_scope_idx": { + "name": "mcp_credential_app_scope_idx", + "columns": ["server_id", "scope"], + "isUnique": true, + "where": "\"mcp_credential\".\"scope\" = 'app'" + }, + "mcp_credential_agent_scope_idx": { + "name": "mcp_credential_agent_scope_idx", + "columns": ["server_id", "agent_id", "scope"], + "isUnique": true, + "where": "\"mcp_credential\".\"scope\" = 'agent' AND \"mcp_credential\".\"agent_id\" IS NOT NULL" + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": { + "mcp_credential_scope_shape_check": { + "name": "mcp_credential_scope_shape_check", + "value": "\n (\"mcp_credential\".\"scope\" = 'app' AND \"mcp_credential\".\"account_id\" IS NULL AND \"mcp_credential\".\"agent_id\" IS NULL)\n OR (\"mcp_credential\".\"scope\" = 'agent' AND \"mcp_credential\".\"account_id\" IS NULL AND \"mcp_credential\".\"agent_id\" IS NOT NULL)\n " + }, + "mcp_credential_scope_values_json_check": { + "name": "mcp_credential_scope_values_json_check", + "value": "\n \"mcp_credential\".\"scope_values_json\" IS NULL\n OR (json_valid(\"mcp_credential\".\"scope_values_json\") AND json_type(\"mcp_credential\".\"scope_values_json\") = 'array')\n " + }, + "mcp_credential_bearer_shape_check": { + "name": "mcp_credential_bearer_shape_check", + "value": "\n \"mcp_credential\".\"auth_type\" != 'bearer'\n OR (\n \"mcp_credential\".\"oauth_client_id\" IS NULL\n AND \"mcp_credential\".\"oauth_client_secret_secret_id\" IS NULL\n AND \"mcp_credential\".\"refresh_secret_id\" IS NULL\n )\n " + } + } + }, + "mcp_oauth_flow": { + "name": "mcp_oauth_flow", + "columns": { + "authorization_endpoint": { + "name": "authorization_endpoint", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "cleanup_after": { + "name": "cleanup_after", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "code_verifier": { + "name": "code_verifier", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "completed_at": { + "name": "completed_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "id": { + "name": "id", + "type": "text CHECK (\"id\" = upper(\"id\") AND length(\"id\") = 26 AND substr(\"id\", 1, 1) GLOB '[0-7]' AND \"id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "initiator_account_id": { + "name": "initiator_account_id", + "type": "text CHECK (\"initiator_account_id\" = upper(\"initiator_account_id\") AND length(\"initiator_account_id\") = 26 AND substr(\"initiator_account_id\", 1, 1) GLOB '[0-7]' AND \"initiator_account_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "oauth_client_id": { + "name": "oauth_client_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "oauth_client_secret_secret_id": { + "name": "oauth_client_secret_secret_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "app_id": { + "name": "app_id", + "type": "text CHECK (\"app_id\" = upper(\"app_id\") AND length(\"app_id\") = 26 AND substr(\"app_id\", 1, 1) GLOB '[0-7]' AND \"app_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "registration_endpoint": { + "name": "registration_endpoint", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "return_url": { + "name": "return_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "scope_values_json": { + "name": "scope_values_json", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "server_id": { + "name": "server_id", + "type": "text CHECK (\"server_id\" = upper(\"server_id\") AND length(\"server_id\") = 26 AND substr(\"server_id\", 1, 1) GLOB '[0-7]' AND \"server_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "subject_label": { + "name": "subject_label", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "token_endpoint": { + "name": "token_endpoint", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "mcp_oauth_flow_status_cleanup_after_idx": { + "name": "mcp_oauth_flow_status_cleanup_after_idx", + "columns": ["status", "cleanup_after"], + "isUnique": false + }, + "mcp_oauth_flow_expires_at_idx": { + "name": "mcp_oauth_flow_expires_at_idx", + "columns": ["expires_at"], + "isUnique": false + }, + "mcp_oauth_flow_server_account_idx": { + "name": "mcp_oauth_flow_server_account_idx", + "columns": ["server_id", "initiator_account_id"], + "isUnique": false + }, + "mcp_oauth_flow_app_server_account_idx": { + "name": "mcp_oauth_flow_app_server_account_idx", + "columns": ["app_id", "server_id", "initiator_account_id"], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": { + "mcp_oauth_flow_scope_values_json_check": { + "name": "mcp_oauth_flow_scope_values_json_check", + "value": "\n \"mcp_oauth_flow\".\"scope_values_json\" IS NULL\n OR (json_valid(\"mcp_oauth_flow\".\"scope_values_json\") AND json_type(\"mcp_oauth_flow\".\"scope_values_json\") = 'array')\n " + } + } + }, + "mcp_server": { + "name": "mcp_server", + "columns": { + "auth_type": { + "name": "auth_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "byo_client_id": { + "name": "byo_client_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "byo_client_secret_secret_id": { + "name": "byo_client_secret_secret_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "credential_scope": { + "name": "credential_scope", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "enabled": { + "name": "enabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "icon_url": { + "name": "icon_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "id": { + "name": "id", + "type": "text CHECK (\"id\" = upper(\"id\") AND length(\"id\") = 26 AND substr(\"id\", 1, 1) GLOB '[0-7]' AND \"id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "oauth_metadata_json": { + "name": "oauth_metadata_json", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "owner_account_id": { + "name": "owner_account_id", + "type": "text CHECK (\"owner_account_id\" = upper(\"owner_account_id\") AND length(\"owner_account_id\") = 26 AND substr(\"owner_account_id\", 1, 1) GLOB '[0-7]' AND \"owner_account_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "app_id": { + "name": "app_id", + "type": "text CHECK (\"app_id\" = upper(\"app_id\") AND length(\"app_id\") = 26 AND substr(\"app_id\", 1, 1) GLOB '[0-7]' AND \"app_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "mcp_server_app_enabled_idx": { + "name": "mcp_server_app_enabled_idx", + "columns": ["app_id", "enabled"], + "isUnique": false + }, + "mcp_server_owner_app_idx": { + "name": "mcp_server_owner_app_idx", + "columns": ["owner_account_id", "app_id"], + "isUnique": false + }, + "mcp_server_app_url_idx": { + "name": "mcp_server_app_url_idx", + "columns": ["app_id", "url"], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": { + "mcp_server_source_scope_check": { + "name": "mcp_server_source_scope_check", + "value": "\"mcp_server\".\"source\" = 'app' AND \"mcp_server\".\"credential_scope\" = 'app'" + } + } + }, + "vault_secret": { + "name": "vault_secret", + "columns": { + "algorithm": { + "name": "algorithm", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'AES-GCM'" + }, + "ciphertext": { + "name": "ciphertext", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "ciphertext_iv": { + "name": "ciphertext_iv", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "id": { + "name": "id", + "type": "text CHECK (\"id\" = upper(\"id\") AND length(\"id\") = 26 AND substr(\"id\", 1, 1) GLOB '[0-7]' AND \"id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "wrapped_dek": { + "name": "wrapped_dek", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "wrapped_dek_iv": { + "name": "wrapped_dek_iv", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "vault_secret_kind_created_at_idx": { + "name": "vault_secret_kind_created_at_idx", + "columns": ["kind", "created_at"], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "organization": { + "name": "organization", + "columns": { + "avatar_url": { + "name": "avatar_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "creator_account_id": { + "name": "creator_account_id", + "type": "text CHECK (\"creator_account_id\" = upper(\"creator_account_id\") AND length(\"creator_account_id\") = 26 AND substr(\"creator_account_id\", 1, 1) GLOB '[0-7]' AND \"creator_account_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "id": { + "name": "id", + "type": "text CHECK (\"id\" = upper(\"id\") AND length(\"id\") = 26 AND substr(\"id\", 1, 1) GLOB '[0-7]' AND \"id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "organization_creator_account_idx": { + "name": "organization_creator_account_idx", + "columns": ["creator_account_id"], + "isUnique": true, + "where": "\"organization\".\"creator_account_id\" IS NOT NULL" + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "app_deployment_run": { + "name": "app_deployment_run", + "columns": { + "app_id": { + "name": "app_id", + "type": "text CHECK (\"app_id\" = upper(\"app_id\") AND length(\"app_id\") = 26 AND substr(\"app_id\", 1, 1) GLOB '[0-7]' AND \"app_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "deployment_id": { + "name": "deployment_id", + "type": "text CHECK (\"deployment_id\" = upper(\"deployment_id\") AND length(\"deployment_id\") = 26 AND substr(\"deployment_id\", 1, 1) GLOB '[0-7]' AND \"deployment_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "error_code": { + "name": "error_code", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "external_deployment_id": { + "name": "external_deployment_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "external_project_id": { + "name": "external_project_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "external_version_id": { + "name": "external_version_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "generated_wrangler_config_json": { + "name": "generated_wrangler_config_json", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "id": { + "name": "id", + "type": "text CHECK (\"id\" = upper(\"id\") AND length(\"id\") = 26 AND substr(\"id\", 1, 1) GLOB '[0-7]' AND \"id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "mosoo_config_json": { + "name": "mosoo_config_json", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "plan_json": { + "name": "plan_json", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "source_branch": { + "name": "source_branch", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "source_commit_sha": { + "name": "source_commit_sha", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "target_kind": { + "name": "target_kind", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "target_project_name": { + "name": "target_project_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "target_script_name": { + "name": "target_script_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "app_deployment_run_app_id_idx": { + "name": "app_deployment_run_app_id_idx", + "columns": ["app_id", "id"], + "isUnique": false + }, + "app_deployment_run_deployment_id_idx": { + "name": "app_deployment_run_deployment_id_idx", + "columns": ["deployment_id", "id"], + "isUnique": false + }, + "app_deployment_run_active_app_idx": { + "name": "app_deployment_run_active_app_idx", + "columns": ["app_id"], + "isUnique": true, + "where": "\"app_deployment_run\".\"status\" IN ('queued', 'preparing', 'building', 'submitting', 'submitted', 'activating')" + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": { + "app_deployment_run_status_check": { + "name": "app_deployment_run_status_check", + "value": "\"app_deployment_run\".\"status\" IN ('queued', 'preparing', 'building', 'submitting', 'submitted', 'activating', 'success', 'failed')" + }, + "app_deployment_run_target_kind_check": { + "name": "app_deployment_run_target_kind_check", + "value": "\"app_deployment_run\".\"target_kind\" IS NULL OR \"app_deployment_run\".\"target_kind\" IN ('cloudflare_pages', 'cloudflare_worker')" + } + } + }, + "app_deployment_secret": { + "name": "app_deployment_secret", + "columns": { + "app_id": { + "name": "app_id", + "type": "text CHECK (\"app_id\" = upper(\"app_id\") AND length(\"app_id\") = 26 AND substr(\"app_id\", 1, 1) GLOB '[0-7]' AND \"app_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "vault_secret_id": { + "name": "vault_secret_id", + "type": "text CHECK (\"vault_secret_id\" = upper(\"vault_secret_id\") AND length(\"vault_secret_id\") = 26 AND substr(\"vault_secret_id\", 1, 1) GLOB '[0-7]' AND \"vault_secret_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "app_deployment_secret_app_name_idx": { + "name": "app_deployment_secret_app_name_idx", + "columns": ["app_id", "name"], + "isUnique": true + }, + "app_deployment_secret_vault_secret_idx": { + "name": "app_deployment_secret_vault_secret_idx", + "columns": ["vault_secret_id"], + "isUnique": true + } + }, + "foreignKeys": { + "app_deployment_secret_vault_secret_id_vault_secret_id_fk": { + "name": "app_deployment_secret_vault_secret_id_vault_secret_id_fk", + "tableFrom": "app_deployment_secret", + "tableTo": "vault_secret", + "columnsFrom": ["vault_secret_id"], + "columnsTo": ["id"], + "onDelete": "restrict", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "app_deployment": { + "name": "app_deployment", + "columns": { + "app_id": { + "name": "app_id", + "type": "text CHECK (\"app_id\" = upper(\"app_id\") AND length(\"app_id\") = 26 AND substr(\"app_id\", 1, 1) GLOB '[0-7]' AND \"app_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "default_branch": { + "name": "default_branch", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "deleted_at": { + "name": "deleted_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "id": { + "name": "id", + "type": "text CHECK (\"id\" = upper(\"id\") AND length(\"id\") = 26 AND substr(\"id\", 1, 1) GLOB '[0-7]' AND \"id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "last_successful_url": { + "name": "last_successful_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "latest_run_id": { + "name": "latest_run_id", + "type": "text CHECK (\"latest_run_id\" = upper(\"latest_run_id\") AND length(\"latest_run_id\") = 26 AND substr(\"latest_run_id\", 1, 1) GLOB '[0-7]' AND \"latest_run_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "mosoo_subdomain": { + "name": "mosoo_subdomain", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "owner_account_id": { + "name": "owner_account_id", + "type": "text CHECK (\"owner_account_id\" = upper(\"owner_account_id\") AND length(\"owner_account_id\") = 26 AND substr(\"owner_account_id\", 1, 1) GLOB '[0-7]' AND \"owner_account_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "repo_name": { + "name": "repo_name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "repo_owner": { + "name": "repo_owner", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "repo_url": { + "name": "repo_url", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "source_kind": { + "name": "source_kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "app_deployment_active_app_idx": { + "name": "app_deployment_active_app_idx", + "columns": ["app_id"], + "isUnique": true, + "where": "\"app_deployment\".\"deleted_at\" IS NULL" + }, + "app_deployment_active_subdomain_idx": { + "name": "app_deployment_active_subdomain_idx", + "columns": ["mosoo_subdomain"], + "isUnique": true, + "where": "\"app_deployment\".\"deleted_at\" IS NULL" + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": { + "app_deployment_source_kind_check": { + "name": "app_deployment_source_kind_check", + "value": "\"app_deployment\".\"source_kind\" IN ('github_public')" + } + } + }, + "app": { + "name": "app", + "columns": { + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "default_environment_id": { + "name": "default_environment_id", + "type": "text CHECK (\"default_environment_id\" = upper(\"default_environment_id\") AND length(\"default_environment_id\") = 26 AND substr(\"default_environment_id\", 1, 1) GLOB '[0-7]' AND \"default_environment_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "id": { + "name": "id", + "type": "text CHECK (\"id\" = upper(\"id\") AND length(\"id\") = 26 AND substr(\"id\", 1, 1) GLOB '[0-7]' AND \"id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "organization_id": { + "name": "organization_id", + "type": "text CHECK (\"organization_id\" = upper(\"organization_id\") AND length(\"organization_id\") = 26 AND substr(\"organization_id\", 1, 1) GLOB '[0-7]' AND \"organization_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "owner_account_id": { + "name": "owner_account_id", + "type": "text CHECK (\"owner_account_id\" = upper(\"owner_account_id\") AND length(\"owner_account_id\") = 26 AND substr(\"owner_account_id\", 1, 1) GLOB '[0-7]' AND \"owner_account_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "bound_agent_call_idempotency_key": { + "name": "bound_agent_call_idempotency_key", + "columns": { + "body_hash": { + "name": "body_hash", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "id": { + "name": "id", + "type": "text CHECK (\"id\" = upper(\"id\") AND length(\"id\") = 26 AND substr(\"id\", 1, 1) GLOB '[0-7]' AND \"id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "idempotency_key": { + "name": "idempotency_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "run_id": { + "name": "run_id", + "type": "text CHECK (\"run_id\" = upper(\"run_id\") AND length(\"run_id\") = 26 AND substr(\"run_id\", 1, 1) GLOB '[0-7]' AND \"run_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "session_id": { + "name": "session_id", + "type": "text CHECK (\"session_id\" = upper(\"session_id\") AND length(\"session_id\") = 26 AND substr(\"session_id\", 1, 1) GLOB '[0-7]' AND \"session_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "subject_hash": { + "name": "subject_hash", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "bound_agent_call_idempotency_subject_key_idx": { + "name": "bound_agent_call_idempotency_subject_key_idx", + "columns": ["subject_hash", "idempotency_key"], + "isUnique": true + }, + "bound_agent_call_idempotency_updated_idx": { + "name": "bound_agent_call_idempotency_updated_idx", + "columns": ["updated_at"], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "public_api_idempotency_key": { + "name": "public_api_idempotency_key", + "columns": { + "body_hash": { + "name": "body_hash", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "id": { + "name": "id", + "type": "text CHECK (\"id\" = upper(\"id\") AND length(\"id\") = 26 AND substr(\"id\", 1, 1) GLOB '[0-7]' AND \"id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "idempotency_key": { + "name": "idempotency_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "method": { + "name": "method", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "response_json": { + "name": "response_json", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "response_status": { + "name": "response_status", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "route": { + "name": "route", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "token_id": { + "name": "token_id", + "type": "text CHECK (\"token_id\" = upper(\"token_id\") AND length(\"token_id\") = 26 AND substr(\"token_id\", 1, 1) GLOB '[0-7]' AND \"token_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "public_api_idempotency_token_key_idx": { + "name": "public_api_idempotency_token_key_idx", + "columns": ["token_id", "idempotency_key"], + "isUnique": true + }, + "public_api_idempotency_updated_idx": { + "name": "public_api_idempotency_updated_idx", + "columns": ["updated_at"], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "public_api_rate_limit_window": { + "name": "public_api_rate_limit_window", + "columns": { + "bucket_key": { + "name": "bucket_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "request_count": { + "name": "request_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "shard": { + "name": "shard", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "window_start": { + "name": "window_start", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "public_api_rate_limit_window_updated_idx": { + "name": "public_api_rate_limit_window_updated_idx", + "columns": ["updated_at"], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "public_api_rate_limit_window_bucket_key_window_start_shard_pk": { + "columns": ["bucket_key", "window_start", "shard"], + "name": "public_api_rate_limit_window_bucket_key_window_start_shard_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "driver_command": { + "name": "driver_command", + "columns": { + "acked_at": { + "name": "acked_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "completed_at": { + "name": "completed_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "delivery_connection_id": { + "name": "delivery_connection_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "driver_generation": { + "name": "driver_generation", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "driver_instance_id": { + "name": "driver_instance_id", + "type": "text CHECK (\"driver_instance_id\" = upper(\"driver_instance_id\") AND length(\"driver_instance_id\") = 26 AND substr(\"driver_instance_id\", 1, 1) GLOB '[0-7]' AND \"driver_instance_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "error_json": { + "name": "error_json", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "id": { + "name": "id", + "type": "text CHECK (\"id\" = upper(\"id\") AND length(\"id\") = 26 AND substr(\"id\", 1, 1) GLOB '[0-7]' AND \"id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "issued_at": { + "name": "issued_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "payload_json": { + "name": "payload_json", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "result_json": { + "name": "result_json", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "seq": { + "name": "seq", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "driver_command_instance_seq_idx": { + "name": "driver_command_instance_seq_idx", + "columns": ["driver_instance_id", "seq"], + "isUnique": true + }, + "driver_command_instance_status_idx": { + "name": "driver_command_instance_status_idx", + "columns": ["driver_instance_id", "status", "expires_at"], + "isUnique": false + } + }, + "foreignKeys": { + "driver_command_driver_instance_id_driver_instance_id_fk": { + "name": "driver_command_driver_instance_id_driver_instance_id_fk", + "tableFrom": "driver_command", + "tableTo": "driver_instance", + "columnsFrom": ["driver_instance_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": { + "driver_command_generation_check": { + "name": "driver_command_generation_check", + "value": "\"driver_command\".\"driver_generation\" IS NULL OR (typeof(\"driver_command\".\"driver_generation\") = 'integer' AND \"driver_command\".\"driver_generation\" BETWEEN 0 AND 9007199254740991)" + }, + "driver_command_nonterminal_generation_check": { + "name": "driver_command_nonterminal_generation_check", + "value": "\"driver_command\".\"status\" IN ('completed', 'failed', 'expired', 'cancelled') OR \"driver_command\".\"driver_generation\" IS NOT NULL" + } + } + }, + "driver_instance_mcp_grant": { + "name": "driver_instance_mcp_grant", + "columns": { + "auth_type": { + "name": "auth_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "authorization_state": { + "name": "authorization_state", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "can_invalidate": { + "name": "can_invalidate", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "can_refresh": { + "name": "can_refresh", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "credential_id": { + "name": "credential_id", + "type": "text CHECK (\"credential_id\" = upper(\"credential_id\") AND length(\"credential_id\") = 26 AND substr(\"credential_id\", 1, 1) GLOB '[0-7]' AND \"credential_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "driver_instance_id": { + "name": "driver_instance_id", + "type": "text CHECK (\"driver_instance_id\" = upper(\"driver_instance_id\") AND length(\"driver_instance_id\") = 26 AND substr(\"driver_instance_id\", 1, 1) GLOB '[0-7]' AND \"driver_instance_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "app_id": { + "name": "app_id", + "type": "text CHECK (\"app_id\" = upper(\"app_id\") AND length(\"app_id\") = 26 AND substr(\"app_id\", 1, 1) GLOB '[0-7]' AND \"app_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "server_id": { + "name": "server_id", + "type": "text CHECK (\"server_id\" = upper(\"server_id\") AND length(\"server_id\") = 26 AND substr(\"server_id\", 1, 1) GLOB '[0-7]' AND \"server_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "driver_instance_mcp_grant_instance_server_idx": { + "name": "driver_instance_mcp_grant_instance_server_idx", + "columns": ["driver_instance_id", "server_id"], + "isUnique": true + }, + "driver_instance_mcp_grant_instance_credential_idx": { + "name": "driver_instance_mcp_grant_instance_credential_idx", + "columns": ["driver_instance_id", "credential_id"], + "isUnique": false + } + }, + "foreignKeys": { + "driver_instance_mcp_grant_driver_instance_id_driver_instance_id_fk": { + "name": "driver_instance_mcp_grant_driver_instance_id_driver_instance_id_fk", + "tableFrom": "driver_instance_mcp_grant", + "tableTo": "driver_instance", + "columnsFrom": ["driver_instance_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "driver_instance": { + "name": "driver_instance", + "columns": { + "boot_token_expires_at": { + "name": "boot_token_expires_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "boot_token_hash": { + "name": "boot_token_hash", + "type": "blob", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "boot_token_used_at": { + "name": "boot_token_used_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "close_code": { + "name": "close_code", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "close_reason": { + "name": "close_reason", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "connection_id": { + "name": "connection_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "command_seq_cursor": { + "name": "command_seq_cursor", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "driver_pid": { + "name": "driver_pid", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "driver_started_at": { + "name": "driver_started_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "driver_version": { + "name": "driver_version", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "heartbeat_count": { + "name": "heartbeat_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "generation": { + "name": "generation", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "id": { + "name": "id", + "type": "text CHECK (\"id\" = upper(\"id\") AND length(\"id\") = 26 AND substr(\"id\", 1, 1) GLOB '[0-7]' AND \"id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "last_heartbeat_at": { + "name": "last_heartbeat_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "process_id": { + "name": "process_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "protocol": { + "name": "protocol", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "protocol_version": { + "name": "protocol_version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "restart_count": { + "name": "restart_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "runtime": { + "name": "runtime", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "sandbox_id": { + "name": "sandbox_id", + "type": "text CHECK (\"sandbox_id\" = upper(\"sandbox_id\") AND length(\"sandbox_id\") = 26 AND substr(\"sandbox_id\", 1, 1) GLOB '[0-7]' AND \"sandbox_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "sandbox_session_id": { + "name": "sandbox_session_id", + "type": "text CHECK (\"sandbox_session_id\" = upper(\"sandbox_session_id\") AND length(\"sandbox_session_id\") = 26 AND substr(\"sandbox_session_id\", 1, 1) GLOB '[0-7]' AND \"sandbox_session_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status_changed_at": { + "name": "status_changed_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "status_event": { + "name": "status_event", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'driver.provision'" + }, + "status_operation_id": { + "name": "status_operation_id", + "type": "text CHECK (\"status_operation_id\" = upper(\"status_operation_id\") AND length(\"status_operation_id\") = 26 AND substr(\"status_operation_id\", 1, 1) GLOB '[0-7]' AND \"status_operation_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status_seq": { + "name": "status_seq", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "status_source": { + "name": "status_source", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'system'" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "driver_instance_completed_idx": { + "name": "driver_instance_completed_idx", + "columns": ["expires_at", "status"], + "isUnique": false + }, + "driver_instance_connection_idx": { + "name": "driver_instance_connection_idx", + "columns": ["connection_id"], + "isUnique": true, + "where": "\"driver_instance\".\"connection_id\" IS NOT NULL" + }, + "driver_instance_boot_token_expiry_idx": { + "name": "driver_instance_boot_token_expiry_idx", + "columns": ["status", "boot_token_expires_at"], + "isUnique": false, + "where": "\"driver_instance\".\"boot_token_used_at\" IS NULL" + }, + "driver_instance_boot_token_hash_idx": { + "name": "driver_instance_boot_token_hash_idx", + "columns": ["boot_token_hash"], + "isUnique": true + }, + "driver_instance_sandbox_session_idx": { + "name": "driver_instance_sandbox_session_idx", + "columns": ["sandbox_id", "sandbox_session_id", "status", "updated_at"], + "isUnique": false + }, + "driver_instance_live_sandbox_session_idx": { + "name": "driver_instance_live_sandbox_session_idx", + "columns": ["sandbox_id", "sandbox_session_id"], + "isUnique": true, + "where": "\"driver_instance\".\"status\" IN ('provisioning', 'connecting', 'ready', 'stopping')" + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": { + "driver_instance_status_check": { + "name": "driver_instance_status_check", + "value": "\"driver_instance\".\"status\" IN ('provisioning', 'connecting', 'ready', 'stopping', 'stopped', 'failed')" + }, + "driver_instance_status_seq_check": { + "name": "driver_instance_status_seq_check", + "value": "\"driver_instance\".\"status_seq\" >= 0" + } + } + }, + "external_tool_effect_attempt": { + "name": "external_tool_effect_attempt", + "columns": { + "attempt": { + "name": "attempt", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "claim_token": { + "name": "claim_token", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "completed_at": { + "name": "completed_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "effect_id": { + "name": "effect_id", + "type": "text CHECK (\"effect_id\" = upper(\"effect_id\") AND length(\"effect_id\") = 26 AND substr(\"effect_id\", 1, 1) GLOB '[0-7]' AND \"effect_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "provider_receipt_json": { + "name": "provider_receipt_json", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "result_json": { + "name": "result_json", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "external_tool_effect_attempt_status_idx": { + "name": "external_tool_effect_attempt_status_idx", + "columns": ["status", "created_at"], + "isUnique": false + } + }, + "foreignKeys": { + "external_tool_effect_attempt_effect_id_external_tool_effect_id_fk": { + "name": "external_tool_effect_attempt_effect_id_external_tool_effect_id_fk", + "tableFrom": "external_tool_effect_attempt", + "tableTo": "external_tool_effect", + "columnsFrom": ["effect_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "external_tool_effect_attempt_effect_id_attempt_pk": { + "columns": ["effect_id", "attempt"], + "name": "external_tool_effect_attempt_effect_id_attempt_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": { + "external_tool_effect_attempt_status_check": { + "name": "external_tool_effect_attempt_status_check", + "value": "\"external_tool_effect_attempt\".\"status\" IN ('claimed', 'succeeded', 'unknown')" + }, + "external_tool_effect_attempt_claim_token_uuid_check": { + "name": "external_tool_effect_attempt_claim_token_uuid_check", + "value": "length(\"external_tool_effect_attempt\".\"claim_token\") = 36 AND length(replace(\"external_tool_effect_attempt\".\"claim_token\", '-', '')) = 32 AND \"external_tool_effect_attempt\".\"claim_token\" = lower(\"external_tool_effect_attempt\".\"claim_token\") AND substr(\"external_tool_effect_attempt\".\"claim_token\", 9, 1) = '-' AND substr(\"external_tool_effect_attempt\".\"claim_token\", 14, 1) = '-' AND substr(\"external_tool_effect_attempt\".\"claim_token\", 15, 1) = '4' AND substr(\"external_tool_effect_attempt\".\"claim_token\", 19, 1) = '-' AND substr(\"external_tool_effect_attempt\".\"claim_token\", 20, 1) GLOB '[89ab]' AND substr(\"external_tool_effect_attempt\".\"claim_token\", 24, 1) = '-' AND replace(\"external_tool_effect_attempt\".\"claim_token\", '-', '') NOT GLOB '*[^0-9a-f]*'" + } + } + }, + "external_tool_effect": { + "name": "external_tool_effect", + "columns": { + "attempt_count": { + "name": "attempt_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "claim_token": { + "name": "claim_token", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "command_id": { + "name": "command_id", + "type": "text CHECK (\"command_id\" = upper(\"command_id\") AND length(\"command_id\") = 26 AND substr(\"command_id\", 1, 1) GLOB '[0-7]' AND \"command_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "driver_instance_id": { + "name": "driver_instance_id", + "type": "text CHECK (\"driver_instance_id\" = upper(\"driver_instance_id\") AND length(\"driver_instance_id\") = 26 AND substr(\"driver_instance_id\", 1, 1) GLOB '[0-7]' AND \"driver_instance_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "id": { + "name": "id", + "type": "text CHECK (\"id\" = upper(\"id\") AND length(\"id\") = 26 AND substr(\"id\", 1, 1) GLOB '[0-7]' AND \"id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "idempotency_key": { + "name": "idempotency_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "provider_receipt_json": { + "name": "provider_receipt_json", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "result_json": { + "name": "result_json", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "server_id": { + "name": "server_id", + "type": "text CHECK (\"server_id\" = upper(\"server_id\") AND length(\"server_id\") = 26 AND substr(\"server_id\", 1, 1) GLOB '[0-7]' AND \"server_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "session_run_id": { + "name": "session_run_id", + "type": "text CHECK (\"session_run_id\" = upper(\"session_run_id\") AND length(\"session_run_id\") = 26 AND substr(\"session_run_id\", 1, 1) GLOB '[0-7]' AND \"session_run_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "tool_name": { + "name": "tool_name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "external_tool_effect_command_idx": { + "name": "external_tool_effect_command_idx", + "columns": ["command_id"], + "isUnique": true + }, + "external_tool_effect_idempotency_key_idx": { + "name": "external_tool_effect_idempotency_key_idx", + "columns": ["idempotency_key"], + "isUnique": true + }, + "external_tool_effect_run_status_idx": { + "name": "external_tool_effect_run_status_idx", + "columns": ["session_run_id", "status", "id"], + "isUnique": false + }, + "external_tool_effect_driver_status_idx": { + "name": "external_tool_effect_driver_status_idx", + "columns": ["driver_instance_id", "status"], + "isUnique": false + } + }, + "foreignKeys": { + "external_tool_effect_command_id_driver_command_id_fk": { + "name": "external_tool_effect_command_id_driver_command_id_fk", + "tableFrom": "external_tool_effect", + "tableTo": "driver_command", + "columnsFrom": ["command_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "external_tool_effect_driver_instance_id_driver_instance_id_fk": { + "name": "external_tool_effect_driver_instance_id_driver_instance_id_fk", + "tableFrom": "external_tool_effect", + "tableTo": "driver_instance", + "columnsFrom": ["driver_instance_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "external_tool_effect_session_run_id_session_run_id_fk": { + "name": "external_tool_effect_session_run_id_session_run_id_fk", + "tableFrom": "external_tool_effect", + "tableTo": "session_run", + "columnsFrom": ["session_run_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": { + "external_tool_effect_status_check": { + "name": "external_tool_effect_status_check", + "value": "\"external_tool_effect\".\"status\" IN ('intent', 'claimed', 'succeeded', 'unknown')" + }, + "external_tool_effect_claim_token_uuid_check": { + "name": "external_tool_effect_claim_token_uuid_check", + "value": "\"external_tool_effect\".\"claim_token\" IS NULL OR (length(\"external_tool_effect\".\"claim_token\") = 36 AND length(replace(\"external_tool_effect\".\"claim_token\", '-', '')) = 32 AND \"external_tool_effect\".\"claim_token\" = lower(\"external_tool_effect\".\"claim_token\") AND substr(\"external_tool_effect\".\"claim_token\", 9, 1) = '-' AND substr(\"external_tool_effect\".\"claim_token\", 14, 1) = '-' AND substr(\"external_tool_effect\".\"claim_token\", 15, 1) = '4' AND substr(\"external_tool_effect\".\"claim_token\", 19, 1) = '-' AND substr(\"external_tool_effect\".\"claim_token\", 20, 1) GLOB '[89ab]' AND substr(\"external_tool_effect\".\"claim_token\", 24, 1) = '-' AND replace(\"external_tool_effect\".\"claim_token\", '-', '') NOT GLOB '*[^0-9a-f]*')" + } + } + }, + "native_resume_ref": { + "name": "native_resume_ref", + "columns": { + "committed_session_run_id": { + "name": "committed_session_run_id", + "type": "text CHECK (\"committed_session_run_id\" = upper(\"committed_session_run_id\") AND length(\"committed_session_run_id\") = 26 AND substr(\"committed_session_run_id\", 1, 1) GLOB '[0-7]' AND \"committed_session_run_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "committed_value": { + "name": "committed_value", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "observed_driver_instance_id": { + "name": "observed_driver_instance_id", + "type": "text CHECK (\"observed_driver_instance_id\" = upper(\"observed_driver_instance_id\") AND length(\"observed_driver_instance_id\") = 26 AND substr(\"observed_driver_instance_id\", 1, 1) GLOB '[0-7]' AND \"observed_driver_instance_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "observed_event_seq": { + "name": "observed_event_seq", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "observed_session_run_id": { + "name": "observed_session_run_id", + "type": "text CHECK (\"observed_session_run_id\" = upper(\"observed_session_run_id\") AND length(\"observed_session_run_id\") = 26 AND substr(\"observed_session_run_id\", 1, 1) GLOB '[0-7]' AND \"observed_session_run_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "runtime_id": { + "name": "runtime_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "session_id": { + "name": "session_id", + "type": "text CHECK (\"session_id\" = upper(\"session_id\") AND length(\"session_id\") = 26 AND substr(\"session_id\", 1, 1) GLOB '[0-7]' AND \"session_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "native_resume_ref_runtime_updated_idx": { + "name": "native_resume_ref_runtime_updated_idx", + "columns": ["runtime_id", "updated_at"], + "isUnique": false + } + }, + "foreignKeys": { + "native_resume_ref_session_id_session_id_fk": { + "name": "native_resume_ref_session_id_session_id_fk", + "tableFrom": "native_resume_ref", + "tableTo": "session", + "columnsFrom": ["session_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": { + "native_resume_ref_observed_event_seq_check": { + "name": "native_resume_ref_observed_event_seq_check", + "value": "\"native_resume_ref\".\"observed_event_seq\" >= 0" + } + } + }, + "sandbox_backup": { + "name": "sandbox_backup", + "columns": { + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "dir": { + "name": "dir", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "id": { + "name": "id", + "type": "text CHECK (\"id\" = upper(\"id\") AND length(\"id\") = 26 AND substr(\"id\", 1, 1) GLOB '[0-7]' AND \"id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "keep": { + "name": "keep", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "sandbox_id": { + "name": "sandbox_id", + "type": "text CHECK (\"sandbox_id\" = upper(\"sandbox_id\") AND length(\"sandbox_id\") = 26 AND substr(\"sandbox_id\", 1, 1) GLOB '[0-7]' AND \"sandbox_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "session_run_id": { + "name": "session_run_id", + "type": "text CHECK (\"session_run_id\" = upper(\"session_run_id\") AND length(\"session_run_id\") = 26 AND substr(\"session_run_id\", 1, 1) GLOB '[0-7]' AND \"session_run_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "ttl_seconds": { + "name": "ttl_seconds", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "sandbox_backup_sandbox_status_created_idx": { + "name": "sandbox_backup_sandbox_status_created_idx", + "columns": ["sandbox_id", "status", "created_at"], + "isUnique": false + }, + "sandbox_backup_terminal_checkpoint_idx": { + "name": "sandbox_backup_terminal_checkpoint_idx", + "columns": ["sandbox_id", "dir", "session_run_id"], + "isUnique": true, + "where": "\"sandbox_backup\".\"session_run_id\" IS NOT NULL AND \"sandbox_backup\".\"status\" = 'ready'" + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "sandbox_session": { + "name": "sandbox_session", + "columns": { + "cloudflare_session_id": { + "name": "cloudflare_session_id", + "type": "text CHECK (\"cloudflare_session_id\" = upper(\"cloudflare_session_id\") AND length(\"cloudflare_session_id\") = 26 AND substr(\"cloudflare_session_id\", 1, 1) GLOB '[0-7]' AND \"cloudflare_session_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "cwd": { + "name": "cwd", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "origin_json": { + "name": "origin_json", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "sandbox_id": { + "name": "sandbox_id", + "type": "text CHECK (\"sandbox_id\" = upper(\"sandbox_id\") AND length(\"sandbox_id\") = 26 AND substr(\"sandbox_id\", 1, 1) GLOB '[0-7]' AND \"sandbox_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "session_id": { + "name": "session_id", + "type": "text CHECK (\"session_id\" = upper(\"session_id\") AND length(\"session_id\") = 26 AND substr(\"session_id\", 1, 1) GLOB '[0-7]' AND \"session_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "sandbox_session_sandbox_status_idx": { + "name": "sandbox_session_sandbox_status_idx", + "columns": ["sandbox_id", "status", "updated_at"], + "isUnique": false + }, + "sandbox_session_cloudflare_session_idx": { + "name": "sandbox_session_cloudflare_session_idx", + "columns": ["cloudflare_session_id"], + "isUnique": true + } + }, + "foreignKeys": { + "sandbox_session_session_id_session_id_fk": { + "name": "sandbox_session_session_id_session_id_fk", + "tableFrom": "sandbox_session", + "tableTo": "session", + "columnsFrom": ["session_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "sandbox": { + "name": "sandbox", + "columns": { + "agent_id": { + "name": "agent_id", + "type": "text CHECK (\"agent_id\" = upper(\"agent_id\") AND length(\"agent_id\") = 26 AND substr(\"agent_id\", 1, 1) GLOB '[0-7]' AND \"agent_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "app_id": { + "name": "app_id", + "type": "text CHECK (\"app_id\" = upper(\"app_id\") AND length(\"app_id\") = 26 AND substr(\"app_id\", 1, 1) GLOB '[0-7]' AND \"app_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "bind_mount_ready": { + "name": "bind_mount_ready", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "claim_expires_at": { + "name": "claim_expires_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "claim_owner": { + "name": "claim_owner", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "global_mounts_json": { + "name": "global_mounts_json", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'[]'" + }, + "id": { + "name": "id", + "type": "text CHECK (\"id\" = upper(\"id\") AND length(\"id\") = 26 AND substr(\"id\", 1, 1) GLOB '[0-7]' AND \"id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "inactive_deadline_at": { + "name": "inactive_deadline_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "last_backup_id": { + "name": "last_backup_id", + "type": "text CHECK (\"last_backup_id\" = upper(\"last_backup_id\") AND length(\"last_backup_id\") = 26 AND substr(\"last_backup_id\", 1, 1) GLOB '[0-7]' AND \"last_backup_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_error_code": { + "name": "last_error_code", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_restore_backup_id": { + "name": "last_restore_backup_id", + "type": "text CHECK (\"last_restore_backup_id\" = upper(\"last_restore_backup_id\") AND length(\"last_restore_backup_id\") = 26 AND substr(\"last_restore_backup_id\", 1, 1) GLOB '[0-7]' AND \"last_restore_backup_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "owner_account_id": { + "name": "owner_account_id", + "type": "text CHECK (\"owner_account_id\" = upper(\"owner_account_id\") AND length(\"owner_account_id\") = 26 AND substr(\"owner_account_id\", 1, 1) GLOB '[0-7]' AND \"owner_account_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status_changed_at": { + "name": "status_changed_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "status_event": { + "name": "status_event", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'runtime_subject.cold'" + }, + "status_operation_id": { + "name": "status_operation_id", + "type": "text CHECK (\"status_operation_id\" = upper(\"status_operation_id\") AND length(\"status_operation_id\") = 26 AND substr(\"status_operation_id\", 1, 1) GLOB '[0-7]' AND \"status_operation_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status_seq": { + "name": "status_seq", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "status_source": { + "name": "status_source", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'system'" + }, + "subject_id": { + "name": "subject_id", + "type": "text CHECK (\"subject_id\" = upper(\"subject_id\") AND length(\"subject_id\") = 26 AND substr(\"subject_id\", 1, 1) GLOB '[0-7]' AND \"subject_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "subject_kind": { + "name": "subject_kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "sandbox_subject_idx": { + "name": "sandbox_subject_idx", + "columns": ["kind", "subject_kind", "subject_id"], + "isUnique": true + }, + "sandbox_status_deadline_idx": { + "name": "sandbox_status_deadline_idx", + "columns": ["status", "inactive_deadline_at", "updated_at"], + "isUnique": false + }, + "sandbox_claim_idx": { + "name": "sandbox_claim_idx", + "columns": ["claim_expires_at", "claim_owner"], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": { + "sandbox_status_check": { + "name": "sandbox_status_check", + "value": "\"sandbox\".\"status\" IN ('cold', 'restoring', 'active', 'backing_up', 'destroying', 'error')" + }, + "sandbox_status_seq_check": { + "name": "sandbox_status_seq_check", + "value": "\"sandbox\".\"status_seq\" >= 0" + } + } + }, + "session_message": { + "name": "session_message", + "columns": { + "content_text": { + "name": "content_text", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_by_account_id": { + "name": "created_by_account_id", + "type": "text CHECK (\"created_by_account_id\" = upper(\"created_by_account_id\") AND length(\"created_by_account_id\") = 26 AND substr(\"created_by_account_id\", 1, 1) GLOB '[0-7]' AND \"created_by_account_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "id": { + "name": "id", + "type": "text CHECK (\"id\" = upper(\"id\") AND length(\"id\") = 26 AND substr(\"id\", 1, 1) GLOB '[0-7]' AND \"id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "plan_json": { + "name": "plan_json", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "projection_format": { + "name": "projection_format", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'materialized'" + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "segments_json": { + "name": "segments_json", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "seq": { + "name": "seq", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "session_id": { + "name": "session_id", + "type": "text CHECK (\"session_id\" = upper(\"session_id\") AND length(\"session_id\") = 26 AND substr(\"session_id\", 1, 1) GLOB '[0-7]' AND \"session_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "session_run_id": { + "name": "session_run_id", + "type": "text CHECK (\"session_run_id\" = upper(\"session_run_id\") AND length(\"session_run_id\") = 26 AND substr(\"session_run_id\", 1, 1) GLOB '[0-7]' AND \"session_run_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "session_message_session_seq_idx": { + "name": "session_message_session_seq_idx", + "columns": ["session_id", "seq"], + "isUnique": true + }, + "session_message_run_idx": { + "name": "session_message_run_idx", + "columns": ["session_run_id"], + "isUnique": false + } + }, + "foreignKeys": { + "session_message_session_id_session_id_fk": { + "name": "session_message_session_id_session_id_fk", + "tableFrom": "session_message", + "tableTo": "session", + "columnsFrom": ["session_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": { + "session_message_projection_format_check": { + "name": "session_message_projection_format_check", + "value": "\"session_message\".\"projection_format\" IN ('materialized', 'event_stream_v3')" + }, + "session_message_event_stream_v3_check": { + "name": "session_message_event_stream_v3_check", + "value": "\"session_message\".\"projection_format\" <> 'event_stream_v3' OR (\"session_message\".\"role\" = 'assistant' AND \"session_message\".\"session_run_id\" IS NOT NULL AND \"session_message\".\"content_text\" = '' AND \"session_message\".\"plan_json\" IS NULL AND \"session_message\".\"segments_json\" IS NULL)" + } + } + }, + "session": { + "name": "session", + "columns": { + "agent_id": { + "name": "agent_id", + "type": "text CHECK (\"agent_id\" = upper(\"agent_id\") AND length(\"agent_id\") = 26 AND substr(\"agent_id\", 1, 1) GLOB '[0-7]' AND \"agent_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "archived_at": { + "name": "archived_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "auto_title_event_seq": { + "name": "auto_title_event_seq", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "cleanup_operation_kind": { + "name": "cleanup_operation_kind", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "end_user_id": { + "name": "end_user_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "attributed_user_id": { + "name": "attributed_user_id", + "type": "text CHECK (\"attributed_user_id\" = upper(\"attributed_user_id\") AND length(\"attributed_user_id\") = 26 AND substr(\"attributed_user_id\", 1, 1) GLOB '[0-7]' AND \"attributed_user_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "creator_account_id": { + "name": "creator_account_id", + "type": "text CHECK (\"creator_account_id\" = upper(\"creator_account_id\") AND length(\"creator_account_id\") = 26 AND substr(\"creator_account_id\", 1, 1) GLOB '[0-7]' AND \"creator_account_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "deployment_version_id": { + "name": "deployment_version_id", + "type": "text CHECK (\"deployment_version_id\" = upper(\"deployment_version_id\") AND length(\"deployment_version_id\") = 26 AND substr(\"deployment_version_id\", 1, 1) GLOB '[0-7]' AND \"deployment_version_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "deployment_version_number": { + "name": "deployment_version_number", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "id": { + "name": "id", + "type": "text CHECK (\"id\" = upper(\"id\") AND length(\"id\") = 26 AND substr(\"id\", 1, 1) GLOB '[0-7]' AND \"id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "last_message_at": { + "name": "last_message_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_run_id": { + "name": "last_run_id", + "type": "text CHECK (\"last_run_id\" = upper(\"last_run_id\") AND length(\"last_run_id\") = 26 AND substr(\"last_run_id\", 1, 1) GLOB '[0-7]' AND \"last_run_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "message_seq_cursor": { + "name": "message_seq_cursor", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "metadata_json": { + "name": "metadata_json", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'{}'" + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "app_id": { + "name": "app_id", + "type": "text CHECK (\"app_id\" = upper(\"app_id\") AND length(\"app_id\") = 26 AND substr(\"app_id\", 1, 1) GLOB '[0-7]' AND \"app_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "renamed": { + "name": "renamed", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "runtime_id": { + "name": "runtime_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status_operation_id": { + "name": "status_operation_id", + "type": "text CHECK (\"status_operation_id\" = upper(\"status_operation_id\") AND length(\"status_operation_id\") = 26 AND substr(\"status_operation_id\", 1, 1) GLOB '[0-7]' AND \"status_operation_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status_seq": { + "name": "status_seq", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "runtime_event_seq_cursor": { + "name": "runtime_event_seq_cursor", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "runtime_provisioning_heartbeat_at": { + "name": "runtime_provisioning_heartbeat_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "runtime_provisioning_operation_id": { + "name": "runtime_provisioning_operation_id", + "type": "text CHECK (\"runtime_provisioning_operation_id\" = upper(\"runtime_provisioning_operation_id\") AND length(\"runtime_provisioning_operation_id\") = 26 AND substr(\"runtime_provisioning_operation_id\", 1, 1) GLOB '[0-7]' AND \"runtime_provisioning_operation_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "runtime_provisioning_run_id": { + "name": "runtime_provisioning_run_id", + "type": "text CHECK (\"runtime_provisioning_run_id\" = upper(\"runtime_provisioning_run_id\") AND length(\"runtime_provisioning_run_id\") = 26 AND substr(\"runtime_provisioning_run_id\", 1, 1) GLOB '[0-7]' AND \"runtime_provisioning_run_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "runtime_provisioning_sandbox_id": { + "name": "runtime_provisioning_sandbox_id", + "type": "text CHECK (\"runtime_provisioning_sandbox_id\" = upper(\"runtime_provisioning_sandbox_id\") AND length(\"runtime_provisioning_sandbox_id\") = 26 AND substr(\"runtime_provisioning_sandbox_id\", 1, 1) GLOB '[0-7]' AND \"runtime_provisioning_sandbox_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'preview'" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "workspace_checkpoint_required": { + "name": "workspace_checkpoint_required", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + } + }, + "indexes": { + "session_agent_updated_idx": { + "name": "session_agent_updated_idx", + "columns": ["agent_id", "updated_at", "id"], + "isUnique": false + }, + "session_app_creator_archived_updated_idx": { + "name": "session_app_creator_archived_updated_idx", + "columns": ["app_id", "creator_account_id", "archived_at", "updated_at", "id"], + "isUnique": false + }, + "session_app_attributed_archived_updated_idx": { + "name": "session_app_attributed_archived_updated_idx", + "columns": ["app_id", "attributed_user_id", "archived_at", "updated_at", "id"], + "isUnique": false + }, + "session_app_creator_type_archived_updated_idx": { + "name": "session_app_creator_type_archived_updated_idx", + "columns": ["app_id", "creator_account_id", "type", "archived_at", "updated_at", "id"], + "isUnique": false + }, + "session_app_attributed_type_archived_updated_idx": { + "name": "session_app_attributed_type_archived_updated_idx", + "columns": ["app_id", "attributed_user_id", "type", "archived_at", "updated_at", "id"], + "isUnique": false + }, + "session_status_operation_updated_idx": { + "name": "session_status_operation_updated_idx", + "columns": ["status", "status_operation_id", "updated_at"], + "isUnique": false + }, + "session_cleanup_operation_updated_idx": { + "name": "session_cleanup_operation_updated_idx", + "columns": ["cleanup_operation_kind", "status", "updated_at", "id"], + "isUnique": false + }, + "session_runtime_provisioning_heartbeat_idx": { + "name": "session_runtime_provisioning_heartbeat_idx", + "columns": ["runtime_provisioning_heartbeat_at", "id"], + "isUnique": false + }, + "session_status_updated_idx": { + "name": "session_status_updated_idx", + "columns": ["status", "updated_at", "id"], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": { + "session_cleanup_operation_kind_check": { + "name": "session_cleanup_operation_kind_check", + "value": "\"session\".\"cleanup_operation_kind\" IS NULL OR (\"session\".\"cleanup_operation_kind\" IN ('archive', 'delete') AND \"session\".\"archived_at\" IS NOT NULL AND \"session\".\"status\" IN ('IDLE', 'RESCHEDULING') AND (\"session\".\"status_operation_id\" IS NOT NULL OR (\"session\".\"cleanup_operation_kind\" = 'archive' AND \"session\".\"status\" = 'IDLE')))" + }, + "session_runtime_provisioning_lease_check": { + "name": "session_runtime_provisioning_lease_check", + "value": "(\"session\".\"runtime_provisioning_operation_id\" IS NULL AND \"session\".\"runtime_provisioning_run_id\" IS NULL AND \"session\".\"runtime_provisioning_sandbox_id\" IS NULL AND \"session\".\"runtime_provisioning_heartbeat_at\" IS NULL) OR (\"session\".\"runtime_provisioning_operation_id\" IS NOT NULL AND \"session\".\"runtime_provisioning_sandbox_id\" IS NOT NULL AND \"session\".\"runtime_provisioning_heartbeat_at\" IS NOT NULL AND typeof(\"session\".\"runtime_provisioning_heartbeat_at\") = 'integer' AND \"session\".\"runtime_provisioning_heartbeat_at\" >= 0 AND \"session\".\"archived_at\" IS NULL AND \"session\".\"cleanup_operation_kind\" IS NULL AND \"session\".\"status_operation_id\" IS NULL)" + }, + "session_status_check": { + "name": "session_status_check", + "value": "\"session\".\"status\" IN ('IDLE', 'RUNNING', 'RESCHEDULING', 'TERMINATED')" + }, + "session_auto_title_event_seq_check": { + "name": "session_auto_title_event_seq_check", + "value": "\"session\".\"auto_title_event_seq\" IS NULL OR \"session\".\"auto_title_event_seq\" >= 0" + }, + "session_status_seq_check": { + "name": "session_status_seq_check", + "value": "\"session\".\"status_seq\" >= 0" + } + } + }, + "session_execution_snapshot": { + "name": "session_execution_snapshot", + "columns": { + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "plan_json": { + "name": "plan_json", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "session_id": { + "name": "session_id", + "type": "text CHECK (\"session_id\" = upper(\"session_id\") AND length(\"session_id\") = 26 AND substr(\"session_id\", 1, 1) GLOB '[0-7]' AND \"session_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": true, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "session_execution_snapshot_session_id_session_id_fk": { + "name": "session_execution_snapshot_session_id_session_id_fk", + "tableFrom": "session_execution_snapshot", + "tableTo": "session", + "columnsFrom": ["session_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "session_run_skill": { + "name": "session_run_skill", + "columns": { + "blob_sha256": { + "name": "blob_sha256", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "materialization_status": { + "name": "materialization_status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "mount_path": { + "name": "mount_path", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "resolution_mode": { + "name": "resolution_mode", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "session_run_id": { + "name": "session_run_id", + "type": "text CHECK (\"session_run_id\" = upper(\"session_run_id\") AND length(\"session_run_id\") = 26 AND substr(\"session_run_id\", 1, 1) GLOB '[0-7]' AND \"session_run_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "skill_id": { + "name": "skill_id", + "type": "text CHECK (\"skill_id\" = upper(\"skill_id\") AND length(\"skill_id\") = 26 AND substr(\"skill_id\", 1, 1) GLOB '[0-7]' AND \"skill_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "skill_name": { + "name": "skill_name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "snapshot_id": { + "name": "snapshot_id", + "type": "text CHECK (\"snapshot_id\" = upper(\"snapshot_id\") AND length(\"snapshot_id\") = 26 AND substr(\"snapshot_id\", 1, 1) GLOB '[0-7]' AND \"snapshot_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "warning_code": { + "name": "warning_code", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "session_run_skill_run_resolution_idx": { + "name": "session_run_skill_run_resolution_idx", + "columns": ["session_run_id", "resolution_mode"], + "isUnique": false + } + }, + "foreignKeys": { + "session_run_skill_session_run_id_session_run_id_fk": { + "name": "session_run_skill_session_run_id_session_run_id_fk", + "tableFrom": "session_run_skill", + "tableTo": "session_run", + "columnsFrom": ["session_run_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "session_run_skill_session_run_id_skill_id_pk": { + "columns": ["session_run_id", "skill_id"], + "name": "session_run_skill_session_run_id_skill_id_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "session_run": { + "name": "session_run", + "columns": { + "agent_id": { + "name": "agent_id", + "type": "text CHECK (\"agent_id\" = upper(\"agent_id\") AND length(\"agent_id\") = 26 AND substr(\"agent_id\", 1, 1) GLOB '[0-7]' AND \"agent_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "bound_capability_agent_id": { + "name": "bound_capability_agent_id", + "type": "text CHECK (\"bound_capability_agent_id\" = upper(\"bound_capability_agent_id\") AND length(\"bound_capability_agent_id\") = 26 AND substr(\"bound_capability_agent_id\", 1, 1) GLOB '[0-7]' AND \"bound_capability_agent_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "bound_capability_app_id": { + "name": "bound_capability_app_id", + "type": "text CHECK (\"bound_capability_app_id\" = upper(\"bound_capability_app_id\") AND length(\"bound_capability_app_id\") = 26 AND substr(\"bound_capability_app_id\", 1, 1) GLOB '[0-7]' AND \"bound_capability_app_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "bound_capability_binding_env": { + "name": "bound_capability_binding_env", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "bound_capability_binding_name": { + "name": "bound_capability_binding_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "bound_capability_deployment_id": { + "name": "bound_capability_deployment_id", + "type": "text CHECK (\"bound_capability_deployment_id\" = upper(\"bound_capability_deployment_id\") AND length(\"bound_capability_deployment_id\") = 26 AND substr(\"bound_capability_deployment_id\", 1, 1) GLOB '[0-7]' AND \"bound_capability_deployment_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "bound_capability_deployment_run_id": { + "name": "bound_capability_deployment_run_id", + "type": "text CHECK (\"bound_capability_deployment_run_id\" = upper(\"bound_capability_deployment_run_id\") AND length(\"bound_capability_deployment_run_id\") = 26 AND substr(\"bound_capability_deployment_run_id\", 1, 1) GLOB '[0-7]' AND \"bound_capability_deployment_run_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "completed_at": { + "name": "completed_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_by_account_id": { + "name": "created_by_account_id", + "type": "text CHECK (\"created_by_account_id\" = upper(\"created_by_account_id\") AND length(\"created_by_account_id\") = 26 AND substr(\"created_by_account_id\", 1, 1) GLOB '[0-7]' AND \"created_by_account_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "deployment_version_id": { + "name": "deployment_version_id", + "type": "text CHECK (\"deployment_version_id\" = upper(\"deployment_version_id\") AND length(\"deployment_version_id\") = 26 AND substr(\"deployment_version_id\", 1, 1) GLOB '[0-7]' AND \"deployment_version_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "deployment_version_number": { + "name": "deployment_version_number", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "driver_instance_id": { + "name": "driver_instance_id", + "type": "text CHECK (\"driver_instance_id\" = upper(\"driver_instance_id\") AND length(\"driver_instance_id\") = 26 AND substr(\"driver_instance_id\", 1, 1) GLOB '[0-7]' AND \"driver_instance_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "error_code": { + "name": "error_code", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "error_details_json": { + "name": "error_details_json", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "error_retryable": { + "name": "error_retryable", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "id": { + "name": "id", + "type": "text CHECK (\"id\" = upper(\"id\") AND length(\"id\") = 26 AND substr(\"id\", 1, 1) GLOB '[0-7]' AND \"id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "runtime_id": { + "name": "runtime_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "session_id": { + "name": "session_id", + "type": "text CHECK (\"session_id\" = upper(\"session_id\") AND length(\"session_id\") = 26 AND substr(\"session_id\", 1, 1) GLOB '[0-7]' AND \"session_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "started_at": { + "name": "started_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status_changed_at": { + "name": "status_changed_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "status_event": { + "name": "status_event", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'run.queue'" + }, + "status_operation_id": { + "name": "status_operation_id", + "type": "text CHECK (\"status_operation_id\" = upper(\"status_operation_id\") AND length(\"status_operation_id\") = 26 AND substr(\"status_operation_id\", 1, 1) GLOB '[0-7]' AND \"status_operation_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status_seq": { + "name": "status_seq", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "status_source": { + "name": "status_source", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'system'" + }, + "trace_id": { + "name": "trace_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "trigger": { + "name": "trigger", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "session_run_driver_instance_idx": { + "name": "session_run_driver_instance_idx", + "columns": ["driver_instance_id", "created_at"], + "isUnique": false + }, + "session_run_active_driver_lease_idx": { + "name": "session_run_active_driver_lease_idx", + "columns": ["driver_instance_id"], + "isUnique": true, + "where": "\"session_run\".\"driver_instance_id\" IS NOT NULL AND \"session_run\".\"status\" IN ('queued', 'booting', 'running', 'waiting_input')" + }, + "session_run_session_created_at_idx": { + "name": "session_run_session_created_at_idx", + "columns": ["session_id", "created_at"], + "isUnique": false + }, + "session_run_session_status_idx": { + "name": "session_run_session_status_idx", + "columns": ["session_id", "status"], + "isUnique": false + } + }, + "foreignKeys": { + "session_run_session_id_session_id_fk": { + "name": "session_run_session_id_session_id_fk", + "tableFrom": "session_run", + "tableTo": "session", + "columnsFrom": ["session_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": { + "session_run_error_retryable_check": { + "name": "session_run_error_retryable_check", + "value": "\"session_run\".\"error_retryable\" IS NULL OR (\"session_run\".\"error_retryable\" IN (false, true) AND \"session_run\".\"error_code\" IS NOT NULL AND \"session_run\".\"error_details_json\" IS NOT NULL AND \"session_run\".\"error_message\" IS NOT NULL)" + }, + "session_run_status_check": { + "name": "session_run_status_check", + "value": "\"session_run\".\"status\" IN ('queued', 'booting', 'running', 'waiting_input', 'completed', 'failed', 'cancelled', 'expired')" + }, + "session_run_status_seq_check": { + "name": "session_run_status_seq_check", + "value": "\"session_run\".\"status_seq\" >= 0" + } + } + }, + "session_agent_task_snapshot": { + "name": "session_agent_task_snapshot", + "columns": { + "driver_instance_id": { + "name": "driver_instance_id", + "type": "text CHECK (\"driver_instance_id\" = upper(\"driver_instance_id\") AND length(\"driver_instance_id\") = 26 AND substr(\"driver_instance_id\", 1, 1) GLOB '[0-7]' AND \"driver_instance_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "run_id": { + "name": "run_id", + "type": "text CHECK (\"run_id\" = upper(\"run_id\") AND length(\"run_id\") = 26 AND substr(\"run_id\", 1, 1) GLOB '[0-7]' AND \"run_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "seq": { + "name": "seq", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "session_id": { + "name": "session_id", + "type": "text CHECK (\"session_id\" = upper(\"session_id\") AND length(\"session_id\") = 26 AND substr(\"session_id\", 1, 1) GLOB '[0-7]' AND \"session_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tasks_json": { + "name": "tasks_json", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "session_agent_task_snapshot_run_id_session_run_id_fk": { + "name": "session_agent_task_snapshot_run_id_session_run_id_fk", + "tableFrom": "session_agent_task_snapshot", + "tableTo": "session_run", + "columnsFrom": ["run_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "session_agent_task_snapshot_session_id_session_id_fk": { + "name": "session_agent_task_snapshot_session_id_session_id_fk", + "tableFrom": "session_agent_task_snapshot", + "tableTo": "session", + "columnsFrom": ["session_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "session_event": { + "name": "session_event", + "columns": { + "agent_id": { + "name": "agent_id", + "type": "text CHECK (\"agent_id\" = upper(\"agent_id\") AND length(\"agent_id\") = 26 AND substr(\"agent_id\", 1, 1) GLOB '[0-7]' AND \"agent_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "artifact_attempt_id": { + "name": "artifact_attempt_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "artifact_manifest_json": { + "name": "artifact_manifest_json", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "artifact_manifest_sha256": { + "name": "artifact_manifest_sha256", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "content_text": { + "name": "content_text", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "ended_at": { + "name": "ended_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "event_type": { + "name": "event_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "family": { + "name": "family", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "id": { + "name": "id", + "type": "text CHECK (\"id\" = upper(\"id\") AND length(\"id\") = 26 AND substr(\"id\", 1, 1) GLOB '[0-7]' AND \"id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "mcp_command_id": { + "name": "mcp_command_id", + "type": "text CHECK (\"mcp_command_id\" = upper(\"mcp_command_id\") AND length(\"mcp_command_id\") = 26 AND substr(\"mcp_command_id\", 1, 1) GLOB '[0-7]' AND \"mcp_command_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "occurred_at": { + "name": "occurred_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "process_status": { + "name": "process_status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "process_type": { + "name": "process_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "run_id": { + "name": "run_id", + "type": "text CHECK (\"run_id\" = upper(\"run_id\") AND length(\"run_id\") = 26 AND substr(\"run_id\", 1, 1) GLOB '[0-7]' AND \"run_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "semantic_hash": { + "name": "semantic_hash", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "seq": { + "name": "seq", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "session_id": { + "name": "session_id", + "type": "text CHECK (\"session_id\" = upper(\"session_id\") AND length(\"session_id\") = 26 AND substr(\"session_id\", 1, 1) GLOB '[0-7]' AND \"session_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "source_event_id": { + "name": "source_event_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "stream_id": { + "name": "stream_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "terminal_event_json": { + "name": "terminal_event_json", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "tool_call_id": { + "name": "tool_call_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "tool_input_delta_json": { + "name": "tool_input_delta_json", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "tool_input_json": { + "name": "tool_input_json", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "tool_name": { + "name": "tool_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "tool_output_delta_text": { + "name": "tool_output_delta_text", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "tool_output_text": { + "name": "tool_output_text", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "tool_parent_message_id": { + "name": "tool_parent_message_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "tool_result_message_id": { + "name": "tool_result_message_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "tool_status": { + "name": "tool_status", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "tokens": { + "name": "tokens", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "trace_id": { + "name": "trace_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "visibility": { + "name": "visibility", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "session_event_agent_family_created_idx": { + "name": "session_event_agent_family_created_idx", + "columns": ["agent_id", "family", "created_at", "id"], + "isUnique": false + }, + "session_event_artifact_attempt_idx": { + "name": "session_event_artifact_attempt_idx", + "columns": ["artifact_attempt_id"], + "isUnique": true, + "where": "\"session_event\".\"artifact_attempt_id\" IS NOT NULL" + }, + "session_event_agent_visibility_created_idx": { + "name": "session_event_agent_visibility_created_idx", + "columns": ["agent_id", "visibility", "created_at", "id"], + "isUnique": false + }, + "session_event_agent_created_idx": { + "name": "session_event_agent_created_idx", + "columns": ["agent_id", "created_at", "id"], + "isUnique": false + }, + "session_event_session_visibility_seq_idx": { + "name": "session_event_session_visibility_seq_idx", + "columns": ["session_id", "visibility", "seq"], + "isUnique": false + }, + "session_event_run_event_type_idx": { + "name": "session_event_run_event_type_idx", + "columns": ["run_id", "event_type"], + "isUnique": false + }, + "session_event_run_stream_process_seq_idx": { + "name": "session_event_run_stream_process_seq_idx", + "columns": ["run_id", "stream_id", "process_type", "seq"], + "isUnique": false + }, + "session_event_run_tool_call_seq_idx": { + "name": "session_event_run_tool_call_seq_idx", + "columns": ["run_id", "tool_call_id", "seq"], + "isUnique": false + }, + "session_event_session_seq_idx": { + "name": "session_event_session_seq_idx", + "columns": ["session_id", "seq"], + "isUnique": true + }, + "session_event_session_source_idx": { + "name": "session_event_session_source_idx", + "columns": ["session_id", "source_event_id"], + "isUnique": true + }, + "session_event_run_terminal_winner_idx": { + "name": "session_event_run_terminal_winner_idx", + "columns": ["session_id", "run_id"], + "isUnique": true, + "where": "\"session_event\".\"semantic_hash\" IS NOT NULL AND \"session_event\".\"run_id\" IS NOT NULL AND \"session_event\".\"event_type\" IN ('run.cancelled', 'run.completed', 'run.failed')" + }, + "session_event_mcp_terminal_winner_idx": { + "name": "session_event_mcp_terminal_winner_idx", + "columns": ["session_id", "mcp_command_id"], + "isUnique": true, + "where": "\"session_event\".\"mcp_command_id\" IS NOT NULL" + } + }, + "foreignKeys": { + "session_event_session_id_session_id_fk": { + "name": "session_event_session_id_session_id_fk", + "tableFrom": "session_event", + "tableTo": "session", + "columnsFrom": ["session_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": { + "session_event_artifact_manifest_check": { + "name": "session_event_artifact_manifest_check", + "value": "(\"session_event\".\"artifact_attempt_id\" IS NULL AND \"session_event\".\"artifact_manifest_json\" IS NULL AND \"session_event\".\"artifact_manifest_sha256\" IS NULL) OR (\"session_event\".\"artifact_attempt_id\" IS NOT NULL AND \"session_event\".\"artifact_manifest_json\" IS NOT NULL AND json_valid(\"session_event\".\"artifact_manifest_json\") = 1 AND json_extract(\"session_event\".\"artifact_manifest_json\", '$.version') IS 1 AND json_type(\"session_event\".\"artifact_manifest_json\", '$.captureStatus') IS 'text' AND json_extract(\"session_event\".\"artifact_manifest_json\", '$.captureStatus') IN ('complete', 'omitted_file_limit', 'omitted_runtime_unavailable', 'omitted_size_limit', 'omitted_source_changed', 'omitted_source_missing') AND json_type(\"session_event\".\"artifact_manifest_json\", '$.mode') IS 'text' AND json_extract(\"session_event\".\"artifact_manifest_json\", '$.mode') IN ('delta', 'snapshot') AND (json_extract(\"session_event\".\"artifact_manifest_json\", '$.captureStatus') = 'complete' OR json_array_length(\"session_event\".\"artifact_manifest_json\", '$.files') = 0) AND json_extract(\"session_event\".\"artifact_manifest_json\", '$.sourceEventId') IS \"session_event\".\"source_event_id\" AND json_extract(\"session_event\".\"artifact_manifest_json\", '$.semanticHash') IS \"session_event\".\"semantic_hash\" AND json_type(\"session_event\".\"artifact_manifest_json\", '$.files') IS 'array' AND \"session_event\".\"artifact_manifest_sha256\" IS NOT NULL AND length(\"session_event\".\"artifact_manifest_sha256\") = 64 AND \"session_event\".\"artifact_manifest_sha256\" = lower(\"session_event\".\"artifact_manifest_sha256\") AND \"session_event\".\"artifact_manifest_sha256\" NOT GLOB '*[^0-9a-f]*' AND \"session_event\".\"semantic_hash\" IS NOT NULL AND \"session_event\".\"event_type\" IN ('file.change.updated', 'file.changed', 'run.completed'))" + }, + "session_event_mcp_command_check": { + "name": "session_event_mcp_command_check", + "value": "\"session_event\".\"mcp_command_id\" IS NULL OR (\"session_event\".\"event_type\" = 'tool.call.updated' AND \"session_event\".\"tool_status\" IS NOT NULL AND \"session_event\".\"tool_status\" IN ('completed', 'failed', 'cancelled'))" + }, + "session_event_semantic_hash_check": { + "name": "session_event_semantic_hash_check", + "value": "\"session_event\".\"semantic_hash\" IS NULL OR (length(\"session_event\".\"semantic_hash\") = 64 AND \"session_event\".\"semantic_hash\" = lower(\"session_event\".\"semantic_hash\") AND \"session_event\".\"semantic_hash\" NOT GLOB '*[^0-9a-f]*')" + }, + "session_event_terminal_event_json_check": { + "name": "session_event_terminal_event_json_check", + "value": "(\"session_event\".\"terminal_event_json\" IS NULL AND NOT (\"session_event\".\"semantic_hash\" IS NOT NULL AND \"session_event\".\"event_type\" IN ('run.cancelled', 'run.completed', 'run.failed'))) OR (\"session_event\".\"terminal_event_json\" IS NOT NULL AND json_valid(\"session_event\".\"terminal_event_json\") = 1 AND \"session_event\".\"semantic_hash\" IS NOT NULL AND \"session_event\".\"event_type\" IN ('run.cancelled', 'run.completed', 'run.failed'))" + }, + "session_event_tool_input_kind_check": { + "name": "session_event_tool_input_kind_check", + "value": "\"session_event\".\"tool_input_delta_json\" IS NULL OR \"session_event\".\"tool_input_json\" IS NULL" + }, + "session_event_tool_output_kind_check": { + "name": "session_event_tool_output_kind_check", + "value": "\"session_event\".\"tool_output_delta_text\" IS NULL OR \"session_event\".\"tool_output_text\" IS NULL" + }, + "session_event_tool_status_check": { + "name": "session_event_tool_status_check", + "value": "\"session_event\".\"tool_status\" IS NULL OR \"session_event\".\"tool_status\" IN ('running', 'completed', 'failed', 'cancelled')" + } + } + }, + "session_model_call": { + "name": "session_model_call", + "columns": { + "cache_creation_tokens": { + "name": "cache_creation_tokens", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "cache_read_tokens": { + "name": "cache_read_tokens", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "call_key": { + "name": "call_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "completed_at": { + "name": "completed_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "cost_currency": { + "name": "cost_currency", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "driver_instance_id": { + "name": "driver_instance_id", + "type": "text CHECK (\"driver_instance_id\" = upper(\"driver_instance_id\") AND length(\"driver_instance_id\") = 26 AND substr(\"driver_instance_id\", 1, 1) GLOB '[0-7]' AND \"driver_instance_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "error_code": { + "name": "error_code", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "id": { + "name": "id", + "type": "text CHECK (\"id\" = upper(\"id\") AND length(\"id\") = 26 AND substr(\"id\", 1, 1) GLOB '[0-7]' AND \"id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "input_tokens": { + "name": "input_tokens", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "metadata_json": { + "name": "metadata_json", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "native_call_id": { + "name": "native_call_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "output_tokens": { + "name": "output_tokens", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "source_event_seq": { + "name": "source_event_seq", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "session_id": { + "name": "session_id", + "type": "text CHECK (\"session_id\" = upper(\"session_id\") AND length(\"session_id\") = 26 AND substr(\"session_id\", 1, 1) GLOB '[0-7]' AND \"session_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "session_run_id": { + "name": "session_run_id", + "type": "text CHECK (\"session_run_id\" = upper(\"session_run_id\") AND length(\"session_run_id\") = 26 AND substr(\"session_run_id\", 1, 1) GLOB '[0-7]' AND \"session_run_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "started_at": { + "name": "started_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "total_cost_usd_micros": { + "name": "total_cost_usd_micros", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "trace_id": { + "name": "trace_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "session_model_call_run_created_idx": { + "name": "session_model_call_run_created_idx", + "columns": ["session_run_id", "created_at"], + "isUnique": false + }, + "session_model_call_session_created_idx": { + "name": "session_model_call_session_created_idx", + "columns": ["session_id", "created_at"], + "isUnique": false + }, + "session_model_call_run_key_idx": { + "name": "session_model_call_run_key_idx", + "columns": ["session_run_id", "call_key"], + "isUnique": true + }, + "session_model_call_native_idx": { + "name": "session_model_call_native_idx", + "columns": ["driver_instance_id", "native_call_id"], + "isUnique": true + } + }, + "foreignKeys": { + "session_model_call_session_id_session_id_fk": { + "name": "session_model_call_session_id_session_id_fk", + "tableFrom": "session_model_call", + "tableTo": "session", + "columnsFrom": ["session_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "session_model_call_session_run_id_session_run_id_fk": { + "name": "session_model_call_session_run_id_session_run_id_fk", + "tableFrom": "session_model_call", + "tableTo": "session_run", + "columnsFrom": ["session_run_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": { + "session_model_call_source_event_seq_check": { + "name": "session_model_call_source_event_seq_check", + "value": "\"session_model_call\".\"source_event_seq\" >= 0" + } + } + }, + "session_permission_request": { + "name": "session_permission_request", + "columns": { + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "driver_instance_id": { + "name": "driver_instance_id", + "type": "text CHECK (\"driver_instance_id\" = upper(\"driver_instance_id\") AND length(\"driver_instance_id\") = 26 AND substr(\"driver_instance_id\", 1, 1) GLOB '[0-7]' AND \"driver_instance_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "raw_input": { + "name": "raw_input", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "request_id": { + "name": "request_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "run_id": { + "name": "run_id", + "type": "text CHECK (\"run_id\" = upper(\"run_id\") AND length(\"run_id\") = 26 AND substr(\"run_id\", 1, 1) GLOB '[0-7]' AND \"run_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "session_id": { + "name": "session_id", + "type": "text CHECK (\"session_id\" = upper(\"session_id\") AND length(\"session_id\") = 26 AND substr(\"session_id\", 1, 1) GLOB '[0-7]' AND \"session_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "tool_call_id": { + "name": "tool_call_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "tool_kind": { + "name": "tool_kind", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "session_permission_request_run_idx": { + "name": "session_permission_request_run_idx", + "columns": ["session_id", "run_id"], + "isUnique": false + } + }, + "foreignKeys": { + "session_permission_request_session_id_session_id_fk": { + "name": "session_permission_request_session_id_session_id_fk", + "tableFrom": "session_permission_request", + "tableTo": "session", + "columnsFrom": ["session_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "session_permission_request_session_id_request_id_pk": { + "columns": ["session_id", "request_id"], + "name": "session_permission_request_session_id_request_id_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "session_readiness_snapshot": { + "name": "session_readiness_snapshot", + "columns": { + "readiness_json": { + "name": "readiness_json", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "session_id": { + "name": "session_id", + "type": "text CHECK (\"session_id\" = upper(\"session_id\") AND length(\"session_id\") = 26 AND substr(\"session_id\", 1, 1) GLOB '[0-7]' AND \"session_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "session_readiness_snapshot_session_id_session_id_fk": { + "name": "session_readiness_snapshot_session_id_session_id_fk", + "tableFrom": "session_readiness_snapshot", + "tableTo": "session", + "columnsFrom": ["session_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "skill_snapshot_entry": { + "name": "skill_snapshot_entry", + "columns": { + "entry_kind": { + "name": "entry_kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "is_executable": { + "name": "is_executable", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "mime_type": { + "name": "mime_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "path": { + "name": "path", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "sha256": { + "name": "sha256", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "size": { + "name": "size", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "snapshot_id": { + "name": "snapshot_id", + "type": "text CHECK (\"snapshot_id\" = upper(\"snapshot_id\") AND length(\"snapshot_id\") = 26 AND substr(\"snapshot_id\", 1, 1) GLOB '[0-7]' AND \"snapshot_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "skill_snapshot_entry_snapshot_id_path_pk": { + "columns": ["snapshot_id", "path"], + "name": "skill_snapshot_entry_snapshot_id_path_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "skill_snapshot": { + "name": "skill_snapshot", + "columns": { + "author": { + "name": "author", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "blob_key": { + "name": "blob_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "blob_sha256": { + "name": "blob_sha256", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "blob_size": { + "name": "blob_size", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "id": { + "name": "id", + "type": "text CHECK (\"id\" = upper(\"id\") AND length(\"id\") = 26 AND substr(\"id\", 1, 1) GLOB '[0-7]' AND \"id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "app_id": { + "name": "app_id", + "type": "text CHECK (\"app_id\" = upper(\"app_id\") AND length(\"app_id\") = 26 AND substr(\"app_id\", 1, 1) GLOB '[0-7]' AND \"app_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "skill_markdown_path": { + "name": "skill_markdown_path", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "uncompressed_size": { + "name": "uncompressed_size", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "version": { + "name": "version", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "skill_snapshot_app_created_at_idx": { + "name": "skill_snapshot_app_created_at_idx", + "columns": ["app_id", "created_at"], + "isUnique": false + }, + "skill_snapshot_blob_sha256_idx": { + "name": "skill_snapshot_blob_sha256_idx", + "columns": ["app_id", "blob_sha256"], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "skill": { + "name": "skill", + "columns": { + "author": { + "name": "author", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "current_snapshot_id": { + "name": "current_snapshot_id", + "type": "text CHECK (\"current_snapshot_id\" = upper(\"current_snapshot_id\") AND length(\"current_snapshot_id\") = 26 AND substr(\"current_snapshot_id\", 1, 1) GLOB '[0-7]' AND \"current_snapshot_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "forked_from_owner_name": { + "name": "forked_from_owner_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "forked_from_skill_id": { + "name": "forked_from_skill_id", + "type": "text CHECK (\"forked_from_skill_id\" = upper(\"forked_from_skill_id\") AND length(\"forked_from_skill_id\") = 26 AND substr(\"forked_from_skill_id\", 1, 1) GLOB '[0-7]' AND \"forked_from_skill_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "forked_from_skill_name": { + "name": "forked_from_skill_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "id": { + "name": "id", + "type": "text CHECK (\"id\" = upper(\"id\") AND length(\"id\") = 26 AND substr(\"id\", 1, 1) GLOB '[0-7]' AND \"id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "owner_account_id": { + "name": "owner_account_id", + "type": "text CHECK (\"owner_account_id\" = upper(\"owner_account_id\") AND length(\"owner_account_id\") = 26 AND substr(\"owner_account_id\", 1, 1) GLOB '[0-7]' AND \"owner_account_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "app_id": { + "name": "app_id", + "type": "text CHECK (\"app_id\" = upper(\"app_id\") AND length(\"app_id\") = 26 AND substr(\"app_id\", 1, 1) GLOB '[0-7]' AND \"app_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "source_kind": { + "name": "source_kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "version": { + "name": "version", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "skill_app_updated_at_idx": { + "name": "skill_app_updated_at_idx", + "columns": ["app_id", "updated_at"], + "isUnique": false + }, + "skill_owner_account_updated_at_idx": { + "name": "skill_owner_account_updated_at_idx", + "columns": ["owner_account_id", "updated_at"], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "account": { + "name": "account", + "columns": { + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "email_verified": { + "name": "email_verified", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "id": { + "name": "id", + "type": "text CHECK (\"id\" = upper(\"id\") AND length(\"id\") = 26 AND substr(\"id\", 1, 1) GLOB '[0-7]' AND \"id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "image_url": { + "name": "image_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_active_organization_id": { + "name": "last_active_organization_id", + "type": "text CHECK (\"last_active_organization_id\" = upper(\"last_active_organization_id\") AND length(\"last_active_organization_id\") = 26 AND substr(\"last_active_organization_id\", 1, 1) GLOB '[0-7]' AND \"last_active_organization_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "system_agent_model": { + "name": "system_agent_model", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "account_email_idx": { + "name": "account_email_idx", + "columns": ["email"], + "isUnique": true + }, + "account_last_active_organization_idx": { + "name": "account_last_active_organization_idx", + "columns": ["last_active_organization_id"], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "usage_daily_rollup": { + "name": "usage_daily_rollup", + "columns": { + "actor_user_id": { + "name": "actor_user_id", + "type": "text CHECK (\"actor_user_id\" = upper(\"actor_user_id\") AND length(\"actor_user_id\") = 26 AND substr(\"actor_user_id\", 1, 1) GLOB '[0-7]' AND \"actor_user_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "agent_id": { + "name": "agent_id", + "type": "text CHECK (\"agent_id\" = upper(\"agent_id\") AND length(\"agent_id\") = 26 AND substr(\"agent_id\", 1, 1) GLOB '[0-7]' AND \"agent_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "agent_owner_user_id": { + "name": "agent_owner_user_id", + "type": "text CHECK (\"agent_owner_user_id\" = upper(\"agent_owner_user_id\") AND length(\"agent_owner_user_id\") = 26 AND substr(\"agent_owner_user_id\", 1, 1) GLOB '[0-7]' AND \"agent_owner_user_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "agent_publication_state_at_run": { + "name": "agent_publication_state_at_run", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "cache_creation_tokens": { + "name": "cache_creation_tokens", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "cache_read_tokens": { + "name": "cache_read_tokens", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "date": { + "name": "date", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "input_tokens": { + "name": "input_tokens", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "organization_id": { + "name": "organization_id", + "type": "text CHECK (\"organization_id\" = upper(\"organization_id\") AND length(\"organization_id\") = 26 AND substr(\"organization_id\", 1, 1) GLOB '[0-7]' AND \"organization_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "app_id": { + "name": "app_id", + "type": "text CHECK (\"app_id\" = upper(\"app_id\") AND length(\"app_id\") = 26 AND substr(\"app_id\", 1, 1) GLOB '[0-7]' AND \"app_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "output_tokens": { + "name": "output_tokens", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "request_count": { + "name": "request_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "run_purpose": { + "name": "run_purpose", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "total_cost_usd_micros": { + "name": "total_cost_usd_micros", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "unpriced_request_count": { + "name": "unpriced_request_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "usage_daily_rollup_app_date_idx": { + "name": "usage_daily_rollup_app_date_idx", + "columns": ["app_id", "date"], + "isUnique": false + }, + "usage_daily_rollup_organization_date_idx": { + "name": "usage_daily_rollup_organization_date_idx", + "columns": ["organization_id", "date"], + "isUnique": false + }, + "usage_daily_rollup_agent_date_idx": { + "name": "usage_daily_rollup_agent_date_idx", + "columns": ["agent_id", "date"], + "isUnique": false + }, + "usage_daily_rollup_actor_date_idx": { + "name": "usage_daily_rollup_actor_date_idx", + "columns": ["actor_user_id", "date"], + "isUnique": false + }, + "usage_daily_rollup_owner_date_idx": { + "name": "usage_daily_rollup_owner_date_idx", + "columns": ["agent_owner_user_id", "date"], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "usage_daily_rollup_organization_id_app_id_agent_id_actor_user_id_agent_owner_user_id_date_agent_publication_state_at_run_run_purpose_provider_model_pk": { + "columns": [ + "organization_id", + "app_id", + "agent_id", + "actor_user_id", + "agent_owner_user_id", + "date", + "agent_publication_state_at_run", + "run_purpose", + "provider", + "model" + ], + "name": "usage_daily_rollup_organization_id_app_id_agent_id_actor_user_id_agent_owner_user_id_date_agent_publication_state_at_run_run_purpose_provider_model_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "usage_event_rollup_receipt": { + "name": "usage_event_rollup_receipt", + "columns": { + "rolled_up_at": { + "name": "rolled_up_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "source_event_id": { + "name": "source_event_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "usage_event_rollup_receipt_rolled_up_at_idx": { + "name": "usage_event_rollup_receipt_rolled_up_at_idx", + "columns": ["rolled_up_at"], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "usage_event_rollup_receipt_source_source_event_id_pk": { + "columns": ["source", "source_event_id"], + "name": "usage_event_rollup_receipt_source_source_event_id_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "usage_event": { + "name": "usage_event", + "columns": { + "actor_user_id": { + "name": "actor_user_id", + "type": "text CHECK (\"actor_user_id\" = upper(\"actor_user_id\") AND length(\"actor_user_id\") = 26 AND substr(\"actor_user_id\", 1, 1) GLOB '[0-7]' AND \"actor_user_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "agent_id": { + "name": "agent_id", + "type": "text CHECK (\"agent_id\" = upper(\"agent_id\") AND length(\"agent_id\") = 26 AND substr(\"agent_id\", 1, 1) GLOB '[0-7]' AND \"agent_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "agent_owner_user_id": { + "name": "agent_owner_user_id", + "type": "text CHECK (\"agent_owner_user_id\" = upper(\"agent_owner_user_id\") AND length(\"agent_owner_user_id\") = 26 AND substr(\"agent_owner_user_id\", 1, 1) GLOB '[0-7]' AND \"agent_owner_user_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "agent_publication_state_at_run": { + "name": "agent_publication_state_at_run", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "agent_revision_id": { + "name": "agent_revision_id", + "type": "text CHECK (\"agent_revision_id\" = upper(\"agent_revision_id\") AND length(\"agent_revision_id\") = 26 AND substr(\"agent_revision_id\", 1, 1) GLOB '[0-7]' AND \"agent_revision_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "cache_creation_tokens": { + "name": "cache_creation_tokens", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "cache_read_tokens": { + "name": "cache_read_tokens", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "id": { + "name": "id", + "type": "text CHECK (\"id\" = upper(\"id\") AND length(\"id\") = 26 AND substr(\"id\", 1, 1) GLOB '[0-7]' AND \"id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "input_tokens": { + "name": "input_tokens", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "organization_id": { + "name": "organization_id", + "type": "text CHECK (\"organization_id\" = upper(\"organization_id\") AND length(\"organization_id\") = 26 AND substr(\"organization_id\", 1, 1) GLOB '[0-7]' AND \"organization_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "app_id": { + "name": "app_id", + "type": "text CHECK (\"app_id\" = upper(\"app_id\") AND length(\"app_id\") = 26 AND substr(\"app_id\", 1, 1) GLOB '[0-7]' AND \"app_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "output_tokens": { + "name": "output_tokens", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "price_snapshot_json": { + "name": "price_snapshot_json", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "pricing_status": { + "name": "pricing_status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "run_purpose": { + "name": "run_purpose", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "runtime_id": { + "name": "runtime_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "session_id": { + "name": "session_id", + "type": "text CHECK (\"session_id\" = upper(\"session_id\") AND length(\"session_id\") = 26 AND substr(\"session_id\", 1, 1) GLOB '[0-7]' AND \"session_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "session_run_id": { + "name": "session_run_id", + "type": "text CHECK (\"session_run_id\" = upper(\"session_run_id\") AND length(\"session_run_id\") = 26 AND substr(\"session_run_id\", 1, 1) GLOB '[0-7]' AND \"session_run_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "source_event_id": { + "name": "source_event_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "source_event_seq": { + "name": "source_event_seq", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "total_cost_usd_micros": { + "name": "total_cost_usd_micros", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "usage_contract": { + "name": "usage_contract", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "usage_event_app_created_idx": { + "name": "usage_event_app_created_idx", + "columns": ["app_id", "created_at"], + "isUnique": false + }, + "usage_event_organization_created_idx": { + "name": "usage_event_organization_created_idx", + "columns": ["organization_id", "created_at"], + "isUnique": false + }, + "usage_event_agent_created_idx": { + "name": "usage_event_agent_created_idx", + "columns": ["agent_id", "created_at"], + "isUnique": false + }, + "usage_event_actor_created_idx": { + "name": "usage_event_actor_created_idx", + "columns": ["actor_user_id", "created_at"], + "isUnique": false + }, + "usage_event_owner_created_idx": { + "name": "usage_event_owner_created_idx", + "columns": ["agent_owner_user_id", "created_at"], + "isUnique": false + }, + "usage_event_session_run_idx": { + "name": "usage_event_session_run_idx", + "columns": ["session_run_id"], + "isUnique": false + }, + "usage_event_source_event_idx": { + "name": "usage_event_source_event_idx", + "columns": ["source", "source_event_id"], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": { + "usage_event_source_event_seq_check": { + "name": "usage_event_source_event_seq_check", + "value": "\"usage_event\".\"source_event_seq\" >= 0" + } + } + }, + "vendor_credential": { + "name": "vendor_credential", + "columns": { + "api_base": { + "name": "api_base", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "api_key_secret_id": { + "name": "api_key_secret_id", + "type": "text CHECK (\"api_key_secret_id\" = upper(\"api_key_secret_id\") AND length(\"api_key_secret_id\") = 26 AND substr(\"api_key_secret_id\", 1, 1) GLOB '[0-7]' AND \"api_key_secret_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "id": { + "name": "id", + "type": "text CHECK (\"id\" = upper(\"id\") AND length(\"id\") = 26 AND substr(\"id\", 1, 1) GLOB '[0-7]' AND \"id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "is_default": { + "name": "is_default", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "models": { + "name": "models", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "app_id": { + "name": "app_id", + "type": "text CHECK (\"app_id\" = upper(\"app_id\") AND length(\"app_id\") = 26 AND substr(\"app_id\", 1, 1) GLOB '[0-7]' AND \"app_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "vendor_id": { + "name": "vendor_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "vendor_credential_app_vendor_idx": { + "name": "vendor_credential_app_vendor_idx", + "columns": ["app_id", "vendor_id"], + "isUnique": false + }, + "vendor_credential_app_vendor_name_idx": { + "name": "vendor_credential_app_vendor_name_idx", + "columns": ["app_id", "vendor_id", "name"], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + } + }, + "views": {}, + "enums": {}, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + }, + "internal": { + "indexes": { + "file_record_listing_idx": { + "columns": { + "lower(\"name\")": { + "isExpression": true + } + } + } + } + } +} diff --git a/pkgs/db/drizzle/meta/0017_snapshot.json b/pkgs/db/drizzle/meta/0017_snapshot.json new file mode 100644 index 00000000..2efa19da --- /dev/null +++ b/pkgs/db/drizzle/meta/0017_snapshot.json @@ -0,0 +1,7217 @@ +{ + "version": "6", + "dialect": "sqlite", + "id": "de5f1a98-662b-494a-bf74-542d94feddad", + "prevId": "0e5397c2-d7b5-4fff-bc06-7bff73b243cc", + "tables": { + "agent_deployment_version": { + "name": "agent_deployment_version", + "columns": { + "agent_id": { + "name": "agent_id", + "type": "text CHECK (\"agent_id\" = upper(\"agent_id\") AND length(\"agent_id\") = 26 AND substr(\"agent_id\", 1, 1) GLOB '[0-7]' AND \"agent_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "config_json": { + "name": "config_json", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_by_account_id": { + "name": "created_by_account_id", + "type": "text CHECK (\"created_by_account_id\" = upper(\"created_by_account_id\") AND length(\"created_by_account_id\") = 26 AND substr(\"created_by_account_id\", 1, 1) GLOB '[0-7]' AND \"created_by_account_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "environment_id": { + "name": "environment_id", + "type": "text CHECK (\"environment_id\" = upper(\"environment_id\") AND length(\"environment_id\") = 26 AND substr(\"environment_id\", 1, 1) GLOB '[0-7]' AND \"environment_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "id": { + "name": "id", + "type": "text CHECK (\"id\" = upper(\"id\") AND length(\"id\") = 26 AND substr(\"id\", 1, 1) GLOB '[0-7]' AND \"id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "mcp_bindings_json": { + "name": "mcp_bindings_json", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "prompt": { + "name": "prompt", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "runtime_id": { + "name": "runtime_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "skills_json": { + "name": "skills_json", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "summary": { + "name": "summary", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "version_number": { + "name": "version_number", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "agent_deployment_version_agent_number_idx": { + "name": "agent_deployment_version_agent_number_idx", + "columns": ["agent_id", "version_number"], + "isUnique": true + }, + "agent_deployment_version_agent_created_idx": { + "name": "agent_deployment_version_agent_created_idx", + "columns": ["agent_id", "created_at"], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "agent_mcp_binding": { + "name": "agent_mcp_binding", + "columns": { + "agent_credential_id": { + "name": "agent_credential_id", + "type": "text CHECK (\"agent_credential_id\" = upper(\"agent_credential_id\") AND length(\"agent_credential_id\") = 26 AND substr(\"agent_credential_id\", 1, 1) GLOB '[0-7]' AND \"agent_credential_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "agent_id": { + "name": "agent_id", + "type": "text CHECK (\"agent_id\" = upper(\"agent_id\") AND length(\"agent_id\") = 26 AND substr(\"agent_id\", 1, 1) GLOB '[0-7]' AND \"agent_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "credential_mode": { + "name": "credential_mode", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'runtime_resolved'" + }, + "enabled": { + "name": "enabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "id": { + "name": "id", + "type": "text CHECK (\"id\" = upper(\"id\") AND length(\"id\") = 26 AND substr(\"id\", 1, 1) GLOB '[0-7]' AND \"id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "server_id": { + "name": "server_id", + "type": "text CHECK (\"server_id\" = upper(\"server_id\") AND length(\"server_id\") = 26 AND substr(\"server_id\", 1, 1) GLOB '[0-7]' AND \"server_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "agent_mcp_binding_agent_sort_idx": { + "name": "agent_mcp_binding_agent_sort_idx", + "columns": ["agent_id", "sort_order"], + "isUnique": true + }, + "agent_mcp_binding_server_idx": { + "name": "agent_mcp_binding_server_idx", + "columns": ["server_id"], + "isUnique": false + }, + "agent_mcp_binding_profile_server_idx": { + "name": "agent_mcp_binding_profile_server_idx", + "columns": ["agent_id", "server_id"], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": { + "agent_mcp_binding_agent_credential_shape_check": { + "name": "agent_mcp_binding_agent_credential_shape_check", + "value": "\n (\"agent_mcp_binding\".\"credential_mode\" = 'agent_bound' AND \"agent_mcp_binding\".\"agent_credential_id\" IS NOT NULL)\n OR (\"agent_mcp_binding\".\"credential_mode\" = 'runtime_resolved' AND \"agent_mcp_binding\".\"agent_credential_id\" IS NULL)\n " + } + } + }, + "agent_skill": { + "name": "agent_skill", + "columns": { + "agent_id": { + "name": "agent_id", + "type": "text CHECK (\"agent_id\" = upper(\"agent_id\") AND length(\"agent_id\") = 26 AND substr(\"agent_id\", 1, 1) GLOB '[0-7]' AND \"agent_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "skill_id": { + "name": "skill_id", + "type": "text CHECK (\"skill_id\" = upper(\"skill_id\") AND length(\"skill_id\") = 26 AND substr(\"skill_id\", 1, 1) GLOB '[0-7]' AND \"skill_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "agent_skill_agent_sort_idx": { + "name": "agent_skill_agent_sort_idx", + "columns": ["agent_id", "sort_order"], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "agent_skill_agent_id_skill_id_pk": { + "columns": ["agent_id", "skill_id"], + "name": "agent_skill_agent_id_skill_id_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "agent": { + "name": "agent", + "columns": { + "config_json": { + "name": "config_json", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "environment_id": { + "name": "environment_id", + "type": "text CHECK (\"environment_id\" = upper(\"environment_id\") AND length(\"environment_id\") = 26 AND substr(\"environment_id\", 1, 1) GLOB '[0-7]' AND \"environment_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "id": { + "name": "id", + "type": "text CHECK (\"id\" = upper(\"id\") AND length(\"id\") = 26 AND substr(\"id\", 1, 1) GLOB '[0-7]' AND \"id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'pet'" + }, + "live_deployment_version_id": { + "name": "live_deployment_version_id", + "type": "text CHECK (\"live_deployment_version_id\" = upper(\"live_deployment_version_id\") AND length(\"live_deployment_version_id\") = 26 AND substr(\"live_deployment_version_id\", 1, 1) GLOB '[0-7]' AND \"live_deployment_version_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "owner_account_id": { + "name": "owner_account_id", + "type": "text CHECK (\"owner_account_id\" = upper(\"owner_account_id\") AND length(\"owner_account_id\") = 26 AND substr(\"owner_account_id\", 1, 1) GLOB '[0-7]' AND \"owner_account_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "app_id": { + "name": "app_id", + "type": "text CHECK (\"app_id\" = upper(\"app_id\") AND length(\"app_id\") = 26 AND substr(\"app_id\", 1, 1) GLOB '[0-7]' AND \"app_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "prompt": { + "name": "prompt", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "runtime_id": { + "name": "runtime_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'draft'" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "visibility": { + "name": "visibility", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'private'" + } + }, + "indexes": { + "agent_app_owner_account_idx": { + "name": "agent_app_owner_account_idx", + "columns": ["app_id", "owner_account_id"], + "isUnique": false + }, + "agent_app_status_idx": { + "name": "agent_app_status_idx", + "columns": ["app_id", "status"], + "isUnique": false + }, + "agent_environment_idx": { + "name": "agent_environment_idx", + "columns": ["environment_id"], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": { + "agent_published_live_deployment_version_check": { + "name": "agent_published_live_deployment_version_check", + "value": "\"agent\".\"status\" <> 'published' OR \"agent\".\"live_deployment_version_id\" IS NOT NULL" + } + } + }, + "api_command": { + "name": "api_command", + "columns": { + "attempt_count": { + "name": "attempt_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "claim_expires_at": { + "name": "claim_expires_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "claim_owner": { + "name": "claim_owner", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "completed_at": { + "name": "completed_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "dedupe_key": { + "name": "dedupe_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "id": { + "name": "id", + "type": "text CHECK (\"id\" = upper(\"id\") AND length(\"id\") = 26 AND substr(\"id\", 1, 1) GLOB '[0-7]' AND \"id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "last_error_code": { + "name": "last_error_code", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_error_message": { + "name": "last_error_message", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "payload_json": { + "name": "payload_json", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "api_command_dedupe_idx": { + "name": "api_command_dedupe_idx", + "columns": ["dedupe_key"], + "isUnique": true + }, + "api_command_status_updated_idx": { + "name": "api_command_status_updated_idx", + "columns": ["status", "updated_at"], + "isUnique": false + }, + "api_command_claim_idx": { + "name": "api_command_claim_idx", + "columns": ["status", "claim_expires_at"], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "auth_account": { + "name": "auth_account", + "columns": { + "access_token": { + "name": "access_token", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "access_token_expires_at": { + "name": "access_token_expires_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "provider_account_id": { + "name": "provider_account_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "id_token": { + "name": "id_token", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "refresh_token": { + "name": "refresh_token", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "refresh_token_expires_at": { + "name": "refresh_token_expires_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "account_id": { + "name": "account_id", + "type": "text CHECK (\"account_id\" = upper(\"account_id\") AND length(\"account_id\") = 26 AND substr(\"account_id\", 1, 1) GLOB '[0-7]' AND \"account_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "auth_account_provider_account_idx": { + "name": "auth_account_provider_account_idx", + "columns": ["provider_id", "provider_account_id"], + "isUnique": true + }, + "auth_account_account_id_idx": { + "name": "auth_account_account_id_idx", + "columns": ["account_id"], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "auth_session": { + "name": "auth_session", + "columns": { + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "ip_address": { + "name": "ip_address", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "account_id": { + "name": "account_id", + "type": "text CHECK (\"account_id\" = upper(\"account_id\") AND length(\"account_id\") = 26 AND substr(\"account_id\", 1, 1) GLOB '[0-7]' AND \"account_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "auth_session_expires_at_idx": { + "name": "auth_session_expires_at_idx", + "columns": ["expires_at"], + "isUnique": false + }, + "auth_session_token_idx": { + "name": "auth_session_token_idx", + "columns": ["token"], + "isUnique": true + }, + "auth_session_account_id_idx": { + "name": "auth_session_account_id_idx", + "columns": ["account_id"], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "auth_verification": { + "name": "auth_verification", + "columns": { + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "identifier": { + "name": "identifier", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "auth_verification_expires_at_idx": { + "name": "auth_verification_expires_at_idx", + "columns": ["expires_at"], + "isUnique": false + }, + "auth_verification_identifier_idx": { + "name": "auth_verification_identifier_idx", + "columns": ["identifier"], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "cli_oauth_flow": { + "name": "cli_oauth_flow", + "columns": { + "account_id": { + "name": "account_id", + "type": "text CHECK (\"account_id\" = upper(\"account_id\") AND length(\"account_id\") = 26 AND substr(\"account_id\", 1, 1) GLOB '[0-7]' AND \"account_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "authorized_at": { + "name": "authorized_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "completed_at": { + "name": "completed_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "device_code_hash": { + "name": "device_code_hash", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "hostname": { + "name": "hostname", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "id": { + "name": "id", + "type": "text CHECK (\"id\" = upper(\"id\") AND length(\"id\") = 26 AND substr(\"id\", 1, 1) GLOB '[0-7]' AND \"id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_code": { + "name": "user_code", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "cli_oauth_flow_status_expires_idx": { + "name": "cli_oauth_flow_status_expires_idx", + "columns": ["status", "expires_at"], + "isUnique": false + }, + "cli_oauth_flow_device_code_hash_idx": { + "name": "cli_oauth_flow_device_code_hash_idx", + "columns": ["device_code_hash"], + "isUnique": true + }, + "cli_oauth_flow_user_code_idx": { + "name": "cli_oauth_flow_user_code_idx", + "columns": ["user_code"], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "personal_access_token": { + "name": "personal_access_token", + "columns": { + "account_id": { + "name": "account_id", + "type": "text CHECK (\"account_id\" = upper(\"account_id\") AND length(\"account_id\") = 26 AND substr(\"account_id\", 1, 1) GLOB '[0-7]' AND \"account_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "id": { + "name": "id", + "type": "text CHECK (\"id\" = upper(\"id\") AND length(\"id\") = 26 AND substr(\"id\", 1, 1) GLOB '[0-7]' AND \"id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "last_used_at": { + "name": "last_used_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "revoked_at": { + "name": "revoked_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "token_hash": { + "name": "token_hash", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "personal_access_token_account_created_idx": { + "name": "personal_access_token_account_created_idx", + "columns": ["account_id", "created_at"], + "isUnique": false + }, + "personal_access_token_hash_idx": { + "name": "personal_access_token_hash_idx", + "columns": ["token_hash"], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "email_log": { + "name": "email_log", + "columns": { + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "recipient_domain": { + "name": "recipient_domain", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "recipient_masked": { + "name": "recipient_masked", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "subject": { + "name": "subject", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "email_log_created_at_idx": { + "name": "email_log_created_at_idx", + "columns": ["created_at"], + "isUnique": false + }, + "email_log_type_status_idx": { + "name": "email_log_type_status_idx", + "columns": ["type", "status"], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "environment_revision": { + "name": "environment_revision", + "columns": { + "allow_mcp_servers": { + "name": "allow_mcp_servers", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "allow_package_managers": { + "name": "allow_package_managers", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "allowed_hosts_json": { + "name": "allowed_hosts_json", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_by_account_id": { + "name": "created_by_account_id", + "type": "text CHECK (\"created_by_account_id\" = upper(\"created_by_account_id\") AND length(\"created_by_account_id\") = 26 AND substr(\"created_by_account_id\", 1, 1) GLOB '[0-7]' AND \"created_by_account_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "env_vars_json": { + "name": "env_vars_json", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "environment_id": { + "name": "environment_id", + "type": "text CHECK (\"environment_id\" = upper(\"environment_id\") AND length(\"environment_id\") = 26 AND substr(\"environment_id\", 1, 1) GLOB '[0-7]' AND \"environment_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "id": { + "name": "id", + "type": "text CHECK (\"id\" = upper(\"id\") AND length(\"id\") = 26 AND substr(\"id\", 1, 1) GLOB '[0-7]' AND \"id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "network_policy": { + "name": "network_policy", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "packages_json": { + "name": "packages_json", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "app_id": { + "name": "app_id", + "type": "text CHECK (\"app_id\" = upper(\"app_id\") AND length(\"app_id\") = 26 AND substr(\"app_id\", 1, 1) GLOB '[0-7]' AND \"app_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "setup_script": { + "name": "setup_script", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "environment_revision_environment_created_at_idx": { + "name": "environment_revision_environment_created_at_idx", + "columns": ["environment_id", "created_at"], + "isUnique": false + }, + "environment_revision_app_created_at_idx": { + "name": "environment_revision_app_created_at_idx", + "columns": ["app_id", "created_at"], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": { + "environment_revision_network_policy_check": { + "name": "environment_revision_network_policy_check", + "value": "\"environment_revision\".\"network_policy\" IN ('full', 'limited')" + } + } + }, + "environment": { + "name": "environment", + "columns": { + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "current_revision_id": { + "name": "current_revision_id", + "type": "text CHECK (\"current_revision_id\" = upper(\"current_revision_id\") AND length(\"current_revision_id\") = 26 AND substr(\"current_revision_id\", 1, 1) GLOB '[0-7]' AND \"current_revision_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "forked_from_environment_id": { + "name": "forked_from_environment_id", + "type": "text CHECK (\"forked_from_environment_id\" = upper(\"forked_from_environment_id\") AND length(\"forked_from_environment_id\") = 26 AND substr(\"forked_from_environment_id\", 1, 1) GLOB '[0-7]' AND \"forked_from_environment_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "forked_from_environment_name": { + "name": "forked_from_environment_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "forked_from_owner_name": { + "name": "forked_from_owner_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "id": { + "name": "id", + "type": "text CHECK (\"id\" = upper(\"id\") AND length(\"id\") = 26 AND substr(\"id\", 1, 1) GLOB '[0-7]' AND \"id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "owner_account_id": { + "name": "owner_account_id", + "type": "text CHECK (\"owner_account_id\" = upper(\"owner_account_id\") AND length(\"owner_account_id\") = 26 AND substr(\"owner_account_id\", 1, 1) GLOB '[0-7]' AND \"owner_account_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "app_id": { + "name": "app_id", + "type": "text CHECK (\"app_id\" = upper(\"app_id\") AND length(\"app_id\") = 26 AND substr(\"app_id\", 1, 1) GLOB '[0-7]' AND \"app_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "environment_app_updated_at_idx": { + "name": "environment_app_updated_at_idx", + "columns": ["app_id", "updated_at"], + "isUnique": false + }, + "environment_owner_updated_at_idx": { + "name": "environment_owner_updated_at_idx", + "columns": ["owner_account_id", "updated_at"], + "isUnique": false + }, + "environment_owner_name_idx": { + "name": "environment_owner_name_idx", + "columns": ["app_id", "owner_account_id", "name"], + "isUnique": true, + "where": "\"environment\".\"owner_account_id\" IS NOT NULL" + }, + "environment_system_default_idx": { + "name": "environment_system_default_idx", + "columns": ["app_id"], + "isUnique": true, + "where": "\"environment\".\"owner_account_id\" IS NULL" + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "file_record": { + "name": "file_record", + "columns": { + "committed": { + "name": "committed", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_by_account_id": { + "name": "created_by_account_id", + "type": "text CHECK (\"created_by_account_id\" = upper(\"created_by_account_id\") AND length(\"created_by_account_id\") = 26 AND substr(\"created_by_account_id\", 1, 1) GLOB '[0-7]' AND \"created_by_account_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "etag": { + "name": "etag", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "id": { + "name": "id", + "type": "text CHECK (\"id\" = upper(\"id\") AND length(\"id\") = 26 AND substr(\"id\", 1, 1) GLOB '[0-7]' AND \"id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "mime_type": { + "name": "mime_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "object_key": { + "name": "object_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "owner_id": { + "name": "owner_id", + "type": "text CHECK (\"owner_id\" = upper(\"owner_id\") AND length(\"owner_id\") = 26 AND substr(\"owner_id\", 1, 1) GLOB '[0-7]' AND \"owner_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "owner_kind": { + "name": "owner_kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "parent_path": { + "name": "parent_path", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "path": { + "name": "path", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "purpose": { + "name": "purpose", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "runtime_event_seq": { + "name": "runtime_event_seq", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "scope_id": { + "name": "scope_id", + "type": "text CHECK (\"scope_id\" = upper(\"scope_id\") AND length(\"scope_id\") = 26 AND substr(\"scope_id\", 1, 1) GLOB '[0-7]' AND \"scope_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "scope_kind": { + "name": "scope_kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "session_kind": { + "name": "session_kind", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "size": { + "name": "size", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "version": { + "name": "version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "file_record_runtime_event_seq_idx": { + "name": "file_record_runtime_event_seq_idx", + "columns": ["scope_id", "runtime_event_seq"], + "isUnique": false + }, + "file_record_object_key_idx": { + "name": "file_record_object_key_idx", + "columns": ["object_key"], + "isUnique": true + }, + "file_record_unscoped_parent_path_name_status_idx": { + "name": "file_record_unscoped_parent_path_name_status_idx", + "columns": ["scope_kind", "parent_path", "name", "status"], + "isUnique": true, + "where": "\"file_record\".\"scope_id\" IS NULL" + }, + "file_record_scoped_parent_path_name_status_idx": { + "name": "file_record_scoped_parent_path_name_status_idx", + "columns": ["scope_kind", "scope_id", "parent_path", "name", "status"], + "isUnique": true + }, + "file_record_unscoped_pending_path_idx": { + "name": "file_record_unscoped_pending_path_idx", + "columns": ["scope_kind", "path"], + "isUnique": true, + "where": "\"file_record\".\"status\" = 'pending' AND \"file_record\".\"scope_id\" IS NULL" + }, + "file_record_scoped_pending_path_idx": { + "name": "file_record_scoped_pending_path_idx", + "columns": ["scope_kind", "scope_id", "path"], + "isUnique": true, + "where": "\"file_record\".\"status\" = 'pending' AND \"file_record\".\"scope_id\" IS NOT NULL" + }, + "file_record_unscoped_ready_path_idx": { + "name": "file_record_unscoped_ready_path_idx", + "columns": ["scope_kind", "path"], + "isUnique": true, + "where": "\"file_record\".\"status\" = 'ready' AND \"file_record\".\"scope_id\" IS NULL" + }, + "file_record_scoped_ready_path_idx": { + "name": "file_record_scoped_ready_path_idx", + "columns": ["scope_kind", "scope_id", "path"], + "isUnique": true, + "where": "\"file_record\".\"status\" = 'ready' AND \"file_record\".\"scope_id\" IS NOT NULL" + }, + "file_record_governance_idx": { + "name": "file_record_governance_idx", + "columns": ["purpose", "owner_kind", "owner_id", "status", "expires_at"], + "isUnique": false + }, + "file_record_listing_idx": { + "name": "file_record_listing_idx", + "columns": ["scope_kind", "scope_id", "parent_path", "status", "lower(\"name\")"], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": { + "file_record_runtime_event_seq_check": { + "name": "file_record_runtime_event_seq_check", + "value": "\"file_record\".\"runtime_event_seq\" IS NULL OR \"file_record\".\"runtime_event_seq\" >= 0" + } + } + }, + "file_upload": { + "name": "file_upload", + "columns": { + "content_type": { + "name": "content_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_by_account_id": { + "name": "created_by_account_id", + "type": "text CHECK (\"created_by_account_id\" = upper(\"created_by_account_id\") AND length(\"created_by_account_id\") = 26 AND substr(\"created_by_account_id\", 1, 1) GLOB '[0-7]' AND \"created_by_account_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expected_size": { + "name": "expected_size", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "file_id": { + "name": "file_id", + "type": "text CHECK (\"file_id\" = upper(\"file_id\") AND length(\"file_id\") = 26 AND substr(\"file_id\", 1, 1) GLOB '[0-7]' AND \"file_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "id": { + "name": "id", + "type": "text CHECK (\"id\" = upper(\"id\") AND length(\"id\") = 26 AND substr(\"id\", 1, 1) GLOB '[0-7]' AND \"id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "if_match_etag": { + "name": "if_match_etag", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "multipart_upload_id": { + "name": "multipart_upload_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "overwrite": { + "name": "overwrite", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "part_size": { + "name": "part_size", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "scope_id": { + "name": "scope_id", + "type": "text CHECK (\"scope_id\" = upper(\"scope_id\") AND length(\"scope_id\") = 26 AND substr(\"scope_id\", 1, 1) GLOB '[0-7]' AND \"scope_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "scope_kind": { + "name": "scope_kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "strategy": { + "name": "strategy", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "file_upload_file_id_idx": { + "name": "file_upload_file_id_idx", + "columns": ["file_id"], + "isUnique": true + }, + "file_upload_status_expires_idx": { + "name": "file_upload_status_expires_idx", + "columns": ["status", "expires_at"], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "file_version": { + "name": "file_version", + "columns": { + "committed": { + "name": "committed", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "committed_at": { + "name": "committed_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_by_account_id": { + "name": "created_by_account_id", + "type": "text CHECK (\"created_by_account_id\" = upper(\"created_by_account_id\") AND length(\"created_by_account_id\") = 26 AND substr(\"created_by_account_id\", 1, 1) GLOB '[0-7]' AND \"created_by_account_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "file_id": { + "name": "file_id", + "type": "text CHECK (\"file_id\" = upper(\"file_id\") AND length(\"file_id\") = 26 AND substr(\"file_id\", 1, 1) GLOB '[0-7]' AND \"file_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "id": { + "name": "id", + "type": "text CHECK (\"id\" = upper(\"id\") AND length(\"id\") = 26 AND substr(\"id\", 1, 1) GLOB '[0-7]' AND \"id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "mime_type": { + "name": "mime_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "object_key": { + "name": "object_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "path": { + "name": "path", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "reason": { + "name": "reason", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "scope_id": { + "name": "scope_id", + "type": "text CHECK (\"scope_id\" = upper(\"scope_id\") AND length(\"scope_id\") = 26 AND substr(\"scope_id\", 1, 1) GLOB '[0-7]' AND \"scope_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "scope_kind": { + "name": "scope_kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "size": { + "name": "size", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "source_etag": { + "name": "source_etag", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "source_object_key": { + "name": "source_object_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "version": { + "name": "version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "file_version_object_key_idx": { + "name": "file_version_object_key_idx", + "columns": ["object_key"], + "isUnique": true + }, + "file_version_scope_path_created_idx": { + "name": "file_version_scope_path_created_idx", + "columns": ["scope_kind", "scope_id", "path", "created_at"], + "isUnique": false + }, + "file_version_file_created_idx": { + "name": "file_version_file_created_idx", + "columns": ["file_id", "created_at"], + "isUnique": false + }, + "file_version_pending_idx": { + "name": "file_version_pending_idx", + "columns": ["committed", "created_at"], + "isUnique": false, + "where": "\"file_version\".\"committed\" = 0" + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "runtime_artifact_attempt": { + "name": "runtime_artifact_attempt", + "columns": { + "accepted_event_id": { + "name": "accepted_event_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_by_account_id": { + "name": "created_by_account_id", + "type": "text CHECK (\"created_by_account_id\" = upper(\"created_by_account_id\") AND length(\"created_by_account_id\") = 26 AND substr(\"created_by_account_id\", 1, 1) GLOB '[0-7]' AND \"created_by_account_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "delete_after": { + "name": "delete_after", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "driver_connection_id": { + "name": "driver_connection_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "driver_generation": { + "name": "driver_generation", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "driver_instance_id": { + "name": "driver_instance_id", + "type": "text CHECK (\"driver_instance_id\" = upper(\"driver_instance_id\") AND length(\"driver_instance_id\") = 26 AND substr(\"driver_instance_id\", 1, 1) GLOB '[0-7]' AND \"driver_instance_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "event_type": { + "name": "event_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "manifest_json": { + "name": "manifest_json", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "manifest_sha256": { + "name": "manifest_sha256", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "owned_object_keys_json": { + "name": "owned_object_keys_json", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'[]'" + }, + "run_id": { + "name": "run_id", + "type": "text CHECK (\"run_id\" = upper(\"run_id\") AND length(\"run_id\") = 26 AND substr(\"run_id\", 1, 1) GLOB '[0-7]' AND \"run_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "semantic_hash": { + "name": "semantic_hash", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "session_id": { + "name": "session_id", + "type": "text CHECK (\"session_id\" = upper(\"session_id\") AND length(\"session_id\") = 26 AND substr(\"session_id\", 1, 1) GLOB '[0-7]' AND \"session_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "source_event_id": { + "name": "source_event_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "runtime_artifact_attempt_accepted_event_idx": { + "name": "runtime_artifact_attempt_accepted_event_idx", + "columns": ["accepted_event_id"], + "isUnique": true, + "where": "\"runtime_artifact_attempt\".\"accepted_event_id\" IS NOT NULL" + }, + "runtime_artifact_attempt_cleanup_idx": { + "name": "runtime_artifact_attempt_cleanup_idx", + "columns": ["status", "expires_at", "updated_at", "id"], + "isUnique": false + }, + "runtime_artifact_attempt_session_status_idx": { + "name": "runtime_artifact_attempt_session_status_idx", + "columns": ["session_id", "status", "id"], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": { + "runtime_artifact_attempt_manifest_check": { + "name": "runtime_artifact_attempt_manifest_check", + "value": "(\"runtime_artifact_attempt\".\"manifest_json\" IS NULL AND \"runtime_artifact_attempt\".\"manifest_sha256\" IS NULL) OR (\"runtime_artifact_attempt\".\"manifest_json\" IS NOT NULL AND json_valid(\"runtime_artifact_attempt\".\"manifest_json\") = 1 AND json_extract(\"runtime_artifact_attempt\".\"manifest_json\", '$.version') IS 1 AND json_type(\"runtime_artifact_attempt\".\"manifest_json\", '$.captureStatus') IS 'text' AND json_extract(\"runtime_artifact_attempt\".\"manifest_json\", '$.captureStatus') IN ('complete', 'omitted_file_limit', 'omitted_runtime_unavailable', 'omitted_size_limit', 'omitted_source_changed', 'omitted_source_missing') AND json_type(\"runtime_artifact_attempt\".\"manifest_json\", '$.mode') IS 'text' AND json_extract(\"runtime_artifact_attempt\".\"manifest_json\", '$.mode') IN ('delta', 'snapshot') AND (json_extract(\"runtime_artifact_attempt\".\"manifest_json\", '$.captureStatus') = 'complete' OR json_array_length(\"runtime_artifact_attempt\".\"manifest_json\", '$.files') = 0) AND json_extract(\"runtime_artifact_attempt\".\"manifest_json\", '$.sourceEventId') IS \"runtime_artifact_attempt\".\"source_event_id\" AND json_extract(\"runtime_artifact_attempt\".\"manifest_json\", '$.semanticHash') IS \"runtime_artifact_attempt\".\"semantic_hash\" AND json_type(\"runtime_artifact_attempt\".\"manifest_json\", '$.files') IS 'array' AND \"runtime_artifact_attempt\".\"manifest_sha256\" IS NOT NULL AND length(\"runtime_artifact_attempt\".\"manifest_sha256\") = 64 AND \"runtime_artifact_attempt\".\"manifest_sha256\" = lower(\"runtime_artifact_attempt\".\"manifest_sha256\") AND \"runtime_artifact_attempt\".\"manifest_sha256\" NOT GLOB '*[^0-9a-f]*')" + }, + "runtime_artifact_attempt_owned_keys_check": { + "name": "runtime_artifact_attempt_owned_keys_check", + "value": "json_valid(\"runtime_artifact_attempt\".\"owned_object_keys_json\") = 1 AND json_type(\"runtime_artifact_attempt\".\"owned_object_keys_json\") IS 'array'" + }, + "runtime_artifact_attempt_semantic_hash_check": { + "name": "runtime_artifact_attempt_semantic_hash_check", + "value": "length(\"runtime_artifact_attempt\".\"semantic_hash\") = 64 AND \"runtime_artifact_attempt\".\"semantic_hash\" = lower(\"runtime_artifact_attempt\".\"semantic_hash\") AND \"runtime_artifact_attempt\".\"semantic_hash\" NOT GLOB '*[^0-9a-f]*'" + }, + "runtime_artifact_attempt_status_check": { + "name": "runtime_artifact_attempt_status_check", + "value": "(\"runtime_artifact_attempt\".\"status\" = 'staging' AND \"runtime_artifact_attempt\".\"manifest_json\" IS NULL AND \"runtime_artifact_attempt\".\"accepted_event_id\" IS NULL AND \"runtime_artifact_attempt\".\"expires_at\" IS NOT NULL AND \"runtime_artifact_attempt\".\"delete_after\" IS NULL) OR (\"runtime_artifact_attempt\".\"status\" = 'staged' AND \"runtime_artifact_attempt\".\"manifest_json\" IS NOT NULL AND \"runtime_artifact_attempt\".\"accepted_event_id\" IS NULL AND \"runtime_artifact_attempt\".\"expires_at\" IS NOT NULL AND \"runtime_artifact_attempt\".\"delete_after\" IS NULL) OR (\"runtime_artifact_attempt\".\"status\" = 'accepted' AND \"runtime_artifact_attempt\".\"manifest_json\" IS NOT NULL AND \"runtime_artifact_attempt\".\"accepted_event_id\" IS NOT NULL AND \"runtime_artifact_attempt\".\"expires_at\" IS NULL AND \"runtime_artifact_attempt\".\"delete_after\" IS NULL AND json_array_length(\"runtime_artifact_attempt\".\"owned_object_keys_json\") = 0) OR (\"runtime_artifact_attempt\".\"status\" = 'deleting' AND \"runtime_artifact_attempt\".\"accepted_event_id\" IS NULL AND \"runtime_artifact_attempt\".\"delete_after\" IS NOT NULL)" + }, + "runtime_artifact_attempt_time_check": { + "name": "runtime_artifact_attempt_time_check", + "value": "\"runtime_artifact_attempt\".\"driver_generation\" >= 0 AND (\"runtime_artifact_attempt\".\"expires_at\" IS NULL OR \"runtime_artifact_attempt\".\"expires_at\" >= \"runtime_artifact_attempt\".\"created_at\") AND (\"runtime_artifact_attempt\".\"delete_after\" IS NULL OR \"runtime_artifact_attempt\".\"delete_after\" >= \"runtime_artifact_attempt\".\"created_at\") AND \"runtime_artifact_attempt\".\"updated_at\" >= \"runtime_artifact_attempt\".\"created_at\"" + } + } + }, + "session_artifact_head": { + "name": "session_artifact_head", + "columns": { + "file_id": { + "name": "file_id", + "type": "text CHECK (\"file_id\" = upper(\"file_id\") AND length(\"file_id\") = 26 AND substr(\"file_id\", 1, 1) GLOB '[0-7]' AND \"file_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "runtime_event_seq": { + "name": "runtime_event_seq", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "session_id": { + "name": "session_id", + "type": "text CHECK (\"session_id\" = upper(\"session_id\") AND length(\"session_id\") = 26 AND substr(\"session_id\", 1, 1) GLOB '[0-7]' AND \"session_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "source_event_id": { + "name": "source_event_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "source_path": { + "name": "source_path", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "session_artifact_head_session_path_idx": { + "name": "session_artifact_head_session_path_idx", + "columns": ["session_id", "source_path"], + "isUnique": true + }, + "session_artifact_head_session_seq_idx": { + "name": "session_artifact_head_session_seq_idx", + "columns": ["session_id", "runtime_event_seq", "source_path"], + "isUnique": false + } + }, + "foreignKeys": { + "session_artifact_head_session_id_session_id_fk": { + "name": "session_artifact_head_session_id_session_id_fk", + "tableFrom": "session_artifact_head", + "tableTo": "session", + "columnsFrom": ["session_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": { + "session_artifact_head_path_check": { + "name": "session_artifact_head_path_check", + "value": "length(\"session_artifact_head\".\"source_path\") > 8 AND substr(\"session_artifact_head\".\"source_path\", 1, 8) = 'outputs/' AND instr(\"session_artifact_head\".\"source_path\", char(0)) = 0 AND instr(\"session_artifact_head\".\"source_path\", '\\') = 0 AND \"session_artifact_head\".\"source_path\" NOT LIKE '%//%' AND \"session_artifact_head\".\"source_path\" NOT LIKE '%/./%' AND \"session_artifact_head\".\"source_path\" NOT LIKE '%/.' AND \"session_artifact_head\".\"source_path\" NOT LIKE '%/../%' AND \"session_artifact_head\".\"source_path\" NOT LIKE '%/..'" + }, + "session_artifact_head_seq_check": { + "name": "session_artifact_head_seq_check", + "value": "\"session_artifact_head\".\"runtime_event_seq\" >= 0 AND \"session_artifact_head\".\"updated_at\" >= 0" + } + } + }, + "mcp_credential": { + "name": "mcp_credential", + "columns": { + "account_id": { + "name": "account_id", + "type": "text CHECK (\"account_id\" = upper(\"account_id\") AND length(\"account_id\") = 26 AND substr(\"account_id\", 1, 1) GLOB '[0-7]' AND \"account_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "agent_id": { + "name": "agent_id", + "type": "text CHECK (\"agent_id\" = upper(\"agent_id\") AND length(\"agent_id\") = 26 AND substr(\"agent_id\", 1, 1) GLOB '[0-7]' AND \"agent_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "auth_type": { + "name": "auth_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "id": { + "name": "id", + "type": "text CHECK (\"id\" = upper(\"id\") AND length(\"id\") = 26 AND substr(\"id\", 1, 1) GLOB '[0-7]' AND \"id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "last_refreshed_at": { + "name": "last_refreshed_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "oauth_client_id": { + "name": "oauth_client_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "oauth_client_secret_secret_id": { + "name": "oauth_client_secret_secret_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "app_id": { + "name": "app_id", + "type": "text CHECK (\"app_id\" = upper(\"app_id\") AND length(\"app_id\") = 26 AND substr(\"app_id\", 1, 1) GLOB '[0-7]' AND \"app_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "refresh_secret_id": { + "name": "refresh_secret_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "scope_values_json": { + "name": "scope_values_json", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "secret_id": { + "name": "secret_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "server_id": { + "name": "server_id", + "type": "text CHECK (\"server_id\" = upper(\"server_id\") AND length(\"server_id\") = 26 AND substr(\"server_id\", 1, 1) GLOB '[0-7]' AND \"server_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "subject_label": { + "name": "subject_label", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "mcp_credential_server_scope_status_idx": { + "name": "mcp_credential_server_scope_status_idx", + "columns": ["server_id", "scope", "status"], + "isUnique": false + }, + "mcp_credential_app_scope_status_idx": { + "name": "mcp_credential_app_scope_status_idx", + "columns": ["app_id", "scope", "status"], + "isUnique": false + }, + "mcp_credential_app_scope_idx": { + "name": "mcp_credential_app_scope_idx", + "columns": ["server_id", "scope"], + "isUnique": true, + "where": "\"mcp_credential\".\"scope\" = 'app'" + }, + "mcp_credential_agent_scope_idx": { + "name": "mcp_credential_agent_scope_idx", + "columns": ["server_id", "agent_id", "scope"], + "isUnique": true, + "where": "\"mcp_credential\".\"scope\" = 'agent' AND \"mcp_credential\".\"agent_id\" IS NOT NULL" + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": { + "mcp_credential_scope_shape_check": { + "name": "mcp_credential_scope_shape_check", + "value": "\n (\"mcp_credential\".\"scope\" = 'app' AND \"mcp_credential\".\"account_id\" IS NULL AND \"mcp_credential\".\"agent_id\" IS NULL)\n OR (\"mcp_credential\".\"scope\" = 'agent' AND \"mcp_credential\".\"account_id\" IS NULL AND \"mcp_credential\".\"agent_id\" IS NOT NULL)\n " + }, + "mcp_credential_scope_values_json_check": { + "name": "mcp_credential_scope_values_json_check", + "value": "\n \"mcp_credential\".\"scope_values_json\" IS NULL\n OR (json_valid(\"mcp_credential\".\"scope_values_json\") AND json_type(\"mcp_credential\".\"scope_values_json\") = 'array')\n " + }, + "mcp_credential_bearer_shape_check": { + "name": "mcp_credential_bearer_shape_check", + "value": "\n \"mcp_credential\".\"auth_type\" != 'bearer'\n OR (\n \"mcp_credential\".\"oauth_client_id\" IS NULL\n AND \"mcp_credential\".\"oauth_client_secret_secret_id\" IS NULL\n AND \"mcp_credential\".\"refresh_secret_id\" IS NULL\n )\n " + } + } + }, + "mcp_oauth_flow": { + "name": "mcp_oauth_flow", + "columns": { + "authorization_endpoint": { + "name": "authorization_endpoint", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "cleanup_after": { + "name": "cleanup_after", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "code_verifier": { + "name": "code_verifier", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "completed_at": { + "name": "completed_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "id": { + "name": "id", + "type": "text CHECK (\"id\" = upper(\"id\") AND length(\"id\") = 26 AND substr(\"id\", 1, 1) GLOB '[0-7]' AND \"id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "initiator_account_id": { + "name": "initiator_account_id", + "type": "text CHECK (\"initiator_account_id\" = upper(\"initiator_account_id\") AND length(\"initiator_account_id\") = 26 AND substr(\"initiator_account_id\", 1, 1) GLOB '[0-7]' AND \"initiator_account_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "oauth_client_id": { + "name": "oauth_client_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "oauth_client_secret_secret_id": { + "name": "oauth_client_secret_secret_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "app_id": { + "name": "app_id", + "type": "text CHECK (\"app_id\" = upper(\"app_id\") AND length(\"app_id\") = 26 AND substr(\"app_id\", 1, 1) GLOB '[0-7]' AND \"app_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "registration_endpoint": { + "name": "registration_endpoint", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "return_url": { + "name": "return_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "scope_values_json": { + "name": "scope_values_json", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "server_id": { + "name": "server_id", + "type": "text CHECK (\"server_id\" = upper(\"server_id\") AND length(\"server_id\") = 26 AND substr(\"server_id\", 1, 1) GLOB '[0-7]' AND \"server_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "subject_label": { + "name": "subject_label", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "token_endpoint": { + "name": "token_endpoint", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "mcp_oauth_flow_status_cleanup_after_idx": { + "name": "mcp_oauth_flow_status_cleanup_after_idx", + "columns": ["status", "cleanup_after"], + "isUnique": false + }, + "mcp_oauth_flow_expires_at_idx": { + "name": "mcp_oauth_flow_expires_at_idx", + "columns": ["expires_at"], + "isUnique": false + }, + "mcp_oauth_flow_server_account_idx": { + "name": "mcp_oauth_flow_server_account_idx", + "columns": ["server_id", "initiator_account_id"], + "isUnique": false + }, + "mcp_oauth_flow_app_server_account_idx": { + "name": "mcp_oauth_flow_app_server_account_idx", + "columns": ["app_id", "server_id", "initiator_account_id"], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": { + "mcp_oauth_flow_scope_values_json_check": { + "name": "mcp_oauth_flow_scope_values_json_check", + "value": "\n \"mcp_oauth_flow\".\"scope_values_json\" IS NULL\n OR (json_valid(\"mcp_oauth_flow\".\"scope_values_json\") AND json_type(\"mcp_oauth_flow\".\"scope_values_json\") = 'array')\n " + } + } + }, + "mcp_server": { + "name": "mcp_server", + "columns": { + "auth_type": { + "name": "auth_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "byo_client_id": { + "name": "byo_client_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "byo_client_secret_secret_id": { + "name": "byo_client_secret_secret_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "credential_scope": { + "name": "credential_scope", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "enabled": { + "name": "enabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "icon_url": { + "name": "icon_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "id": { + "name": "id", + "type": "text CHECK (\"id\" = upper(\"id\") AND length(\"id\") = 26 AND substr(\"id\", 1, 1) GLOB '[0-7]' AND \"id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "oauth_metadata_json": { + "name": "oauth_metadata_json", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "owner_account_id": { + "name": "owner_account_id", + "type": "text CHECK (\"owner_account_id\" = upper(\"owner_account_id\") AND length(\"owner_account_id\") = 26 AND substr(\"owner_account_id\", 1, 1) GLOB '[0-7]' AND \"owner_account_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "app_id": { + "name": "app_id", + "type": "text CHECK (\"app_id\" = upper(\"app_id\") AND length(\"app_id\") = 26 AND substr(\"app_id\", 1, 1) GLOB '[0-7]' AND \"app_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "mcp_server_app_enabled_idx": { + "name": "mcp_server_app_enabled_idx", + "columns": ["app_id", "enabled"], + "isUnique": false + }, + "mcp_server_owner_app_idx": { + "name": "mcp_server_owner_app_idx", + "columns": ["owner_account_id", "app_id"], + "isUnique": false + }, + "mcp_server_app_url_idx": { + "name": "mcp_server_app_url_idx", + "columns": ["app_id", "url"], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": { + "mcp_server_source_scope_check": { + "name": "mcp_server_source_scope_check", + "value": "\"mcp_server\".\"source\" = 'app' AND \"mcp_server\".\"credential_scope\" = 'app'" + } + } + }, + "vault_secret": { + "name": "vault_secret", + "columns": { + "algorithm": { + "name": "algorithm", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'AES-GCM'" + }, + "ciphertext": { + "name": "ciphertext", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "ciphertext_iv": { + "name": "ciphertext_iv", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "id": { + "name": "id", + "type": "text CHECK (\"id\" = upper(\"id\") AND length(\"id\") = 26 AND substr(\"id\", 1, 1) GLOB '[0-7]' AND \"id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "wrapped_dek": { + "name": "wrapped_dek", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "wrapped_dek_iv": { + "name": "wrapped_dek_iv", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "vault_secret_kind_created_at_idx": { + "name": "vault_secret_kind_created_at_idx", + "columns": ["kind", "created_at"], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "organization": { + "name": "organization", + "columns": { + "avatar_url": { + "name": "avatar_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "creator_account_id": { + "name": "creator_account_id", + "type": "text CHECK (\"creator_account_id\" = upper(\"creator_account_id\") AND length(\"creator_account_id\") = 26 AND substr(\"creator_account_id\", 1, 1) GLOB '[0-7]' AND \"creator_account_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "id": { + "name": "id", + "type": "text CHECK (\"id\" = upper(\"id\") AND length(\"id\") = 26 AND substr(\"id\", 1, 1) GLOB '[0-7]' AND \"id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "organization_creator_account_idx": { + "name": "organization_creator_account_idx", + "columns": ["creator_account_id"], + "isUnique": true, + "where": "\"organization\".\"creator_account_id\" IS NOT NULL" + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "app_deployment_run": { + "name": "app_deployment_run", + "columns": { + "app_id": { + "name": "app_id", + "type": "text CHECK (\"app_id\" = upper(\"app_id\") AND length(\"app_id\") = 26 AND substr(\"app_id\", 1, 1) GLOB '[0-7]' AND \"app_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "deployment_id": { + "name": "deployment_id", + "type": "text CHECK (\"deployment_id\" = upper(\"deployment_id\") AND length(\"deployment_id\") = 26 AND substr(\"deployment_id\", 1, 1) GLOB '[0-7]' AND \"deployment_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "error_code": { + "name": "error_code", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "external_deployment_id": { + "name": "external_deployment_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "external_project_id": { + "name": "external_project_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "external_version_id": { + "name": "external_version_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "generated_wrangler_config_json": { + "name": "generated_wrangler_config_json", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "id": { + "name": "id", + "type": "text CHECK (\"id\" = upper(\"id\") AND length(\"id\") = 26 AND substr(\"id\", 1, 1) GLOB '[0-7]' AND \"id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "mosoo_config_json": { + "name": "mosoo_config_json", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "plan_json": { + "name": "plan_json", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "source_branch": { + "name": "source_branch", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "source_commit_sha": { + "name": "source_commit_sha", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "target_kind": { + "name": "target_kind", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "target_project_name": { + "name": "target_project_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "target_script_name": { + "name": "target_script_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "app_deployment_run_app_id_idx": { + "name": "app_deployment_run_app_id_idx", + "columns": ["app_id", "id"], + "isUnique": false + }, + "app_deployment_run_deployment_id_idx": { + "name": "app_deployment_run_deployment_id_idx", + "columns": ["deployment_id", "id"], + "isUnique": false + }, + "app_deployment_run_active_app_idx": { + "name": "app_deployment_run_active_app_idx", + "columns": ["app_id"], + "isUnique": true, + "where": "\"app_deployment_run\".\"status\" IN ('queued', 'preparing', 'building', 'submitting', 'submitted', 'activating')" + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": { + "app_deployment_run_status_check": { + "name": "app_deployment_run_status_check", + "value": "\"app_deployment_run\".\"status\" IN ('queued', 'preparing', 'building', 'submitting', 'submitted', 'activating', 'success', 'failed')" + }, + "app_deployment_run_target_kind_check": { + "name": "app_deployment_run_target_kind_check", + "value": "\"app_deployment_run\".\"target_kind\" IS NULL OR \"app_deployment_run\".\"target_kind\" IN ('cloudflare_pages', 'cloudflare_worker')" + } + } + }, + "app_deployment_secret": { + "name": "app_deployment_secret", + "columns": { + "app_id": { + "name": "app_id", + "type": "text CHECK (\"app_id\" = upper(\"app_id\") AND length(\"app_id\") = 26 AND substr(\"app_id\", 1, 1) GLOB '[0-7]' AND \"app_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "vault_secret_id": { + "name": "vault_secret_id", + "type": "text CHECK (\"vault_secret_id\" = upper(\"vault_secret_id\") AND length(\"vault_secret_id\") = 26 AND substr(\"vault_secret_id\", 1, 1) GLOB '[0-7]' AND \"vault_secret_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "app_deployment_secret_app_name_idx": { + "name": "app_deployment_secret_app_name_idx", + "columns": ["app_id", "name"], + "isUnique": true + }, + "app_deployment_secret_vault_secret_idx": { + "name": "app_deployment_secret_vault_secret_idx", + "columns": ["vault_secret_id"], + "isUnique": true + } + }, + "foreignKeys": { + "app_deployment_secret_vault_secret_id_vault_secret_id_fk": { + "name": "app_deployment_secret_vault_secret_id_vault_secret_id_fk", + "tableFrom": "app_deployment_secret", + "tableTo": "vault_secret", + "columnsFrom": ["vault_secret_id"], + "columnsTo": ["id"], + "onDelete": "restrict", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "app_deployment": { + "name": "app_deployment", + "columns": { + "app_id": { + "name": "app_id", + "type": "text CHECK (\"app_id\" = upper(\"app_id\") AND length(\"app_id\") = 26 AND substr(\"app_id\", 1, 1) GLOB '[0-7]' AND \"app_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "default_branch": { + "name": "default_branch", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "deleted_at": { + "name": "deleted_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "id": { + "name": "id", + "type": "text CHECK (\"id\" = upper(\"id\") AND length(\"id\") = 26 AND substr(\"id\", 1, 1) GLOB '[0-7]' AND \"id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "last_successful_url": { + "name": "last_successful_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "latest_run_id": { + "name": "latest_run_id", + "type": "text CHECK (\"latest_run_id\" = upper(\"latest_run_id\") AND length(\"latest_run_id\") = 26 AND substr(\"latest_run_id\", 1, 1) GLOB '[0-7]' AND \"latest_run_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "mosoo_subdomain": { + "name": "mosoo_subdomain", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "owner_account_id": { + "name": "owner_account_id", + "type": "text CHECK (\"owner_account_id\" = upper(\"owner_account_id\") AND length(\"owner_account_id\") = 26 AND substr(\"owner_account_id\", 1, 1) GLOB '[0-7]' AND \"owner_account_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "repo_name": { + "name": "repo_name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "repo_owner": { + "name": "repo_owner", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "repo_url": { + "name": "repo_url", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "source_kind": { + "name": "source_kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "app_deployment_active_app_idx": { + "name": "app_deployment_active_app_idx", + "columns": ["app_id"], + "isUnique": true, + "where": "\"app_deployment\".\"deleted_at\" IS NULL" + }, + "app_deployment_active_subdomain_idx": { + "name": "app_deployment_active_subdomain_idx", + "columns": ["mosoo_subdomain"], + "isUnique": true, + "where": "\"app_deployment\".\"deleted_at\" IS NULL" + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": { + "app_deployment_source_kind_check": { + "name": "app_deployment_source_kind_check", + "value": "\"app_deployment\".\"source_kind\" IN ('github_public')" + } + } + }, + "app": { + "name": "app", + "columns": { + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "default_environment_id": { + "name": "default_environment_id", + "type": "text CHECK (\"default_environment_id\" = upper(\"default_environment_id\") AND length(\"default_environment_id\") = 26 AND substr(\"default_environment_id\", 1, 1) GLOB '[0-7]' AND \"default_environment_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "id": { + "name": "id", + "type": "text CHECK (\"id\" = upper(\"id\") AND length(\"id\") = 26 AND substr(\"id\", 1, 1) GLOB '[0-7]' AND \"id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "organization_id": { + "name": "organization_id", + "type": "text CHECK (\"organization_id\" = upper(\"organization_id\") AND length(\"organization_id\") = 26 AND substr(\"organization_id\", 1, 1) GLOB '[0-7]' AND \"organization_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "owner_account_id": { + "name": "owner_account_id", + "type": "text CHECK (\"owner_account_id\" = upper(\"owner_account_id\") AND length(\"owner_account_id\") = 26 AND substr(\"owner_account_id\", 1, 1) GLOB '[0-7]' AND \"owner_account_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "bound_agent_call_idempotency_key": { + "name": "bound_agent_call_idempotency_key", + "columns": { + "body_hash": { + "name": "body_hash", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "id": { + "name": "id", + "type": "text CHECK (\"id\" = upper(\"id\") AND length(\"id\") = 26 AND substr(\"id\", 1, 1) GLOB '[0-7]' AND \"id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "idempotency_key": { + "name": "idempotency_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "run_id": { + "name": "run_id", + "type": "text CHECK (\"run_id\" = upper(\"run_id\") AND length(\"run_id\") = 26 AND substr(\"run_id\", 1, 1) GLOB '[0-7]' AND \"run_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "session_id": { + "name": "session_id", + "type": "text CHECK (\"session_id\" = upper(\"session_id\") AND length(\"session_id\") = 26 AND substr(\"session_id\", 1, 1) GLOB '[0-7]' AND \"session_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "subject_hash": { + "name": "subject_hash", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "bound_agent_call_idempotency_subject_key_idx": { + "name": "bound_agent_call_idempotency_subject_key_idx", + "columns": ["subject_hash", "idempotency_key"], + "isUnique": true + }, + "bound_agent_call_idempotency_updated_idx": { + "name": "bound_agent_call_idempotency_updated_idx", + "columns": ["updated_at"], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "public_api_idempotency_key": { + "name": "public_api_idempotency_key", + "columns": { + "body_hash": { + "name": "body_hash", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "id": { + "name": "id", + "type": "text CHECK (\"id\" = upper(\"id\") AND length(\"id\") = 26 AND substr(\"id\", 1, 1) GLOB '[0-7]' AND \"id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "idempotency_key": { + "name": "idempotency_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "method": { + "name": "method", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "response_json": { + "name": "response_json", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "response_status": { + "name": "response_status", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "route": { + "name": "route", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "token_id": { + "name": "token_id", + "type": "text CHECK (\"token_id\" = upper(\"token_id\") AND length(\"token_id\") = 26 AND substr(\"token_id\", 1, 1) GLOB '[0-7]' AND \"token_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "public_api_idempotency_token_key_idx": { + "name": "public_api_idempotency_token_key_idx", + "columns": ["token_id", "idempotency_key"], + "isUnique": true + }, + "public_api_idempotency_updated_idx": { + "name": "public_api_idempotency_updated_idx", + "columns": ["updated_at"], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "public_api_rate_limit_window": { + "name": "public_api_rate_limit_window", + "columns": { + "bucket_key": { + "name": "bucket_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "request_count": { + "name": "request_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "shard": { + "name": "shard", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "window_start": { + "name": "window_start", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "public_api_rate_limit_window_updated_idx": { + "name": "public_api_rate_limit_window_updated_idx", + "columns": ["updated_at"], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "public_api_rate_limit_window_bucket_key_window_start_shard_pk": { + "columns": ["bucket_key", "window_start", "shard"], + "name": "public_api_rate_limit_window_bucket_key_window_start_shard_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "driver_command": { + "name": "driver_command", + "columns": { + "acked_at": { + "name": "acked_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "completed_at": { + "name": "completed_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "delivery_connection_id": { + "name": "delivery_connection_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "driver_generation": { + "name": "driver_generation", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "driver_instance_id": { + "name": "driver_instance_id", + "type": "text CHECK (\"driver_instance_id\" = upper(\"driver_instance_id\") AND length(\"driver_instance_id\") = 26 AND substr(\"driver_instance_id\", 1, 1) GLOB '[0-7]' AND \"driver_instance_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "error_json": { + "name": "error_json", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "id": { + "name": "id", + "type": "text CHECK (\"id\" = upper(\"id\") AND length(\"id\") = 26 AND substr(\"id\", 1, 1) GLOB '[0-7]' AND \"id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "issued_at": { + "name": "issued_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "payload_json": { + "name": "payload_json", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "result_json": { + "name": "result_json", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "seq": { + "name": "seq", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "driver_command_instance_seq_idx": { + "name": "driver_command_instance_seq_idx", + "columns": ["driver_instance_id", "seq"], + "isUnique": true + }, + "driver_command_instance_status_idx": { + "name": "driver_command_instance_status_idx", + "columns": ["driver_instance_id", "status", "expires_at"], + "isUnique": false + } + }, + "foreignKeys": { + "driver_command_driver_instance_id_driver_instance_id_fk": { + "name": "driver_command_driver_instance_id_driver_instance_id_fk", + "tableFrom": "driver_command", + "tableTo": "driver_instance", + "columnsFrom": ["driver_instance_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": { + "driver_command_generation_check": { + "name": "driver_command_generation_check", + "value": "\"driver_command\".\"driver_generation\" IS NULL OR (typeof(\"driver_command\".\"driver_generation\") = 'integer' AND \"driver_command\".\"driver_generation\" BETWEEN 0 AND 9007199254740991)" + }, + "driver_command_nonterminal_generation_check": { + "name": "driver_command_nonterminal_generation_check", + "value": "\"driver_command\".\"status\" IN ('completed', 'failed', 'expired', 'cancelled') OR \"driver_command\".\"driver_generation\" IS NOT NULL" + } + } + }, + "driver_instance_mcp_grant": { + "name": "driver_instance_mcp_grant", + "columns": { + "auth_type": { + "name": "auth_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "authorization_state": { + "name": "authorization_state", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "can_invalidate": { + "name": "can_invalidate", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "can_refresh": { + "name": "can_refresh", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "credential_id": { + "name": "credential_id", + "type": "text CHECK (\"credential_id\" = upper(\"credential_id\") AND length(\"credential_id\") = 26 AND substr(\"credential_id\", 1, 1) GLOB '[0-7]' AND \"credential_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "driver_instance_id": { + "name": "driver_instance_id", + "type": "text CHECK (\"driver_instance_id\" = upper(\"driver_instance_id\") AND length(\"driver_instance_id\") = 26 AND substr(\"driver_instance_id\", 1, 1) GLOB '[0-7]' AND \"driver_instance_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "app_id": { + "name": "app_id", + "type": "text CHECK (\"app_id\" = upper(\"app_id\") AND length(\"app_id\") = 26 AND substr(\"app_id\", 1, 1) GLOB '[0-7]' AND \"app_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "server_id": { + "name": "server_id", + "type": "text CHECK (\"server_id\" = upper(\"server_id\") AND length(\"server_id\") = 26 AND substr(\"server_id\", 1, 1) GLOB '[0-7]' AND \"server_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "driver_instance_mcp_grant_instance_server_idx": { + "name": "driver_instance_mcp_grant_instance_server_idx", + "columns": ["driver_instance_id", "server_id"], + "isUnique": true + }, + "driver_instance_mcp_grant_instance_credential_idx": { + "name": "driver_instance_mcp_grant_instance_credential_idx", + "columns": ["driver_instance_id", "credential_id"], + "isUnique": false + } + }, + "foreignKeys": { + "driver_instance_mcp_grant_driver_instance_id_driver_instance_id_fk": { + "name": "driver_instance_mcp_grant_driver_instance_id_driver_instance_id_fk", + "tableFrom": "driver_instance_mcp_grant", + "tableTo": "driver_instance", + "columnsFrom": ["driver_instance_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "driver_instance": { + "name": "driver_instance", + "columns": { + "boot_token_expires_at": { + "name": "boot_token_expires_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "boot_token_hash": { + "name": "boot_token_hash", + "type": "blob", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "boot_token_used_at": { + "name": "boot_token_used_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "close_code": { + "name": "close_code", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "close_reason": { + "name": "close_reason", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "connection_id": { + "name": "connection_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "command_seq_cursor": { + "name": "command_seq_cursor", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "driver_pid": { + "name": "driver_pid", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "driver_started_at": { + "name": "driver_started_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "driver_version": { + "name": "driver_version", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "heartbeat_count": { + "name": "heartbeat_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "generation": { + "name": "generation", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "id": { + "name": "id", + "type": "text CHECK (\"id\" = upper(\"id\") AND length(\"id\") = 26 AND substr(\"id\", 1, 1) GLOB '[0-7]' AND \"id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "last_heartbeat_at": { + "name": "last_heartbeat_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "process_id": { + "name": "process_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "protocol": { + "name": "protocol", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "protocol_version": { + "name": "protocol_version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "restart_count": { + "name": "restart_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "runtime": { + "name": "runtime", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "sandbox_id": { + "name": "sandbox_id", + "type": "text CHECK (\"sandbox_id\" = upper(\"sandbox_id\") AND length(\"sandbox_id\") = 26 AND substr(\"sandbox_id\", 1, 1) GLOB '[0-7]' AND \"sandbox_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "sandbox_session_id": { + "name": "sandbox_session_id", + "type": "text CHECK (\"sandbox_session_id\" = upper(\"sandbox_session_id\") AND length(\"sandbox_session_id\") = 26 AND substr(\"sandbox_session_id\", 1, 1) GLOB '[0-7]' AND \"sandbox_session_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status_changed_at": { + "name": "status_changed_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "status_event": { + "name": "status_event", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'driver.provision'" + }, + "status_operation_id": { + "name": "status_operation_id", + "type": "text CHECK (\"status_operation_id\" = upper(\"status_operation_id\") AND length(\"status_operation_id\") = 26 AND substr(\"status_operation_id\", 1, 1) GLOB '[0-7]' AND \"status_operation_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status_seq": { + "name": "status_seq", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "status_source": { + "name": "status_source", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'system'" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "driver_instance_completed_idx": { + "name": "driver_instance_completed_idx", + "columns": ["expires_at", "status"], + "isUnique": false + }, + "driver_instance_connection_idx": { + "name": "driver_instance_connection_idx", + "columns": ["connection_id"], + "isUnique": true, + "where": "\"driver_instance\".\"connection_id\" IS NOT NULL" + }, + "driver_instance_boot_token_expiry_idx": { + "name": "driver_instance_boot_token_expiry_idx", + "columns": ["status", "boot_token_expires_at"], + "isUnique": false, + "where": "\"driver_instance\".\"boot_token_used_at\" IS NULL" + }, + "driver_instance_boot_token_hash_idx": { + "name": "driver_instance_boot_token_hash_idx", + "columns": ["boot_token_hash"], + "isUnique": true + }, + "driver_instance_sandbox_session_idx": { + "name": "driver_instance_sandbox_session_idx", + "columns": ["sandbox_id", "sandbox_session_id", "status", "updated_at"], + "isUnique": false + }, + "driver_instance_live_sandbox_session_idx": { + "name": "driver_instance_live_sandbox_session_idx", + "columns": ["sandbox_id", "sandbox_session_id"], + "isUnique": true, + "where": "\"driver_instance\".\"status\" IN ('provisioning', 'connecting', 'ready', 'stopping')" + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": { + "driver_instance_status_check": { + "name": "driver_instance_status_check", + "value": "\"driver_instance\".\"status\" IN ('provisioning', 'connecting', 'ready', 'stopping', 'stopped', 'failed')" + }, + "driver_instance_status_seq_check": { + "name": "driver_instance_status_seq_check", + "value": "\"driver_instance\".\"status_seq\" >= 0" + } + } + }, + "external_tool_effect_attempt": { + "name": "external_tool_effect_attempt", + "columns": { + "attempt": { + "name": "attempt", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "claim_token": { + "name": "claim_token", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "completed_at": { + "name": "completed_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "effect_id": { + "name": "effect_id", + "type": "text CHECK (\"effect_id\" = upper(\"effect_id\") AND length(\"effect_id\") = 26 AND substr(\"effect_id\", 1, 1) GLOB '[0-7]' AND \"effect_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "provider_receipt_json": { + "name": "provider_receipt_json", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "result_json": { + "name": "result_json", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "external_tool_effect_attempt_status_idx": { + "name": "external_tool_effect_attempt_status_idx", + "columns": ["status", "created_at"], + "isUnique": false + } + }, + "foreignKeys": { + "external_tool_effect_attempt_effect_id_external_tool_effect_id_fk": { + "name": "external_tool_effect_attempt_effect_id_external_tool_effect_id_fk", + "tableFrom": "external_tool_effect_attempt", + "tableTo": "external_tool_effect", + "columnsFrom": ["effect_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "external_tool_effect_attempt_effect_id_attempt_pk": { + "columns": ["effect_id", "attempt"], + "name": "external_tool_effect_attempt_effect_id_attempt_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": { + "external_tool_effect_attempt_status_check": { + "name": "external_tool_effect_attempt_status_check", + "value": "\"external_tool_effect_attempt\".\"status\" IN ('claimed', 'succeeded', 'unknown')" + }, + "external_tool_effect_attempt_claim_token_uuid_check": { + "name": "external_tool_effect_attempt_claim_token_uuid_check", + "value": "length(\"external_tool_effect_attempt\".\"claim_token\") = 36 AND length(replace(\"external_tool_effect_attempt\".\"claim_token\", '-', '')) = 32 AND \"external_tool_effect_attempt\".\"claim_token\" = lower(\"external_tool_effect_attempt\".\"claim_token\") AND substr(\"external_tool_effect_attempt\".\"claim_token\", 9, 1) = '-' AND substr(\"external_tool_effect_attempt\".\"claim_token\", 14, 1) = '-' AND substr(\"external_tool_effect_attempt\".\"claim_token\", 15, 1) = '4' AND substr(\"external_tool_effect_attempt\".\"claim_token\", 19, 1) = '-' AND substr(\"external_tool_effect_attempt\".\"claim_token\", 20, 1) GLOB '[89ab]' AND substr(\"external_tool_effect_attempt\".\"claim_token\", 24, 1) = '-' AND replace(\"external_tool_effect_attempt\".\"claim_token\", '-', '') NOT GLOB '*[^0-9a-f]*'" + } + } + }, + "external_tool_effect": { + "name": "external_tool_effect", + "columns": { + "attempt_count": { + "name": "attempt_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "claim_token": { + "name": "claim_token", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "command_id": { + "name": "command_id", + "type": "text CHECK (\"command_id\" = upper(\"command_id\") AND length(\"command_id\") = 26 AND substr(\"command_id\", 1, 1) GLOB '[0-7]' AND \"command_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "driver_instance_id": { + "name": "driver_instance_id", + "type": "text CHECK (\"driver_instance_id\" = upper(\"driver_instance_id\") AND length(\"driver_instance_id\") = 26 AND substr(\"driver_instance_id\", 1, 1) GLOB '[0-7]' AND \"driver_instance_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "id": { + "name": "id", + "type": "text CHECK (\"id\" = upper(\"id\") AND length(\"id\") = 26 AND substr(\"id\", 1, 1) GLOB '[0-7]' AND \"id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "idempotency_key": { + "name": "idempotency_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "provider_receipt_json": { + "name": "provider_receipt_json", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "result_json": { + "name": "result_json", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "server_id": { + "name": "server_id", + "type": "text CHECK (\"server_id\" = upper(\"server_id\") AND length(\"server_id\") = 26 AND substr(\"server_id\", 1, 1) GLOB '[0-7]' AND \"server_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "session_run_id": { + "name": "session_run_id", + "type": "text CHECK (\"session_run_id\" = upper(\"session_run_id\") AND length(\"session_run_id\") = 26 AND substr(\"session_run_id\", 1, 1) GLOB '[0-7]' AND \"session_run_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "tool_name": { + "name": "tool_name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "external_tool_effect_command_idx": { + "name": "external_tool_effect_command_idx", + "columns": ["command_id"], + "isUnique": true + }, + "external_tool_effect_idempotency_key_idx": { + "name": "external_tool_effect_idempotency_key_idx", + "columns": ["idempotency_key"], + "isUnique": true + }, + "external_tool_effect_run_status_idx": { + "name": "external_tool_effect_run_status_idx", + "columns": ["session_run_id", "status", "id"], + "isUnique": false + }, + "external_tool_effect_driver_status_idx": { + "name": "external_tool_effect_driver_status_idx", + "columns": ["driver_instance_id", "status"], + "isUnique": false + } + }, + "foreignKeys": { + "external_tool_effect_command_id_driver_command_id_fk": { + "name": "external_tool_effect_command_id_driver_command_id_fk", + "tableFrom": "external_tool_effect", + "tableTo": "driver_command", + "columnsFrom": ["command_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "external_tool_effect_driver_instance_id_driver_instance_id_fk": { + "name": "external_tool_effect_driver_instance_id_driver_instance_id_fk", + "tableFrom": "external_tool_effect", + "tableTo": "driver_instance", + "columnsFrom": ["driver_instance_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "external_tool_effect_session_run_id_session_run_id_fk": { + "name": "external_tool_effect_session_run_id_session_run_id_fk", + "tableFrom": "external_tool_effect", + "tableTo": "session_run", + "columnsFrom": ["session_run_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": { + "external_tool_effect_status_check": { + "name": "external_tool_effect_status_check", + "value": "\"external_tool_effect\".\"status\" IN ('intent', 'claimed', 'succeeded', 'unknown')" + }, + "external_tool_effect_claim_token_uuid_check": { + "name": "external_tool_effect_claim_token_uuid_check", + "value": "\"external_tool_effect\".\"claim_token\" IS NULL OR (length(\"external_tool_effect\".\"claim_token\") = 36 AND length(replace(\"external_tool_effect\".\"claim_token\", '-', '')) = 32 AND \"external_tool_effect\".\"claim_token\" = lower(\"external_tool_effect\".\"claim_token\") AND substr(\"external_tool_effect\".\"claim_token\", 9, 1) = '-' AND substr(\"external_tool_effect\".\"claim_token\", 14, 1) = '-' AND substr(\"external_tool_effect\".\"claim_token\", 15, 1) = '4' AND substr(\"external_tool_effect\".\"claim_token\", 19, 1) = '-' AND substr(\"external_tool_effect\".\"claim_token\", 20, 1) GLOB '[89ab]' AND substr(\"external_tool_effect\".\"claim_token\", 24, 1) = '-' AND replace(\"external_tool_effect\".\"claim_token\", '-', '') NOT GLOB '*[^0-9a-f]*')" + } + } + }, + "native_resume_ref": { + "name": "native_resume_ref", + "columns": { + "committed_session_run_id": { + "name": "committed_session_run_id", + "type": "text CHECK (\"committed_session_run_id\" = upper(\"committed_session_run_id\") AND length(\"committed_session_run_id\") = 26 AND substr(\"committed_session_run_id\", 1, 1) GLOB '[0-7]' AND \"committed_session_run_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "committed_value": { + "name": "committed_value", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "observed_driver_instance_id": { + "name": "observed_driver_instance_id", + "type": "text CHECK (\"observed_driver_instance_id\" = upper(\"observed_driver_instance_id\") AND length(\"observed_driver_instance_id\") = 26 AND substr(\"observed_driver_instance_id\", 1, 1) GLOB '[0-7]' AND \"observed_driver_instance_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "observed_event_seq": { + "name": "observed_event_seq", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "observed_session_run_id": { + "name": "observed_session_run_id", + "type": "text CHECK (\"observed_session_run_id\" = upper(\"observed_session_run_id\") AND length(\"observed_session_run_id\") = 26 AND substr(\"observed_session_run_id\", 1, 1) GLOB '[0-7]' AND \"observed_session_run_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "runtime_id": { + "name": "runtime_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "session_id": { + "name": "session_id", + "type": "text CHECK (\"session_id\" = upper(\"session_id\") AND length(\"session_id\") = 26 AND substr(\"session_id\", 1, 1) GLOB '[0-7]' AND \"session_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "native_resume_ref_runtime_updated_idx": { + "name": "native_resume_ref_runtime_updated_idx", + "columns": ["runtime_id", "updated_at"], + "isUnique": false + } + }, + "foreignKeys": { + "native_resume_ref_session_id_session_id_fk": { + "name": "native_resume_ref_session_id_session_id_fk", + "tableFrom": "native_resume_ref", + "tableTo": "session", + "columnsFrom": ["session_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": { + "native_resume_ref_observed_event_seq_check": { + "name": "native_resume_ref_observed_event_seq_check", + "value": "\"native_resume_ref\".\"observed_event_seq\" >= 0" + } + } + }, + "sandbox_backup": { + "name": "sandbox_backup", + "columns": { + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "dir": { + "name": "dir", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "id": { + "name": "id", + "type": "text CHECK (\"id\" = upper(\"id\") AND length(\"id\") = 26 AND substr(\"id\", 1, 1) GLOB '[0-7]' AND \"id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "keep": { + "name": "keep", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "sandbox_id": { + "name": "sandbox_id", + "type": "text CHECK (\"sandbox_id\" = upper(\"sandbox_id\") AND length(\"sandbox_id\") = 26 AND substr(\"sandbox_id\", 1, 1) GLOB '[0-7]' AND \"sandbox_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "session_run_id": { + "name": "session_run_id", + "type": "text CHECK (\"session_run_id\" = upper(\"session_run_id\") AND length(\"session_run_id\") = 26 AND substr(\"session_run_id\", 1, 1) GLOB '[0-7]' AND \"session_run_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "ttl_seconds": { + "name": "ttl_seconds", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "sandbox_backup_sandbox_status_created_idx": { + "name": "sandbox_backup_sandbox_status_created_idx", + "columns": ["sandbox_id", "status", "created_at"], + "isUnique": false + }, + "sandbox_backup_terminal_checkpoint_idx": { + "name": "sandbox_backup_terminal_checkpoint_idx", + "columns": ["sandbox_id", "dir", "session_run_id"], + "isUnique": true, + "where": "\"sandbox_backup\".\"session_run_id\" IS NOT NULL AND \"sandbox_backup\".\"status\" = 'ready'" + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "sandbox_session": { + "name": "sandbox_session", + "columns": { + "cloudflare_session_id": { + "name": "cloudflare_session_id", + "type": "text CHECK (\"cloudflare_session_id\" = upper(\"cloudflare_session_id\") AND length(\"cloudflare_session_id\") = 26 AND substr(\"cloudflare_session_id\", 1, 1) GLOB '[0-7]' AND \"cloudflare_session_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "cwd": { + "name": "cwd", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "origin_json": { + "name": "origin_json", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "sandbox_id": { + "name": "sandbox_id", + "type": "text CHECK (\"sandbox_id\" = upper(\"sandbox_id\") AND length(\"sandbox_id\") = 26 AND substr(\"sandbox_id\", 1, 1) GLOB '[0-7]' AND \"sandbox_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "session_id": { + "name": "session_id", + "type": "text CHECK (\"session_id\" = upper(\"session_id\") AND length(\"session_id\") = 26 AND substr(\"session_id\", 1, 1) GLOB '[0-7]' AND \"session_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "sandbox_session_sandbox_status_idx": { + "name": "sandbox_session_sandbox_status_idx", + "columns": ["sandbox_id", "status", "updated_at"], + "isUnique": false + }, + "sandbox_session_cloudflare_session_idx": { + "name": "sandbox_session_cloudflare_session_idx", + "columns": ["cloudflare_session_id"], + "isUnique": true + } + }, + "foreignKeys": { + "sandbox_session_session_id_session_id_fk": { + "name": "sandbox_session_session_id_session_id_fk", + "tableFrom": "sandbox_session", + "tableTo": "session", + "columnsFrom": ["session_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "sandbox": { + "name": "sandbox", + "columns": { + "agent_id": { + "name": "agent_id", + "type": "text CHECK (\"agent_id\" = upper(\"agent_id\") AND length(\"agent_id\") = 26 AND substr(\"agent_id\", 1, 1) GLOB '[0-7]' AND \"agent_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "app_id": { + "name": "app_id", + "type": "text CHECK (\"app_id\" = upper(\"app_id\") AND length(\"app_id\") = 26 AND substr(\"app_id\", 1, 1) GLOB '[0-7]' AND \"app_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "bind_mount_ready": { + "name": "bind_mount_ready", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "claim_expires_at": { + "name": "claim_expires_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "claim_owner": { + "name": "claim_owner", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "global_mounts_json": { + "name": "global_mounts_json", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'[]'" + }, + "id": { + "name": "id", + "type": "text CHECK (\"id\" = upper(\"id\") AND length(\"id\") = 26 AND substr(\"id\", 1, 1) GLOB '[0-7]' AND \"id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "inactive_deadline_at": { + "name": "inactive_deadline_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "last_backup_id": { + "name": "last_backup_id", + "type": "text CHECK (\"last_backup_id\" = upper(\"last_backup_id\") AND length(\"last_backup_id\") = 26 AND substr(\"last_backup_id\", 1, 1) GLOB '[0-7]' AND \"last_backup_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_error_code": { + "name": "last_error_code", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_restore_backup_id": { + "name": "last_restore_backup_id", + "type": "text CHECK (\"last_restore_backup_id\" = upper(\"last_restore_backup_id\") AND length(\"last_restore_backup_id\") = 26 AND substr(\"last_restore_backup_id\", 1, 1) GLOB '[0-7]' AND \"last_restore_backup_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "owner_account_id": { + "name": "owner_account_id", + "type": "text CHECK (\"owner_account_id\" = upper(\"owner_account_id\") AND length(\"owner_account_id\") = 26 AND substr(\"owner_account_id\", 1, 1) GLOB '[0-7]' AND \"owner_account_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status_changed_at": { + "name": "status_changed_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "status_event": { + "name": "status_event", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'runtime_subject.cold'" + }, + "status_operation_id": { + "name": "status_operation_id", + "type": "text CHECK (\"status_operation_id\" = upper(\"status_operation_id\") AND length(\"status_operation_id\") = 26 AND substr(\"status_operation_id\", 1, 1) GLOB '[0-7]' AND \"status_operation_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status_seq": { + "name": "status_seq", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "status_source": { + "name": "status_source", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'system'" + }, + "subject_id": { + "name": "subject_id", + "type": "text CHECK (\"subject_id\" = upper(\"subject_id\") AND length(\"subject_id\") = 26 AND substr(\"subject_id\", 1, 1) GLOB '[0-7]' AND \"subject_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "subject_kind": { + "name": "subject_kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "sandbox_subject_idx": { + "name": "sandbox_subject_idx", + "columns": ["kind", "subject_kind", "subject_id"], + "isUnique": true + }, + "sandbox_status_deadline_idx": { + "name": "sandbox_status_deadline_idx", + "columns": ["status", "inactive_deadline_at", "updated_at"], + "isUnique": false + }, + "sandbox_claim_idx": { + "name": "sandbox_claim_idx", + "columns": ["claim_expires_at", "claim_owner"], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": { + "sandbox_status_check": { + "name": "sandbox_status_check", + "value": "\"sandbox\".\"status\" IN ('cold', 'restoring', 'active', 'backing_up', 'destroying', 'error')" + }, + "sandbox_status_seq_check": { + "name": "sandbox_status_seq_check", + "value": "\"sandbox\".\"status_seq\" >= 0" + } + } + }, + "session_message": { + "name": "session_message", + "columns": { + "content_text": { + "name": "content_text", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_by_account_id": { + "name": "created_by_account_id", + "type": "text CHECK (\"created_by_account_id\" = upper(\"created_by_account_id\") AND length(\"created_by_account_id\") = 26 AND substr(\"created_by_account_id\", 1, 1) GLOB '[0-7]' AND \"created_by_account_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "id": { + "name": "id", + "type": "text CHECK (\"id\" = upper(\"id\") AND length(\"id\") = 26 AND substr(\"id\", 1, 1) GLOB '[0-7]' AND \"id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "plan_json": { + "name": "plan_json", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "projection_format": { + "name": "projection_format", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'materialized'" + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "segments_json": { + "name": "segments_json", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "seq": { + "name": "seq", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "session_id": { + "name": "session_id", + "type": "text CHECK (\"session_id\" = upper(\"session_id\") AND length(\"session_id\") = 26 AND substr(\"session_id\", 1, 1) GLOB '[0-7]' AND \"session_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "session_run_id": { + "name": "session_run_id", + "type": "text CHECK (\"session_run_id\" = upper(\"session_run_id\") AND length(\"session_run_id\") = 26 AND substr(\"session_run_id\", 1, 1) GLOB '[0-7]' AND \"session_run_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "session_message_session_seq_idx": { + "name": "session_message_session_seq_idx", + "columns": ["session_id", "seq"], + "isUnique": true + }, + "session_message_run_idx": { + "name": "session_message_run_idx", + "columns": ["session_run_id"], + "isUnique": false + } + }, + "foreignKeys": { + "session_message_session_id_session_id_fk": { + "name": "session_message_session_id_session_id_fk", + "tableFrom": "session_message", + "tableTo": "session", + "columnsFrom": ["session_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": { + "session_message_projection_format_check": { + "name": "session_message_projection_format_check", + "value": "\"session_message\".\"projection_format\" IN ('materialized', 'event_stream_v3')" + }, + "session_message_event_stream_v3_check": { + "name": "session_message_event_stream_v3_check", + "value": "\"session_message\".\"projection_format\" <> 'event_stream_v3' OR (\"session_message\".\"role\" = 'assistant' AND \"session_message\".\"session_run_id\" IS NOT NULL AND \"session_message\".\"content_text\" = '' AND \"session_message\".\"plan_json\" IS NULL AND \"session_message\".\"segments_json\" IS NULL)" + } + } + }, + "session": { + "name": "session", + "columns": { + "agent_id": { + "name": "agent_id", + "type": "text CHECK (\"agent_id\" = upper(\"agent_id\") AND length(\"agent_id\") = 26 AND substr(\"agent_id\", 1, 1) GLOB '[0-7]' AND \"agent_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "archived_at": { + "name": "archived_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "auto_title_event_seq": { + "name": "auto_title_event_seq", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "cleanup_operation_kind": { + "name": "cleanup_operation_kind", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "end_user_id": { + "name": "end_user_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "attributed_user_id": { + "name": "attributed_user_id", + "type": "text CHECK (\"attributed_user_id\" = upper(\"attributed_user_id\") AND length(\"attributed_user_id\") = 26 AND substr(\"attributed_user_id\", 1, 1) GLOB '[0-7]' AND \"attributed_user_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "creator_account_id": { + "name": "creator_account_id", + "type": "text CHECK (\"creator_account_id\" = upper(\"creator_account_id\") AND length(\"creator_account_id\") = 26 AND substr(\"creator_account_id\", 1, 1) GLOB '[0-7]' AND \"creator_account_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "deployment_version_id": { + "name": "deployment_version_id", + "type": "text CHECK (\"deployment_version_id\" = upper(\"deployment_version_id\") AND length(\"deployment_version_id\") = 26 AND substr(\"deployment_version_id\", 1, 1) GLOB '[0-7]' AND \"deployment_version_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "deployment_version_number": { + "name": "deployment_version_number", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "id": { + "name": "id", + "type": "text CHECK (\"id\" = upper(\"id\") AND length(\"id\") = 26 AND substr(\"id\", 1, 1) GLOB '[0-7]' AND \"id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "last_message_at": { + "name": "last_message_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_run_id": { + "name": "last_run_id", + "type": "text CHECK (\"last_run_id\" = upper(\"last_run_id\") AND length(\"last_run_id\") = 26 AND substr(\"last_run_id\", 1, 1) GLOB '[0-7]' AND \"last_run_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "message_seq_cursor": { + "name": "message_seq_cursor", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "metadata_json": { + "name": "metadata_json", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'{}'" + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "app_id": { + "name": "app_id", + "type": "text CHECK (\"app_id\" = upper(\"app_id\") AND length(\"app_id\") = 26 AND substr(\"app_id\", 1, 1) GLOB '[0-7]' AND \"app_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "renamed": { + "name": "renamed", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "runtime_id": { + "name": "runtime_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status_operation_id": { + "name": "status_operation_id", + "type": "text CHECK (\"status_operation_id\" = upper(\"status_operation_id\") AND length(\"status_operation_id\") = 26 AND substr(\"status_operation_id\", 1, 1) GLOB '[0-7]' AND \"status_operation_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status_seq": { + "name": "status_seq", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "runtime_event_seq_cursor": { + "name": "runtime_event_seq_cursor", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "runtime_provisioning_heartbeat_at": { + "name": "runtime_provisioning_heartbeat_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "runtime_provisioning_operation_id": { + "name": "runtime_provisioning_operation_id", + "type": "text CHECK (\"runtime_provisioning_operation_id\" = upper(\"runtime_provisioning_operation_id\") AND length(\"runtime_provisioning_operation_id\") = 26 AND substr(\"runtime_provisioning_operation_id\", 1, 1) GLOB '[0-7]' AND \"runtime_provisioning_operation_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "runtime_provisioning_run_id": { + "name": "runtime_provisioning_run_id", + "type": "text CHECK (\"runtime_provisioning_run_id\" = upper(\"runtime_provisioning_run_id\") AND length(\"runtime_provisioning_run_id\") = 26 AND substr(\"runtime_provisioning_run_id\", 1, 1) GLOB '[0-7]' AND \"runtime_provisioning_run_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "runtime_provisioning_sandbox_id": { + "name": "runtime_provisioning_sandbox_id", + "type": "text CHECK (\"runtime_provisioning_sandbox_id\" = upper(\"runtime_provisioning_sandbox_id\") AND length(\"runtime_provisioning_sandbox_id\") = 26 AND substr(\"runtime_provisioning_sandbox_id\", 1, 1) GLOB '[0-7]' AND \"runtime_provisioning_sandbox_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'preview'" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "workspace_checkpoint_required": { + "name": "workspace_checkpoint_required", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + } + }, + "indexes": { + "session_agent_updated_idx": { + "name": "session_agent_updated_idx", + "columns": ["agent_id", "updated_at", "id"], + "isUnique": false + }, + "session_app_creator_archived_updated_idx": { + "name": "session_app_creator_archived_updated_idx", + "columns": ["app_id", "creator_account_id", "archived_at", "updated_at", "id"], + "isUnique": false + }, + "session_app_attributed_archived_updated_idx": { + "name": "session_app_attributed_archived_updated_idx", + "columns": ["app_id", "attributed_user_id", "archived_at", "updated_at", "id"], + "isUnique": false + }, + "session_app_creator_type_archived_updated_idx": { + "name": "session_app_creator_type_archived_updated_idx", + "columns": ["app_id", "creator_account_id", "type", "archived_at", "updated_at", "id"], + "isUnique": false + }, + "session_app_attributed_type_archived_updated_idx": { + "name": "session_app_attributed_type_archived_updated_idx", + "columns": ["app_id", "attributed_user_id", "type", "archived_at", "updated_at", "id"], + "isUnique": false + }, + "session_status_operation_updated_idx": { + "name": "session_status_operation_updated_idx", + "columns": ["status", "status_operation_id", "updated_at"], + "isUnique": false + }, + "session_cleanup_operation_updated_idx": { + "name": "session_cleanup_operation_updated_idx", + "columns": ["cleanup_operation_kind", "status", "updated_at", "id"], + "isUnique": false + }, + "session_runtime_provisioning_heartbeat_idx": { + "name": "session_runtime_provisioning_heartbeat_idx", + "columns": ["runtime_provisioning_heartbeat_at", "id"], + "isUnique": false + }, + "session_status_updated_idx": { + "name": "session_status_updated_idx", + "columns": ["status", "updated_at", "id"], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": { + "session_cleanup_operation_kind_check": { + "name": "session_cleanup_operation_kind_check", + "value": "\"session\".\"cleanup_operation_kind\" IS NULL OR (\"session\".\"cleanup_operation_kind\" IN ('archive', 'delete') AND \"session\".\"archived_at\" IS NOT NULL AND \"session\".\"status\" IN ('IDLE', 'RESCHEDULING') AND (\"session\".\"status_operation_id\" IS NOT NULL OR (\"session\".\"cleanup_operation_kind\" = 'archive' AND \"session\".\"status\" = 'IDLE')))" + }, + "session_runtime_provisioning_lease_check": { + "name": "session_runtime_provisioning_lease_check", + "value": "(\"session\".\"runtime_provisioning_operation_id\" IS NULL AND \"session\".\"runtime_provisioning_run_id\" IS NULL AND \"session\".\"runtime_provisioning_sandbox_id\" IS NULL AND \"session\".\"runtime_provisioning_heartbeat_at\" IS NULL) OR (\"session\".\"runtime_provisioning_operation_id\" IS NOT NULL AND \"session\".\"runtime_provisioning_sandbox_id\" IS NOT NULL AND \"session\".\"runtime_provisioning_heartbeat_at\" IS NOT NULL AND typeof(\"session\".\"runtime_provisioning_heartbeat_at\") = 'integer' AND \"session\".\"runtime_provisioning_heartbeat_at\" >= 0 AND \"session\".\"archived_at\" IS NULL AND \"session\".\"cleanup_operation_kind\" IS NULL AND \"session\".\"status_operation_id\" IS NULL)" + }, + "session_status_check": { + "name": "session_status_check", + "value": "\"session\".\"status\" IN ('IDLE', 'RUNNING', 'RESCHEDULING', 'TERMINATED')" + }, + "session_auto_title_event_seq_check": { + "name": "session_auto_title_event_seq_check", + "value": "\"session\".\"auto_title_event_seq\" IS NULL OR \"session\".\"auto_title_event_seq\" >= 0" + }, + "session_status_seq_check": { + "name": "session_status_seq_check", + "value": "\"session\".\"status_seq\" >= 0" + } + } + }, + "session_execution_snapshot": { + "name": "session_execution_snapshot", + "columns": { + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "plan_json": { + "name": "plan_json", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "session_id": { + "name": "session_id", + "type": "text CHECK (\"session_id\" = upper(\"session_id\") AND length(\"session_id\") = 26 AND substr(\"session_id\", 1, 1) GLOB '[0-7]' AND \"session_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": true, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "session_execution_snapshot_session_id_session_id_fk": { + "name": "session_execution_snapshot_session_id_session_id_fk", + "tableFrom": "session_execution_snapshot", + "tableTo": "session", + "columnsFrom": ["session_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "session_run_skill": { + "name": "session_run_skill", + "columns": { + "blob_sha256": { + "name": "blob_sha256", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "materialization_status": { + "name": "materialization_status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "mount_path": { + "name": "mount_path", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "resolution_mode": { + "name": "resolution_mode", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "session_run_id": { + "name": "session_run_id", + "type": "text CHECK (\"session_run_id\" = upper(\"session_run_id\") AND length(\"session_run_id\") = 26 AND substr(\"session_run_id\", 1, 1) GLOB '[0-7]' AND \"session_run_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "skill_id": { + "name": "skill_id", + "type": "text CHECK (\"skill_id\" = upper(\"skill_id\") AND length(\"skill_id\") = 26 AND substr(\"skill_id\", 1, 1) GLOB '[0-7]' AND \"skill_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "skill_name": { + "name": "skill_name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "snapshot_id": { + "name": "snapshot_id", + "type": "text CHECK (\"snapshot_id\" = upper(\"snapshot_id\") AND length(\"snapshot_id\") = 26 AND substr(\"snapshot_id\", 1, 1) GLOB '[0-7]' AND \"snapshot_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "warning_code": { + "name": "warning_code", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "session_run_skill_run_resolution_idx": { + "name": "session_run_skill_run_resolution_idx", + "columns": ["session_run_id", "resolution_mode"], + "isUnique": false + } + }, + "foreignKeys": { + "session_run_skill_session_run_id_session_run_id_fk": { + "name": "session_run_skill_session_run_id_session_run_id_fk", + "tableFrom": "session_run_skill", + "tableTo": "session_run", + "columnsFrom": ["session_run_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "session_run_skill_session_run_id_skill_id_pk": { + "columns": ["session_run_id", "skill_id"], + "name": "session_run_skill_session_run_id_skill_id_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "session_run": { + "name": "session_run", + "columns": { + "agent_id": { + "name": "agent_id", + "type": "text CHECK (\"agent_id\" = upper(\"agent_id\") AND length(\"agent_id\") = 26 AND substr(\"agent_id\", 1, 1) GLOB '[0-7]' AND \"agent_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "bound_capability_agent_id": { + "name": "bound_capability_agent_id", + "type": "text CHECK (\"bound_capability_agent_id\" = upper(\"bound_capability_agent_id\") AND length(\"bound_capability_agent_id\") = 26 AND substr(\"bound_capability_agent_id\", 1, 1) GLOB '[0-7]' AND \"bound_capability_agent_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "bound_capability_app_id": { + "name": "bound_capability_app_id", + "type": "text CHECK (\"bound_capability_app_id\" = upper(\"bound_capability_app_id\") AND length(\"bound_capability_app_id\") = 26 AND substr(\"bound_capability_app_id\", 1, 1) GLOB '[0-7]' AND \"bound_capability_app_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "bound_capability_binding_env": { + "name": "bound_capability_binding_env", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "bound_capability_binding_name": { + "name": "bound_capability_binding_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "bound_capability_deployment_id": { + "name": "bound_capability_deployment_id", + "type": "text CHECK (\"bound_capability_deployment_id\" = upper(\"bound_capability_deployment_id\") AND length(\"bound_capability_deployment_id\") = 26 AND substr(\"bound_capability_deployment_id\", 1, 1) GLOB '[0-7]' AND \"bound_capability_deployment_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "bound_capability_deployment_run_id": { + "name": "bound_capability_deployment_run_id", + "type": "text CHECK (\"bound_capability_deployment_run_id\" = upper(\"bound_capability_deployment_run_id\") AND length(\"bound_capability_deployment_run_id\") = 26 AND substr(\"bound_capability_deployment_run_id\", 1, 1) GLOB '[0-7]' AND \"bound_capability_deployment_run_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "completed_at": { + "name": "completed_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_by_account_id": { + "name": "created_by_account_id", + "type": "text CHECK (\"created_by_account_id\" = upper(\"created_by_account_id\") AND length(\"created_by_account_id\") = 26 AND substr(\"created_by_account_id\", 1, 1) GLOB '[0-7]' AND \"created_by_account_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "deployment_version_id": { + "name": "deployment_version_id", + "type": "text CHECK (\"deployment_version_id\" = upper(\"deployment_version_id\") AND length(\"deployment_version_id\") = 26 AND substr(\"deployment_version_id\", 1, 1) GLOB '[0-7]' AND \"deployment_version_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "deployment_version_number": { + "name": "deployment_version_number", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "driver_instance_id": { + "name": "driver_instance_id", + "type": "text CHECK (\"driver_instance_id\" = upper(\"driver_instance_id\") AND length(\"driver_instance_id\") = 26 AND substr(\"driver_instance_id\", 1, 1) GLOB '[0-7]' AND \"driver_instance_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "error_code": { + "name": "error_code", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "error_details_json": { + "name": "error_details_json", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "error_retryable": { + "name": "error_retryable", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "id": { + "name": "id", + "type": "text CHECK (\"id\" = upper(\"id\") AND length(\"id\") = 26 AND substr(\"id\", 1, 1) GLOB '[0-7]' AND \"id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "runtime_id": { + "name": "runtime_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "session_id": { + "name": "session_id", + "type": "text CHECK (\"session_id\" = upper(\"session_id\") AND length(\"session_id\") = 26 AND substr(\"session_id\", 1, 1) GLOB '[0-7]' AND \"session_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "started_at": { + "name": "started_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status_changed_at": { + "name": "status_changed_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "status_event": { + "name": "status_event", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'run.queue'" + }, + "status_operation_id": { + "name": "status_operation_id", + "type": "text CHECK (\"status_operation_id\" = upper(\"status_operation_id\") AND length(\"status_operation_id\") = 26 AND substr(\"status_operation_id\", 1, 1) GLOB '[0-7]' AND \"status_operation_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status_seq": { + "name": "status_seq", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "status_source": { + "name": "status_source", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'system'" + }, + "terminal_reconciliation_attempted_at": { + "name": "terminal_reconciliation_attempted_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "trace_id": { + "name": "trace_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "trigger": { + "name": "trigger", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "session_run_driver_instance_idx": { + "name": "session_run_driver_instance_idx", + "columns": ["driver_instance_id", "created_at"], + "isUnique": false + }, + "session_run_active_driver_lease_idx": { + "name": "session_run_active_driver_lease_idx", + "columns": ["driver_instance_id"], + "isUnique": true, + "where": "\"session_run\".\"driver_instance_id\" IS NOT NULL AND \"session_run\".\"status\" IN ('queued', 'booting', 'running', 'waiting_input')" + }, + "session_run_session_created_at_idx": { + "name": "session_run_session_created_at_idx", + "columns": ["session_id", "created_at"], + "isUnique": false + }, + "session_run_session_status_idx": { + "name": "session_run_session_status_idx", + "columns": ["session_id", "status"], + "isUnique": false + }, + "session_run_terminal_reconciliation_attempt_idx": { + "name": "session_run_terminal_reconciliation_attempt_idx", + "columns": ["coalesce(\"terminal_reconciliation_attempted_at\", \"updated_at\")", "id"], + "isUnique": false + } + }, + "foreignKeys": { + "session_run_session_id_session_id_fk": { + "name": "session_run_session_id_session_id_fk", + "tableFrom": "session_run", + "tableTo": "session", + "columnsFrom": ["session_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": { + "session_run_error_retryable_check": { + "name": "session_run_error_retryable_check", + "value": "\"session_run\".\"error_retryable\" IS NULL OR (\"session_run\".\"error_retryable\" IN (false, true) AND \"session_run\".\"error_code\" IS NOT NULL AND \"session_run\".\"error_details_json\" IS NOT NULL AND \"session_run\".\"error_message\" IS NOT NULL)" + }, + "session_run_status_check": { + "name": "session_run_status_check", + "value": "\"session_run\".\"status\" IN ('queued', 'booting', 'running', 'waiting_input', 'completed', 'failed', 'cancelled', 'expired')" + }, + "session_run_status_seq_check": { + "name": "session_run_status_seq_check", + "value": "\"session_run\".\"status_seq\" >= 0" + }, + "session_run_terminal_reconciliation_attempted_at_check": { + "name": "session_run_terminal_reconciliation_attempted_at_check", + "value": "\"session_run\".\"terminal_reconciliation_attempted_at\" IS NULL OR \"session_run\".\"terminal_reconciliation_attempted_at\" >= 0" + } + } + }, + "session_agent_task_snapshot": { + "name": "session_agent_task_snapshot", + "columns": { + "driver_instance_id": { + "name": "driver_instance_id", + "type": "text CHECK (\"driver_instance_id\" = upper(\"driver_instance_id\") AND length(\"driver_instance_id\") = 26 AND substr(\"driver_instance_id\", 1, 1) GLOB '[0-7]' AND \"driver_instance_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "run_id": { + "name": "run_id", + "type": "text CHECK (\"run_id\" = upper(\"run_id\") AND length(\"run_id\") = 26 AND substr(\"run_id\", 1, 1) GLOB '[0-7]' AND \"run_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "seq": { + "name": "seq", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "session_id": { + "name": "session_id", + "type": "text CHECK (\"session_id\" = upper(\"session_id\") AND length(\"session_id\") = 26 AND substr(\"session_id\", 1, 1) GLOB '[0-7]' AND \"session_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tasks_json": { + "name": "tasks_json", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "session_agent_task_snapshot_run_id_session_run_id_fk": { + "name": "session_agent_task_snapshot_run_id_session_run_id_fk", + "tableFrom": "session_agent_task_snapshot", + "tableTo": "session_run", + "columnsFrom": ["run_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "session_agent_task_snapshot_session_id_session_id_fk": { + "name": "session_agent_task_snapshot_session_id_session_id_fk", + "tableFrom": "session_agent_task_snapshot", + "tableTo": "session", + "columnsFrom": ["session_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "session_event": { + "name": "session_event", + "columns": { + "agent_id": { + "name": "agent_id", + "type": "text CHECK (\"agent_id\" = upper(\"agent_id\") AND length(\"agent_id\") = 26 AND substr(\"agent_id\", 1, 1) GLOB '[0-7]' AND \"agent_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "artifact_attempt_id": { + "name": "artifact_attempt_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "artifact_manifest_json": { + "name": "artifact_manifest_json", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "artifact_manifest_sha256": { + "name": "artifact_manifest_sha256", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "content_text": { + "name": "content_text", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "ended_at": { + "name": "ended_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "event_type": { + "name": "event_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "family": { + "name": "family", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "id": { + "name": "id", + "type": "text CHECK (\"id\" = upper(\"id\") AND length(\"id\") = 26 AND substr(\"id\", 1, 1) GLOB '[0-7]' AND \"id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "mcp_command_id": { + "name": "mcp_command_id", + "type": "text CHECK (\"mcp_command_id\" = upper(\"mcp_command_id\") AND length(\"mcp_command_id\") = 26 AND substr(\"mcp_command_id\", 1, 1) GLOB '[0-7]' AND \"mcp_command_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "occurred_at": { + "name": "occurred_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "process_status": { + "name": "process_status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "process_type": { + "name": "process_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "run_id": { + "name": "run_id", + "type": "text CHECK (\"run_id\" = upper(\"run_id\") AND length(\"run_id\") = 26 AND substr(\"run_id\", 1, 1) GLOB '[0-7]' AND \"run_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "semantic_hash": { + "name": "semantic_hash", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "seq": { + "name": "seq", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "session_id": { + "name": "session_id", + "type": "text CHECK (\"session_id\" = upper(\"session_id\") AND length(\"session_id\") = 26 AND substr(\"session_id\", 1, 1) GLOB '[0-7]' AND \"session_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "source_event_id": { + "name": "source_event_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "stream_id": { + "name": "stream_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "terminal_event_json": { + "name": "terminal_event_json", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "tool_call_id": { + "name": "tool_call_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "tool_input_delta_json": { + "name": "tool_input_delta_json", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "tool_input_json": { + "name": "tool_input_json", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "tool_name": { + "name": "tool_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "tool_output_delta_text": { + "name": "tool_output_delta_text", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "tool_output_text": { + "name": "tool_output_text", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "tool_parent_message_id": { + "name": "tool_parent_message_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "tool_result_message_id": { + "name": "tool_result_message_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "tool_status": { + "name": "tool_status", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "tokens": { + "name": "tokens", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "trace_id": { + "name": "trace_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "visibility": { + "name": "visibility", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "session_event_agent_family_created_idx": { + "name": "session_event_agent_family_created_idx", + "columns": ["agent_id", "family", "created_at", "id"], + "isUnique": false + }, + "session_event_artifact_attempt_idx": { + "name": "session_event_artifact_attempt_idx", + "columns": ["artifact_attempt_id"], + "isUnique": true, + "where": "\"session_event\".\"artifact_attempt_id\" IS NOT NULL" + }, + "session_event_agent_visibility_created_idx": { + "name": "session_event_agent_visibility_created_idx", + "columns": ["agent_id", "visibility", "created_at", "id"], + "isUnique": false + }, + "session_event_agent_created_idx": { + "name": "session_event_agent_created_idx", + "columns": ["agent_id", "created_at", "id"], + "isUnique": false + }, + "session_event_session_visibility_seq_idx": { + "name": "session_event_session_visibility_seq_idx", + "columns": ["session_id", "visibility", "seq"], + "isUnique": false + }, + "session_event_run_event_type_idx": { + "name": "session_event_run_event_type_idx", + "columns": ["run_id", "event_type"], + "isUnique": false + }, + "session_event_run_stream_process_seq_idx": { + "name": "session_event_run_stream_process_seq_idx", + "columns": ["run_id", "stream_id", "process_type", "seq"], + "isUnique": false + }, + "session_event_run_tool_call_seq_idx": { + "name": "session_event_run_tool_call_seq_idx", + "columns": ["run_id", "tool_call_id", "seq"], + "isUnique": false + }, + "session_event_session_seq_idx": { + "name": "session_event_session_seq_idx", + "columns": ["session_id", "seq"], + "isUnique": true + }, + "session_event_session_source_idx": { + "name": "session_event_session_source_idx", + "columns": ["session_id", "source_event_id"], + "isUnique": true + }, + "session_event_run_terminal_winner_idx": { + "name": "session_event_run_terminal_winner_idx", + "columns": ["session_id", "run_id"], + "isUnique": true, + "where": "\"session_event\".\"semantic_hash\" IS NOT NULL AND \"session_event\".\"run_id\" IS NOT NULL AND \"session_event\".\"event_type\" IN ('run.cancelled', 'run.completed', 'run.failed')" + }, + "session_event_mcp_terminal_winner_idx": { + "name": "session_event_mcp_terminal_winner_idx", + "columns": ["session_id", "mcp_command_id"], + "isUnique": true, + "where": "\"session_event\".\"mcp_command_id\" IS NOT NULL" + } + }, + "foreignKeys": { + "session_event_session_id_session_id_fk": { + "name": "session_event_session_id_session_id_fk", + "tableFrom": "session_event", + "tableTo": "session", + "columnsFrom": ["session_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": { + "session_event_artifact_manifest_check": { + "name": "session_event_artifact_manifest_check", + "value": "(\"session_event\".\"artifact_attempt_id\" IS NULL AND \"session_event\".\"artifact_manifest_json\" IS NULL AND \"session_event\".\"artifact_manifest_sha256\" IS NULL) OR (\"session_event\".\"artifact_attempt_id\" IS NOT NULL AND \"session_event\".\"artifact_manifest_json\" IS NOT NULL AND json_valid(\"session_event\".\"artifact_manifest_json\") = 1 AND json_extract(\"session_event\".\"artifact_manifest_json\", '$.version') IS 1 AND json_type(\"session_event\".\"artifact_manifest_json\", '$.captureStatus') IS 'text' AND json_extract(\"session_event\".\"artifact_manifest_json\", '$.captureStatus') IN ('complete', 'omitted_file_limit', 'omitted_runtime_unavailable', 'omitted_size_limit', 'omitted_source_changed', 'omitted_source_missing') AND json_type(\"session_event\".\"artifact_manifest_json\", '$.mode') IS 'text' AND json_extract(\"session_event\".\"artifact_manifest_json\", '$.mode') IN ('delta', 'snapshot') AND (json_extract(\"session_event\".\"artifact_manifest_json\", '$.captureStatus') = 'complete' OR json_array_length(\"session_event\".\"artifact_manifest_json\", '$.files') = 0) AND json_extract(\"session_event\".\"artifact_manifest_json\", '$.sourceEventId') IS \"session_event\".\"source_event_id\" AND json_extract(\"session_event\".\"artifact_manifest_json\", '$.semanticHash') IS \"session_event\".\"semantic_hash\" AND json_type(\"session_event\".\"artifact_manifest_json\", '$.files') IS 'array' AND \"session_event\".\"artifact_manifest_sha256\" IS NOT NULL AND length(\"session_event\".\"artifact_manifest_sha256\") = 64 AND \"session_event\".\"artifact_manifest_sha256\" = lower(\"session_event\".\"artifact_manifest_sha256\") AND \"session_event\".\"artifact_manifest_sha256\" NOT GLOB '*[^0-9a-f]*' AND \"session_event\".\"semantic_hash\" IS NOT NULL AND \"session_event\".\"event_type\" IN ('file.change.updated', 'file.changed', 'run.completed'))" + }, + "session_event_mcp_command_check": { + "name": "session_event_mcp_command_check", + "value": "\"session_event\".\"mcp_command_id\" IS NULL OR (\"session_event\".\"event_type\" = 'tool.call.updated' AND \"session_event\".\"tool_status\" IS NOT NULL AND \"session_event\".\"tool_status\" IN ('completed', 'failed', 'cancelled'))" + }, + "session_event_semantic_hash_check": { + "name": "session_event_semantic_hash_check", + "value": "\"session_event\".\"semantic_hash\" IS NULL OR (length(\"session_event\".\"semantic_hash\") = 64 AND \"session_event\".\"semantic_hash\" = lower(\"session_event\".\"semantic_hash\") AND \"session_event\".\"semantic_hash\" NOT GLOB '*[^0-9a-f]*')" + }, + "session_event_terminal_event_json_check": { + "name": "session_event_terminal_event_json_check", + "value": "(\"session_event\".\"terminal_event_json\" IS NULL AND NOT (\"session_event\".\"semantic_hash\" IS NOT NULL AND \"session_event\".\"event_type\" IN ('run.cancelled', 'run.completed', 'run.failed'))) OR (\"session_event\".\"terminal_event_json\" IS NOT NULL AND json_valid(\"session_event\".\"terminal_event_json\") = 1 AND \"session_event\".\"semantic_hash\" IS NOT NULL AND \"session_event\".\"event_type\" IN ('run.cancelled', 'run.completed', 'run.failed'))" + }, + "session_event_tool_input_kind_check": { + "name": "session_event_tool_input_kind_check", + "value": "\"session_event\".\"tool_input_delta_json\" IS NULL OR \"session_event\".\"tool_input_json\" IS NULL" + }, + "session_event_tool_output_kind_check": { + "name": "session_event_tool_output_kind_check", + "value": "\"session_event\".\"tool_output_delta_text\" IS NULL OR \"session_event\".\"tool_output_text\" IS NULL" + }, + "session_event_tool_status_check": { + "name": "session_event_tool_status_check", + "value": "\"session_event\".\"tool_status\" IS NULL OR \"session_event\".\"tool_status\" IN ('running', 'completed', 'failed', 'cancelled')" + } + } + }, + "session_model_call": { + "name": "session_model_call", + "columns": { + "cache_creation_tokens": { + "name": "cache_creation_tokens", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "cache_read_tokens": { + "name": "cache_read_tokens", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "call_key": { + "name": "call_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "completed_at": { + "name": "completed_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "cost_currency": { + "name": "cost_currency", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "driver_instance_id": { + "name": "driver_instance_id", + "type": "text CHECK (\"driver_instance_id\" = upper(\"driver_instance_id\") AND length(\"driver_instance_id\") = 26 AND substr(\"driver_instance_id\", 1, 1) GLOB '[0-7]' AND \"driver_instance_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "error_code": { + "name": "error_code", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "id": { + "name": "id", + "type": "text CHECK (\"id\" = upper(\"id\") AND length(\"id\") = 26 AND substr(\"id\", 1, 1) GLOB '[0-7]' AND \"id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "input_tokens": { + "name": "input_tokens", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "metadata_json": { + "name": "metadata_json", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "native_call_id": { + "name": "native_call_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "output_tokens": { + "name": "output_tokens", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "source_event_seq": { + "name": "source_event_seq", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "session_id": { + "name": "session_id", + "type": "text CHECK (\"session_id\" = upper(\"session_id\") AND length(\"session_id\") = 26 AND substr(\"session_id\", 1, 1) GLOB '[0-7]' AND \"session_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "session_run_id": { + "name": "session_run_id", + "type": "text CHECK (\"session_run_id\" = upper(\"session_run_id\") AND length(\"session_run_id\") = 26 AND substr(\"session_run_id\", 1, 1) GLOB '[0-7]' AND \"session_run_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "started_at": { + "name": "started_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "total_cost_usd_micros": { + "name": "total_cost_usd_micros", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "trace_id": { + "name": "trace_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "session_model_call_run_created_idx": { + "name": "session_model_call_run_created_idx", + "columns": ["session_run_id", "created_at"], + "isUnique": false + }, + "session_model_call_session_created_idx": { + "name": "session_model_call_session_created_idx", + "columns": ["session_id", "created_at"], + "isUnique": false + }, + "session_model_call_run_key_idx": { + "name": "session_model_call_run_key_idx", + "columns": ["session_run_id", "call_key"], + "isUnique": true + }, + "session_model_call_native_idx": { + "name": "session_model_call_native_idx", + "columns": ["driver_instance_id", "native_call_id"], + "isUnique": true + } + }, + "foreignKeys": { + "session_model_call_session_id_session_id_fk": { + "name": "session_model_call_session_id_session_id_fk", + "tableFrom": "session_model_call", + "tableTo": "session", + "columnsFrom": ["session_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "session_model_call_session_run_id_session_run_id_fk": { + "name": "session_model_call_session_run_id_session_run_id_fk", + "tableFrom": "session_model_call", + "tableTo": "session_run", + "columnsFrom": ["session_run_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": { + "session_model_call_source_event_seq_check": { + "name": "session_model_call_source_event_seq_check", + "value": "\"session_model_call\".\"source_event_seq\" >= 0" + } + } + }, + "session_permission_request": { + "name": "session_permission_request", + "columns": { + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "driver_instance_id": { + "name": "driver_instance_id", + "type": "text CHECK (\"driver_instance_id\" = upper(\"driver_instance_id\") AND length(\"driver_instance_id\") = 26 AND substr(\"driver_instance_id\", 1, 1) GLOB '[0-7]' AND \"driver_instance_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "raw_input": { + "name": "raw_input", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "request_id": { + "name": "request_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "run_id": { + "name": "run_id", + "type": "text CHECK (\"run_id\" = upper(\"run_id\") AND length(\"run_id\") = 26 AND substr(\"run_id\", 1, 1) GLOB '[0-7]' AND \"run_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "session_id": { + "name": "session_id", + "type": "text CHECK (\"session_id\" = upper(\"session_id\") AND length(\"session_id\") = 26 AND substr(\"session_id\", 1, 1) GLOB '[0-7]' AND \"session_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "tool_call_id": { + "name": "tool_call_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "tool_kind": { + "name": "tool_kind", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "session_permission_request_run_idx": { + "name": "session_permission_request_run_idx", + "columns": ["session_id", "run_id"], + "isUnique": false + } + }, + "foreignKeys": { + "session_permission_request_session_id_session_id_fk": { + "name": "session_permission_request_session_id_session_id_fk", + "tableFrom": "session_permission_request", + "tableTo": "session", + "columnsFrom": ["session_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "session_permission_request_session_id_request_id_pk": { + "columns": ["session_id", "request_id"], + "name": "session_permission_request_session_id_request_id_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "session_readiness_snapshot": { + "name": "session_readiness_snapshot", + "columns": { + "readiness_json": { + "name": "readiness_json", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "session_id": { + "name": "session_id", + "type": "text CHECK (\"session_id\" = upper(\"session_id\") AND length(\"session_id\") = 26 AND substr(\"session_id\", 1, 1) GLOB '[0-7]' AND \"session_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "session_readiness_snapshot_session_id_session_id_fk": { + "name": "session_readiness_snapshot_session_id_session_id_fk", + "tableFrom": "session_readiness_snapshot", + "tableTo": "session", + "columnsFrom": ["session_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "skill_snapshot_entry": { + "name": "skill_snapshot_entry", + "columns": { + "entry_kind": { + "name": "entry_kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "is_executable": { + "name": "is_executable", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "mime_type": { + "name": "mime_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "path": { + "name": "path", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "sha256": { + "name": "sha256", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "size": { + "name": "size", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "snapshot_id": { + "name": "snapshot_id", + "type": "text CHECK (\"snapshot_id\" = upper(\"snapshot_id\") AND length(\"snapshot_id\") = 26 AND substr(\"snapshot_id\", 1, 1) GLOB '[0-7]' AND \"snapshot_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "skill_snapshot_entry_snapshot_id_path_pk": { + "columns": ["snapshot_id", "path"], + "name": "skill_snapshot_entry_snapshot_id_path_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "skill_snapshot": { + "name": "skill_snapshot", + "columns": { + "author": { + "name": "author", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "blob_key": { + "name": "blob_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "blob_sha256": { + "name": "blob_sha256", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "blob_size": { + "name": "blob_size", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "id": { + "name": "id", + "type": "text CHECK (\"id\" = upper(\"id\") AND length(\"id\") = 26 AND substr(\"id\", 1, 1) GLOB '[0-7]' AND \"id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "app_id": { + "name": "app_id", + "type": "text CHECK (\"app_id\" = upper(\"app_id\") AND length(\"app_id\") = 26 AND substr(\"app_id\", 1, 1) GLOB '[0-7]' AND \"app_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "skill_markdown_path": { + "name": "skill_markdown_path", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "uncompressed_size": { + "name": "uncompressed_size", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "version": { + "name": "version", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "skill_snapshot_app_created_at_idx": { + "name": "skill_snapshot_app_created_at_idx", + "columns": ["app_id", "created_at"], + "isUnique": false + }, + "skill_snapshot_blob_sha256_idx": { + "name": "skill_snapshot_blob_sha256_idx", + "columns": ["app_id", "blob_sha256"], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "skill": { + "name": "skill", + "columns": { + "author": { + "name": "author", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "current_snapshot_id": { + "name": "current_snapshot_id", + "type": "text CHECK (\"current_snapshot_id\" = upper(\"current_snapshot_id\") AND length(\"current_snapshot_id\") = 26 AND substr(\"current_snapshot_id\", 1, 1) GLOB '[0-7]' AND \"current_snapshot_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "forked_from_owner_name": { + "name": "forked_from_owner_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "forked_from_skill_id": { + "name": "forked_from_skill_id", + "type": "text CHECK (\"forked_from_skill_id\" = upper(\"forked_from_skill_id\") AND length(\"forked_from_skill_id\") = 26 AND substr(\"forked_from_skill_id\", 1, 1) GLOB '[0-7]' AND \"forked_from_skill_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "forked_from_skill_name": { + "name": "forked_from_skill_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "id": { + "name": "id", + "type": "text CHECK (\"id\" = upper(\"id\") AND length(\"id\") = 26 AND substr(\"id\", 1, 1) GLOB '[0-7]' AND \"id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "owner_account_id": { + "name": "owner_account_id", + "type": "text CHECK (\"owner_account_id\" = upper(\"owner_account_id\") AND length(\"owner_account_id\") = 26 AND substr(\"owner_account_id\", 1, 1) GLOB '[0-7]' AND \"owner_account_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "app_id": { + "name": "app_id", + "type": "text CHECK (\"app_id\" = upper(\"app_id\") AND length(\"app_id\") = 26 AND substr(\"app_id\", 1, 1) GLOB '[0-7]' AND \"app_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "source_kind": { + "name": "source_kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "version": { + "name": "version", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "skill_app_updated_at_idx": { + "name": "skill_app_updated_at_idx", + "columns": ["app_id", "updated_at"], + "isUnique": false + }, + "skill_owner_account_updated_at_idx": { + "name": "skill_owner_account_updated_at_idx", + "columns": ["owner_account_id", "updated_at"], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "account": { + "name": "account", + "columns": { + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "email_verified": { + "name": "email_verified", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "id": { + "name": "id", + "type": "text CHECK (\"id\" = upper(\"id\") AND length(\"id\") = 26 AND substr(\"id\", 1, 1) GLOB '[0-7]' AND \"id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "image_url": { + "name": "image_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_active_organization_id": { + "name": "last_active_organization_id", + "type": "text CHECK (\"last_active_organization_id\" = upper(\"last_active_organization_id\") AND length(\"last_active_organization_id\") = 26 AND substr(\"last_active_organization_id\", 1, 1) GLOB '[0-7]' AND \"last_active_organization_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "system_agent_model": { + "name": "system_agent_model", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "account_email_idx": { + "name": "account_email_idx", + "columns": ["email"], + "isUnique": true + }, + "account_last_active_organization_idx": { + "name": "account_last_active_organization_idx", + "columns": ["last_active_organization_id"], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "usage_daily_rollup": { + "name": "usage_daily_rollup", + "columns": { + "actor_user_id": { + "name": "actor_user_id", + "type": "text CHECK (\"actor_user_id\" = upper(\"actor_user_id\") AND length(\"actor_user_id\") = 26 AND substr(\"actor_user_id\", 1, 1) GLOB '[0-7]' AND \"actor_user_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "agent_id": { + "name": "agent_id", + "type": "text CHECK (\"agent_id\" = upper(\"agent_id\") AND length(\"agent_id\") = 26 AND substr(\"agent_id\", 1, 1) GLOB '[0-7]' AND \"agent_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "agent_owner_user_id": { + "name": "agent_owner_user_id", + "type": "text CHECK (\"agent_owner_user_id\" = upper(\"agent_owner_user_id\") AND length(\"agent_owner_user_id\") = 26 AND substr(\"agent_owner_user_id\", 1, 1) GLOB '[0-7]' AND \"agent_owner_user_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "agent_publication_state_at_run": { + "name": "agent_publication_state_at_run", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "cache_creation_tokens": { + "name": "cache_creation_tokens", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "cache_read_tokens": { + "name": "cache_read_tokens", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "date": { + "name": "date", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "input_tokens": { + "name": "input_tokens", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "organization_id": { + "name": "organization_id", + "type": "text CHECK (\"organization_id\" = upper(\"organization_id\") AND length(\"organization_id\") = 26 AND substr(\"organization_id\", 1, 1) GLOB '[0-7]' AND \"organization_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "app_id": { + "name": "app_id", + "type": "text CHECK (\"app_id\" = upper(\"app_id\") AND length(\"app_id\") = 26 AND substr(\"app_id\", 1, 1) GLOB '[0-7]' AND \"app_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "output_tokens": { + "name": "output_tokens", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "request_count": { + "name": "request_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "run_purpose": { + "name": "run_purpose", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "total_cost_usd_micros": { + "name": "total_cost_usd_micros", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "unpriced_request_count": { + "name": "unpriced_request_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "usage_daily_rollup_app_date_idx": { + "name": "usage_daily_rollup_app_date_idx", + "columns": ["app_id", "date"], + "isUnique": false + }, + "usage_daily_rollup_organization_date_idx": { + "name": "usage_daily_rollup_organization_date_idx", + "columns": ["organization_id", "date"], + "isUnique": false + }, + "usage_daily_rollup_agent_date_idx": { + "name": "usage_daily_rollup_agent_date_idx", + "columns": ["agent_id", "date"], + "isUnique": false + }, + "usage_daily_rollup_actor_date_idx": { + "name": "usage_daily_rollup_actor_date_idx", + "columns": ["actor_user_id", "date"], + "isUnique": false + }, + "usage_daily_rollup_owner_date_idx": { + "name": "usage_daily_rollup_owner_date_idx", + "columns": ["agent_owner_user_id", "date"], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "usage_daily_rollup_organization_id_app_id_agent_id_actor_user_id_agent_owner_user_id_date_agent_publication_state_at_run_run_purpose_provider_model_pk": { + "columns": [ + "organization_id", + "app_id", + "agent_id", + "actor_user_id", + "agent_owner_user_id", + "date", + "agent_publication_state_at_run", + "run_purpose", + "provider", + "model" + ], + "name": "usage_daily_rollup_organization_id_app_id_agent_id_actor_user_id_agent_owner_user_id_date_agent_publication_state_at_run_run_purpose_provider_model_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "usage_event_rollup_receipt": { + "name": "usage_event_rollup_receipt", + "columns": { + "rolled_up_at": { + "name": "rolled_up_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "source_event_id": { + "name": "source_event_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "usage_event_rollup_receipt_rolled_up_at_idx": { + "name": "usage_event_rollup_receipt_rolled_up_at_idx", + "columns": ["rolled_up_at"], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "usage_event_rollup_receipt_source_source_event_id_pk": { + "columns": ["source", "source_event_id"], + "name": "usage_event_rollup_receipt_source_source_event_id_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "usage_event": { + "name": "usage_event", + "columns": { + "actor_user_id": { + "name": "actor_user_id", + "type": "text CHECK (\"actor_user_id\" = upper(\"actor_user_id\") AND length(\"actor_user_id\") = 26 AND substr(\"actor_user_id\", 1, 1) GLOB '[0-7]' AND \"actor_user_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "agent_id": { + "name": "agent_id", + "type": "text CHECK (\"agent_id\" = upper(\"agent_id\") AND length(\"agent_id\") = 26 AND substr(\"agent_id\", 1, 1) GLOB '[0-7]' AND \"agent_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "agent_owner_user_id": { + "name": "agent_owner_user_id", + "type": "text CHECK (\"agent_owner_user_id\" = upper(\"agent_owner_user_id\") AND length(\"agent_owner_user_id\") = 26 AND substr(\"agent_owner_user_id\", 1, 1) GLOB '[0-7]' AND \"agent_owner_user_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "agent_publication_state_at_run": { + "name": "agent_publication_state_at_run", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "agent_revision_id": { + "name": "agent_revision_id", + "type": "text CHECK (\"agent_revision_id\" = upper(\"agent_revision_id\") AND length(\"agent_revision_id\") = 26 AND substr(\"agent_revision_id\", 1, 1) GLOB '[0-7]' AND \"agent_revision_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "cache_creation_tokens": { + "name": "cache_creation_tokens", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "cache_read_tokens": { + "name": "cache_read_tokens", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "id": { + "name": "id", + "type": "text CHECK (\"id\" = upper(\"id\") AND length(\"id\") = 26 AND substr(\"id\", 1, 1) GLOB '[0-7]' AND \"id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "input_tokens": { + "name": "input_tokens", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "organization_id": { + "name": "organization_id", + "type": "text CHECK (\"organization_id\" = upper(\"organization_id\") AND length(\"organization_id\") = 26 AND substr(\"organization_id\", 1, 1) GLOB '[0-7]' AND \"organization_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "app_id": { + "name": "app_id", + "type": "text CHECK (\"app_id\" = upper(\"app_id\") AND length(\"app_id\") = 26 AND substr(\"app_id\", 1, 1) GLOB '[0-7]' AND \"app_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "output_tokens": { + "name": "output_tokens", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "price_snapshot_json": { + "name": "price_snapshot_json", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "pricing_status": { + "name": "pricing_status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "run_purpose": { + "name": "run_purpose", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "runtime_id": { + "name": "runtime_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "session_id": { + "name": "session_id", + "type": "text CHECK (\"session_id\" = upper(\"session_id\") AND length(\"session_id\") = 26 AND substr(\"session_id\", 1, 1) GLOB '[0-7]' AND \"session_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "session_run_id": { + "name": "session_run_id", + "type": "text CHECK (\"session_run_id\" = upper(\"session_run_id\") AND length(\"session_run_id\") = 26 AND substr(\"session_run_id\", 1, 1) GLOB '[0-7]' AND \"session_run_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "source_event_id": { + "name": "source_event_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "source_event_seq": { + "name": "source_event_seq", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "total_cost_usd_micros": { + "name": "total_cost_usd_micros", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "usage_contract": { + "name": "usage_contract", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "usage_event_app_created_idx": { + "name": "usage_event_app_created_idx", + "columns": ["app_id", "created_at"], + "isUnique": false + }, + "usage_event_organization_created_idx": { + "name": "usage_event_organization_created_idx", + "columns": ["organization_id", "created_at"], + "isUnique": false + }, + "usage_event_agent_created_idx": { + "name": "usage_event_agent_created_idx", + "columns": ["agent_id", "created_at"], + "isUnique": false + }, + "usage_event_actor_created_idx": { + "name": "usage_event_actor_created_idx", + "columns": ["actor_user_id", "created_at"], + "isUnique": false + }, + "usage_event_owner_created_idx": { + "name": "usage_event_owner_created_idx", + "columns": ["agent_owner_user_id", "created_at"], + "isUnique": false + }, + "usage_event_session_run_idx": { + "name": "usage_event_session_run_idx", + "columns": ["session_run_id"], + "isUnique": false + }, + "usage_event_source_event_idx": { + "name": "usage_event_source_event_idx", + "columns": ["source", "source_event_id"], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": { + "usage_event_source_event_seq_check": { + "name": "usage_event_source_event_seq_check", + "value": "\"usage_event\".\"source_event_seq\" >= 0" + } + } + }, + "vendor_credential": { + "name": "vendor_credential", + "columns": { + "api_base": { + "name": "api_base", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "api_key_secret_id": { + "name": "api_key_secret_id", + "type": "text CHECK (\"api_key_secret_id\" = upper(\"api_key_secret_id\") AND length(\"api_key_secret_id\") = 26 AND substr(\"api_key_secret_id\", 1, 1) GLOB '[0-7]' AND \"api_key_secret_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "id": { + "name": "id", + "type": "text CHECK (\"id\" = upper(\"id\") AND length(\"id\") = 26 AND substr(\"id\", 1, 1) GLOB '[0-7]' AND \"id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "is_default": { + "name": "is_default", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "models": { + "name": "models", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "app_id": { + "name": "app_id", + "type": "text CHECK (\"app_id\" = upper(\"app_id\") AND length(\"app_id\") = 26 AND substr(\"app_id\", 1, 1) GLOB '[0-7]' AND \"app_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "vendor_id": { + "name": "vendor_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "vendor_credential_app_vendor_idx": { + "name": "vendor_credential_app_vendor_idx", + "columns": ["app_id", "vendor_id"], + "isUnique": false + }, + "vendor_credential_app_vendor_name_idx": { + "name": "vendor_credential_app_vendor_name_idx", + "columns": ["app_id", "vendor_id", "name"], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + } + }, + "views": {}, + "enums": {}, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + }, + "internal": { + "indexes": { + "file_record_listing_idx": { + "columns": { + "lower(\"name\")": { + "isExpression": true + } + } + }, + "session_run_terminal_reconciliation_attempt_idx": { + "columns": { + "coalesce(\"terminal_reconciliation_attempted_at\", \"updated_at\")": { + "isExpression": true + } + } + } + } + } +} diff --git a/pkgs/db/drizzle/meta/0018_snapshot.json b/pkgs/db/drizzle/meta/0018_snapshot.json new file mode 100644 index 00000000..25ff5934 --- /dev/null +++ b/pkgs/db/drizzle/meta/0018_snapshot.json @@ -0,0 +1,7228 @@ +{ + "version": "6", + "dialect": "sqlite", + "id": "d52f396d-150e-422a-8233-a53ca29c3cfc", + "prevId": "de5f1a98-662b-494a-bf74-542d94feddad", + "tables": { + "agent_deployment_version": { + "name": "agent_deployment_version", + "columns": { + "agent_id": { + "name": "agent_id", + "type": "text CHECK (\"agent_id\" = upper(\"agent_id\") AND length(\"agent_id\") = 26 AND substr(\"agent_id\", 1, 1) GLOB '[0-7]' AND \"agent_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "config_json": { + "name": "config_json", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_by_account_id": { + "name": "created_by_account_id", + "type": "text CHECK (\"created_by_account_id\" = upper(\"created_by_account_id\") AND length(\"created_by_account_id\") = 26 AND substr(\"created_by_account_id\", 1, 1) GLOB '[0-7]' AND \"created_by_account_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "environment_id": { + "name": "environment_id", + "type": "text CHECK (\"environment_id\" = upper(\"environment_id\") AND length(\"environment_id\") = 26 AND substr(\"environment_id\", 1, 1) GLOB '[0-7]' AND \"environment_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "id": { + "name": "id", + "type": "text CHECK (\"id\" = upper(\"id\") AND length(\"id\") = 26 AND substr(\"id\", 1, 1) GLOB '[0-7]' AND \"id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "mcp_bindings_json": { + "name": "mcp_bindings_json", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "prompt": { + "name": "prompt", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "runtime_id": { + "name": "runtime_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "skills_json": { + "name": "skills_json", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "summary": { + "name": "summary", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "version_number": { + "name": "version_number", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "agent_deployment_version_agent_number_idx": { + "name": "agent_deployment_version_agent_number_idx", + "columns": ["agent_id", "version_number"], + "isUnique": true + }, + "agent_deployment_version_agent_created_idx": { + "name": "agent_deployment_version_agent_created_idx", + "columns": ["agent_id", "created_at"], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "agent_mcp_binding": { + "name": "agent_mcp_binding", + "columns": { + "agent_credential_id": { + "name": "agent_credential_id", + "type": "text CHECK (\"agent_credential_id\" = upper(\"agent_credential_id\") AND length(\"agent_credential_id\") = 26 AND substr(\"agent_credential_id\", 1, 1) GLOB '[0-7]' AND \"agent_credential_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "agent_id": { + "name": "agent_id", + "type": "text CHECK (\"agent_id\" = upper(\"agent_id\") AND length(\"agent_id\") = 26 AND substr(\"agent_id\", 1, 1) GLOB '[0-7]' AND \"agent_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "credential_mode": { + "name": "credential_mode", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'runtime_resolved'" + }, + "enabled": { + "name": "enabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "id": { + "name": "id", + "type": "text CHECK (\"id\" = upper(\"id\") AND length(\"id\") = 26 AND substr(\"id\", 1, 1) GLOB '[0-7]' AND \"id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "server_id": { + "name": "server_id", + "type": "text CHECK (\"server_id\" = upper(\"server_id\") AND length(\"server_id\") = 26 AND substr(\"server_id\", 1, 1) GLOB '[0-7]' AND \"server_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "agent_mcp_binding_agent_sort_idx": { + "name": "agent_mcp_binding_agent_sort_idx", + "columns": ["agent_id", "sort_order"], + "isUnique": true + }, + "agent_mcp_binding_server_idx": { + "name": "agent_mcp_binding_server_idx", + "columns": ["server_id"], + "isUnique": false + }, + "agent_mcp_binding_profile_server_idx": { + "name": "agent_mcp_binding_profile_server_idx", + "columns": ["agent_id", "server_id"], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": { + "agent_mcp_binding_agent_credential_shape_check": { + "name": "agent_mcp_binding_agent_credential_shape_check", + "value": "\n (\"agent_mcp_binding\".\"credential_mode\" = 'agent_bound' AND \"agent_mcp_binding\".\"agent_credential_id\" IS NOT NULL)\n OR (\"agent_mcp_binding\".\"credential_mode\" = 'runtime_resolved' AND \"agent_mcp_binding\".\"agent_credential_id\" IS NULL)\n " + } + } + }, + "agent_skill": { + "name": "agent_skill", + "columns": { + "agent_id": { + "name": "agent_id", + "type": "text CHECK (\"agent_id\" = upper(\"agent_id\") AND length(\"agent_id\") = 26 AND substr(\"agent_id\", 1, 1) GLOB '[0-7]' AND \"agent_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "skill_id": { + "name": "skill_id", + "type": "text CHECK (\"skill_id\" = upper(\"skill_id\") AND length(\"skill_id\") = 26 AND substr(\"skill_id\", 1, 1) GLOB '[0-7]' AND \"skill_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "agent_skill_agent_sort_idx": { + "name": "agent_skill_agent_sort_idx", + "columns": ["agent_id", "sort_order"], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "agent_skill_agent_id_skill_id_pk": { + "columns": ["agent_id", "skill_id"], + "name": "agent_skill_agent_id_skill_id_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "agent": { + "name": "agent", + "columns": { + "config_json": { + "name": "config_json", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "environment_id": { + "name": "environment_id", + "type": "text CHECK (\"environment_id\" = upper(\"environment_id\") AND length(\"environment_id\") = 26 AND substr(\"environment_id\", 1, 1) GLOB '[0-7]' AND \"environment_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "id": { + "name": "id", + "type": "text CHECK (\"id\" = upper(\"id\") AND length(\"id\") = 26 AND substr(\"id\", 1, 1) GLOB '[0-7]' AND \"id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'pet'" + }, + "live_deployment_version_id": { + "name": "live_deployment_version_id", + "type": "text CHECK (\"live_deployment_version_id\" = upper(\"live_deployment_version_id\") AND length(\"live_deployment_version_id\") = 26 AND substr(\"live_deployment_version_id\", 1, 1) GLOB '[0-7]' AND \"live_deployment_version_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "owner_account_id": { + "name": "owner_account_id", + "type": "text CHECK (\"owner_account_id\" = upper(\"owner_account_id\") AND length(\"owner_account_id\") = 26 AND substr(\"owner_account_id\", 1, 1) GLOB '[0-7]' AND \"owner_account_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "app_id": { + "name": "app_id", + "type": "text CHECK (\"app_id\" = upper(\"app_id\") AND length(\"app_id\") = 26 AND substr(\"app_id\", 1, 1) GLOB '[0-7]' AND \"app_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "prompt": { + "name": "prompt", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "runtime_id": { + "name": "runtime_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'draft'" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "visibility": { + "name": "visibility", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'private'" + } + }, + "indexes": { + "agent_app_owner_account_idx": { + "name": "agent_app_owner_account_idx", + "columns": ["app_id", "owner_account_id"], + "isUnique": false + }, + "agent_app_status_idx": { + "name": "agent_app_status_idx", + "columns": ["app_id", "status"], + "isUnique": false + }, + "agent_environment_idx": { + "name": "agent_environment_idx", + "columns": ["environment_id"], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": { + "agent_published_live_deployment_version_check": { + "name": "agent_published_live_deployment_version_check", + "value": "\"agent\".\"status\" <> 'published' OR \"agent\".\"live_deployment_version_id\" IS NOT NULL" + } + } + }, + "api_command": { + "name": "api_command", + "columns": { + "attempt_count": { + "name": "attempt_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "claim_expires_at": { + "name": "claim_expires_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "claim_owner": { + "name": "claim_owner", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "completed_at": { + "name": "completed_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "dedupe_key": { + "name": "dedupe_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "id": { + "name": "id", + "type": "text CHECK (\"id\" = upper(\"id\") AND length(\"id\") = 26 AND substr(\"id\", 1, 1) GLOB '[0-7]' AND \"id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "last_error_code": { + "name": "last_error_code", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_error_message": { + "name": "last_error_message", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "payload_json": { + "name": "payload_json", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "api_command_dedupe_idx": { + "name": "api_command_dedupe_idx", + "columns": ["dedupe_key"], + "isUnique": true + }, + "api_command_status_updated_idx": { + "name": "api_command_status_updated_idx", + "columns": ["status", "updated_at"], + "isUnique": false + }, + "api_command_claim_idx": { + "name": "api_command_claim_idx", + "columns": ["status", "claim_expires_at"], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "auth_account": { + "name": "auth_account", + "columns": { + "access_token": { + "name": "access_token", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "access_token_expires_at": { + "name": "access_token_expires_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "provider_account_id": { + "name": "provider_account_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "id_token": { + "name": "id_token", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "refresh_token": { + "name": "refresh_token", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "refresh_token_expires_at": { + "name": "refresh_token_expires_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "account_id": { + "name": "account_id", + "type": "text CHECK (\"account_id\" = upper(\"account_id\") AND length(\"account_id\") = 26 AND substr(\"account_id\", 1, 1) GLOB '[0-7]' AND \"account_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "auth_account_provider_account_idx": { + "name": "auth_account_provider_account_idx", + "columns": ["provider_id", "provider_account_id"], + "isUnique": true + }, + "auth_account_account_id_idx": { + "name": "auth_account_account_id_idx", + "columns": ["account_id"], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "auth_session": { + "name": "auth_session", + "columns": { + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "ip_address": { + "name": "ip_address", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "account_id": { + "name": "account_id", + "type": "text CHECK (\"account_id\" = upper(\"account_id\") AND length(\"account_id\") = 26 AND substr(\"account_id\", 1, 1) GLOB '[0-7]' AND \"account_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "auth_session_expires_at_idx": { + "name": "auth_session_expires_at_idx", + "columns": ["expires_at"], + "isUnique": false + }, + "auth_session_token_idx": { + "name": "auth_session_token_idx", + "columns": ["token"], + "isUnique": true + }, + "auth_session_account_id_idx": { + "name": "auth_session_account_id_idx", + "columns": ["account_id"], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "auth_verification": { + "name": "auth_verification", + "columns": { + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "identifier": { + "name": "identifier", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "auth_verification_expires_at_idx": { + "name": "auth_verification_expires_at_idx", + "columns": ["expires_at"], + "isUnique": false + }, + "auth_verification_identifier_idx": { + "name": "auth_verification_identifier_idx", + "columns": ["identifier"], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "cli_oauth_flow": { + "name": "cli_oauth_flow", + "columns": { + "account_id": { + "name": "account_id", + "type": "text CHECK (\"account_id\" = upper(\"account_id\") AND length(\"account_id\") = 26 AND substr(\"account_id\", 1, 1) GLOB '[0-7]' AND \"account_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "authorized_at": { + "name": "authorized_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "completed_at": { + "name": "completed_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "device_code_hash": { + "name": "device_code_hash", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "hostname": { + "name": "hostname", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "id": { + "name": "id", + "type": "text CHECK (\"id\" = upper(\"id\") AND length(\"id\") = 26 AND substr(\"id\", 1, 1) GLOB '[0-7]' AND \"id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_code": { + "name": "user_code", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "cli_oauth_flow_status_expires_idx": { + "name": "cli_oauth_flow_status_expires_idx", + "columns": ["status", "expires_at"], + "isUnique": false + }, + "cli_oauth_flow_device_code_hash_idx": { + "name": "cli_oauth_flow_device_code_hash_idx", + "columns": ["device_code_hash"], + "isUnique": true + }, + "cli_oauth_flow_user_code_idx": { + "name": "cli_oauth_flow_user_code_idx", + "columns": ["user_code"], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "personal_access_token": { + "name": "personal_access_token", + "columns": { + "account_id": { + "name": "account_id", + "type": "text CHECK (\"account_id\" = upper(\"account_id\") AND length(\"account_id\") = 26 AND substr(\"account_id\", 1, 1) GLOB '[0-7]' AND \"account_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "id": { + "name": "id", + "type": "text CHECK (\"id\" = upper(\"id\") AND length(\"id\") = 26 AND substr(\"id\", 1, 1) GLOB '[0-7]' AND \"id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "last_used_at": { + "name": "last_used_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "revoked_at": { + "name": "revoked_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "token_hash": { + "name": "token_hash", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "personal_access_token_account_created_idx": { + "name": "personal_access_token_account_created_idx", + "columns": ["account_id", "created_at"], + "isUnique": false + }, + "personal_access_token_hash_idx": { + "name": "personal_access_token_hash_idx", + "columns": ["token_hash"], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "email_log": { + "name": "email_log", + "columns": { + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "recipient_domain": { + "name": "recipient_domain", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "recipient_masked": { + "name": "recipient_masked", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "subject": { + "name": "subject", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "email_log_created_at_idx": { + "name": "email_log_created_at_idx", + "columns": ["created_at"], + "isUnique": false + }, + "email_log_type_status_idx": { + "name": "email_log_type_status_idx", + "columns": ["type", "status"], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "environment_revision": { + "name": "environment_revision", + "columns": { + "allow_mcp_servers": { + "name": "allow_mcp_servers", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "allow_package_managers": { + "name": "allow_package_managers", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "allowed_hosts_json": { + "name": "allowed_hosts_json", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_by_account_id": { + "name": "created_by_account_id", + "type": "text CHECK (\"created_by_account_id\" = upper(\"created_by_account_id\") AND length(\"created_by_account_id\") = 26 AND substr(\"created_by_account_id\", 1, 1) GLOB '[0-7]' AND \"created_by_account_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "env_vars_json": { + "name": "env_vars_json", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "environment_id": { + "name": "environment_id", + "type": "text CHECK (\"environment_id\" = upper(\"environment_id\") AND length(\"environment_id\") = 26 AND substr(\"environment_id\", 1, 1) GLOB '[0-7]' AND \"environment_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "id": { + "name": "id", + "type": "text CHECK (\"id\" = upper(\"id\") AND length(\"id\") = 26 AND substr(\"id\", 1, 1) GLOB '[0-7]' AND \"id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "network_policy": { + "name": "network_policy", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "packages_json": { + "name": "packages_json", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "app_id": { + "name": "app_id", + "type": "text CHECK (\"app_id\" = upper(\"app_id\") AND length(\"app_id\") = 26 AND substr(\"app_id\", 1, 1) GLOB '[0-7]' AND \"app_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "setup_script": { + "name": "setup_script", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "environment_revision_environment_created_at_idx": { + "name": "environment_revision_environment_created_at_idx", + "columns": ["environment_id", "created_at"], + "isUnique": false + }, + "environment_revision_app_created_at_idx": { + "name": "environment_revision_app_created_at_idx", + "columns": ["app_id", "created_at"], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": { + "environment_revision_network_policy_check": { + "name": "environment_revision_network_policy_check", + "value": "\"environment_revision\".\"network_policy\" IN ('full', 'limited')" + } + } + }, + "environment": { + "name": "environment", + "columns": { + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "current_revision_id": { + "name": "current_revision_id", + "type": "text CHECK (\"current_revision_id\" = upper(\"current_revision_id\") AND length(\"current_revision_id\") = 26 AND substr(\"current_revision_id\", 1, 1) GLOB '[0-7]' AND \"current_revision_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "forked_from_environment_id": { + "name": "forked_from_environment_id", + "type": "text CHECK (\"forked_from_environment_id\" = upper(\"forked_from_environment_id\") AND length(\"forked_from_environment_id\") = 26 AND substr(\"forked_from_environment_id\", 1, 1) GLOB '[0-7]' AND \"forked_from_environment_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "forked_from_environment_name": { + "name": "forked_from_environment_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "forked_from_owner_name": { + "name": "forked_from_owner_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "id": { + "name": "id", + "type": "text CHECK (\"id\" = upper(\"id\") AND length(\"id\") = 26 AND substr(\"id\", 1, 1) GLOB '[0-7]' AND \"id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "owner_account_id": { + "name": "owner_account_id", + "type": "text CHECK (\"owner_account_id\" = upper(\"owner_account_id\") AND length(\"owner_account_id\") = 26 AND substr(\"owner_account_id\", 1, 1) GLOB '[0-7]' AND \"owner_account_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "app_id": { + "name": "app_id", + "type": "text CHECK (\"app_id\" = upper(\"app_id\") AND length(\"app_id\") = 26 AND substr(\"app_id\", 1, 1) GLOB '[0-7]' AND \"app_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "environment_app_updated_at_idx": { + "name": "environment_app_updated_at_idx", + "columns": ["app_id", "updated_at"], + "isUnique": false + }, + "environment_owner_updated_at_idx": { + "name": "environment_owner_updated_at_idx", + "columns": ["owner_account_id", "updated_at"], + "isUnique": false + }, + "environment_owner_name_idx": { + "name": "environment_owner_name_idx", + "columns": ["app_id", "owner_account_id", "name"], + "isUnique": true, + "where": "\"environment\".\"owner_account_id\" IS NOT NULL" + }, + "environment_system_default_idx": { + "name": "environment_system_default_idx", + "columns": ["app_id"], + "isUnique": true, + "where": "\"environment\".\"owner_account_id\" IS NULL" + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "file_record": { + "name": "file_record", + "columns": { + "committed": { + "name": "committed", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_by_account_id": { + "name": "created_by_account_id", + "type": "text CHECK (\"created_by_account_id\" = upper(\"created_by_account_id\") AND length(\"created_by_account_id\") = 26 AND substr(\"created_by_account_id\", 1, 1) GLOB '[0-7]' AND \"created_by_account_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "etag": { + "name": "etag", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "id": { + "name": "id", + "type": "text CHECK (\"id\" = upper(\"id\") AND length(\"id\") = 26 AND substr(\"id\", 1, 1) GLOB '[0-7]' AND \"id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "mime_type": { + "name": "mime_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "object_key": { + "name": "object_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "owner_id": { + "name": "owner_id", + "type": "text CHECK (\"owner_id\" = upper(\"owner_id\") AND length(\"owner_id\") = 26 AND substr(\"owner_id\", 1, 1) GLOB '[0-7]' AND \"owner_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "owner_kind": { + "name": "owner_kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "parent_path": { + "name": "parent_path", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "path": { + "name": "path", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "purpose": { + "name": "purpose", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "runtime_event_seq": { + "name": "runtime_event_seq", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "scope_id": { + "name": "scope_id", + "type": "text CHECK (\"scope_id\" = upper(\"scope_id\") AND length(\"scope_id\") = 26 AND substr(\"scope_id\", 1, 1) GLOB '[0-7]' AND \"scope_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "scope_kind": { + "name": "scope_kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "session_kind": { + "name": "session_kind", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "size": { + "name": "size", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "version": { + "name": "version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "file_record_runtime_event_seq_idx": { + "name": "file_record_runtime_event_seq_idx", + "columns": ["scope_id", "runtime_event_seq"], + "isUnique": false + }, + "file_record_object_key_idx": { + "name": "file_record_object_key_idx", + "columns": ["object_key"], + "isUnique": true + }, + "file_record_unscoped_parent_path_name_status_idx": { + "name": "file_record_unscoped_parent_path_name_status_idx", + "columns": ["scope_kind", "parent_path", "name", "status"], + "isUnique": true, + "where": "\"file_record\".\"scope_id\" IS NULL" + }, + "file_record_scoped_parent_path_name_status_idx": { + "name": "file_record_scoped_parent_path_name_status_idx", + "columns": ["scope_kind", "scope_id", "parent_path", "name", "status"], + "isUnique": true + }, + "file_record_unscoped_pending_path_idx": { + "name": "file_record_unscoped_pending_path_idx", + "columns": ["scope_kind", "path"], + "isUnique": true, + "where": "\"file_record\".\"status\" = 'pending' AND \"file_record\".\"scope_id\" IS NULL" + }, + "file_record_scoped_pending_path_idx": { + "name": "file_record_scoped_pending_path_idx", + "columns": ["scope_kind", "scope_id", "path"], + "isUnique": true, + "where": "\"file_record\".\"status\" = 'pending' AND \"file_record\".\"scope_id\" IS NOT NULL" + }, + "file_record_unscoped_ready_path_idx": { + "name": "file_record_unscoped_ready_path_idx", + "columns": ["scope_kind", "path"], + "isUnique": true, + "where": "\"file_record\".\"status\" = 'ready' AND \"file_record\".\"scope_id\" IS NULL" + }, + "file_record_scoped_ready_path_idx": { + "name": "file_record_scoped_ready_path_idx", + "columns": ["scope_kind", "scope_id", "path"], + "isUnique": true, + "where": "\"file_record\".\"status\" = 'ready' AND \"file_record\".\"scope_id\" IS NOT NULL" + }, + "file_record_governance_idx": { + "name": "file_record_governance_idx", + "columns": ["purpose", "owner_kind", "owner_id", "status", "expires_at"], + "isUnique": false + }, + "file_record_listing_idx": { + "name": "file_record_listing_idx", + "columns": ["scope_kind", "scope_id", "parent_path", "status", "lower(\"name\")"], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": { + "file_record_runtime_event_seq_check": { + "name": "file_record_runtime_event_seq_check", + "value": "\"file_record\".\"runtime_event_seq\" IS NULL OR \"file_record\".\"runtime_event_seq\" >= 0" + } + } + }, + "file_upload": { + "name": "file_upload", + "columns": { + "content_type": { + "name": "content_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_by_account_id": { + "name": "created_by_account_id", + "type": "text CHECK (\"created_by_account_id\" = upper(\"created_by_account_id\") AND length(\"created_by_account_id\") = 26 AND substr(\"created_by_account_id\", 1, 1) GLOB '[0-7]' AND \"created_by_account_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expected_size": { + "name": "expected_size", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "file_id": { + "name": "file_id", + "type": "text CHECK (\"file_id\" = upper(\"file_id\") AND length(\"file_id\") = 26 AND substr(\"file_id\", 1, 1) GLOB '[0-7]' AND \"file_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "id": { + "name": "id", + "type": "text CHECK (\"id\" = upper(\"id\") AND length(\"id\") = 26 AND substr(\"id\", 1, 1) GLOB '[0-7]' AND \"id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "if_match_etag": { + "name": "if_match_etag", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "multipart_upload_id": { + "name": "multipart_upload_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "overwrite": { + "name": "overwrite", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "part_size": { + "name": "part_size", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "scope_id": { + "name": "scope_id", + "type": "text CHECK (\"scope_id\" = upper(\"scope_id\") AND length(\"scope_id\") = 26 AND substr(\"scope_id\", 1, 1) GLOB '[0-7]' AND \"scope_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "scope_kind": { + "name": "scope_kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "strategy": { + "name": "strategy", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "file_upload_file_id_idx": { + "name": "file_upload_file_id_idx", + "columns": ["file_id"], + "isUnique": true + }, + "file_upload_status_expires_idx": { + "name": "file_upload_status_expires_idx", + "columns": ["status", "expires_at"], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "file_version": { + "name": "file_version", + "columns": { + "committed": { + "name": "committed", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "committed_at": { + "name": "committed_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_by_account_id": { + "name": "created_by_account_id", + "type": "text CHECK (\"created_by_account_id\" = upper(\"created_by_account_id\") AND length(\"created_by_account_id\") = 26 AND substr(\"created_by_account_id\", 1, 1) GLOB '[0-7]' AND \"created_by_account_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "file_id": { + "name": "file_id", + "type": "text CHECK (\"file_id\" = upper(\"file_id\") AND length(\"file_id\") = 26 AND substr(\"file_id\", 1, 1) GLOB '[0-7]' AND \"file_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "id": { + "name": "id", + "type": "text CHECK (\"id\" = upper(\"id\") AND length(\"id\") = 26 AND substr(\"id\", 1, 1) GLOB '[0-7]' AND \"id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "mime_type": { + "name": "mime_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "object_key": { + "name": "object_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "path": { + "name": "path", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "reason": { + "name": "reason", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "scope_id": { + "name": "scope_id", + "type": "text CHECK (\"scope_id\" = upper(\"scope_id\") AND length(\"scope_id\") = 26 AND substr(\"scope_id\", 1, 1) GLOB '[0-7]' AND \"scope_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "scope_kind": { + "name": "scope_kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "size": { + "name": "size", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "source_etag": { + "name": "source_etag", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "source_object_key": { + "name": "source_object_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "version": { + "name": "version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "file_version_object_key_idx": { + "name": "file_version_object_key_idx", + "columns": ["object_key"], + "isUnique": true + }, + "file_version_scope_path_created_idx": { + "name": "file_version_scope_path_created_idx", + "columns": ["scope_kind", "scope_id", "path", "created_at"], + "isUnique": false + }, + "file_version_file_created_idx": { + "name": "file_version_file_created_idx", + "columns": ["file_id", "created_at"], + "isUnique": false + }, + "file_version_pending_idx": { + "name": "file_version_pending_idx", + "columns": ["committed", "created_at"], + "isUnique": false, + "where": "\"file_version\".\"committed\" = 0" + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "runtime_artifact_attempt": { + "name": "runtime_artifact_attempt", + "columns": { + "accepted_event_id": { + "name": "accepted_event_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_by_account_id": { + "name": "created_by_account_id", + "type": "text CHECK (\"created_by_account_id\" = upper(\"created_by_account_id\") AND length(\"created_by_account_id\") = 26 AND substr(\"created_by_account_id\", 1, 1) GLOB '[0-7]' AND \"created_by_account_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "delete_after": { + "name": "delete_after", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "driver_connection_id": { + "name": "driver_connection_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "driver_generation": { + "name": "driver_generation", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "driver_instance_id": { + "name": "driver_instance_id", + "type": "text CHECK (\"driver_instance_id\" = upper(\"driver_instance_id\") AND length(\"driver_instance_id\") = 26 AND substr(\"driver_instance_id\", 1, 1) GLOB '[0-7]' AND \"driver_instance_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "event_type": { + "name": "event_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "manifest_json": { + "name": "manifest_json", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "manifest_sha256": { + "name": "manifest_sha256", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "owned_object_keys_json": { + "name": "owned_object_keys_json", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'[]'" + }, + "run_id": { + "name": "run_id", + "type": "text CHECK (\"run_id\" = upper(\"run_id\") AND length(\"run_id\") = 26 AND substr(\"run_id\", 1, 1) GLOB '[0-7]' AND \"run_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "semantic_hash": { + "name": "semantic_hash", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "session_id": { + "name": "session_id", + "type": "text CHECK (\"session_id\" = upper(\"session_id\") AND length(\"session_id\") = 26 AND substr(\"session_id\", 1, 1) GLOB '[0-7]' AND \"session_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "source_event_id": { + "name": "source_event_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "runtime_artifact_attempt_accepted_event_idx": { + "name": "runtime_artifact_attempt_accepted_event_idx", + "columns": ["accepted_event_id"], + "isUnique": true, + "where": "\"runtime_artifact_attempt\".\"accepted_event_id\" IS NOT NULL" + }, + "runtime_artifact_attempt_cleanup_idx": { + "name": "runtime_artifact_attempt_cleanup_idx", + "columns": ["status", "expires_at", "updated_at", "id"], + "isUnique": false + }, + "runtime_artifact_attempt_session_status_idx": { + "name": "runtime_artifact_attempt_session_status_idx", + "columns": ["session_id", "status", "id"], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": { + "runtime_artifact_attempt_manifest_check": { + "name": "runtime_artifact_attempt_manifest_check", + "value": "(\"runtime_artifact_attempt\".\"manifest_json\" IS NULL AND \"runtime_artifact_attempt\".\"manifest_sha256\" IS NULL) OR (\"runtime_artifact_attempt\".\"manifest_json\" IS NOT NULL AND json_valid(\"runtime_artifact_attempt\".\"manifest_json\") = 1 AND json_extract(\"runtime_artifact_attempt\".\"manifest_json\", '$.version') IS 1 AND json_type(\"runtime_artifact_attempt\".\"manifest_json\", '$.captureStatus') IS 'text' AND json_extract(\"runtime_artifact_attempt\".\"manifest_json\", '$.captureStatus') IN ('complete', 'omitted_file_limit', 'omitted_runtime_unavailable', 'omitted_size_limit', 'omitted_source_changed', 'omitted_source_missing') AND json_type(\"runtime_artifact_attempt\".\"manifest_json\", '$.mode') IS 'text' AND json_extract(\"runtime_artifact_attempt\".\"manifest_json\", '$.mode') IN ('delta', 'snapshot') AND (json_extract(\"runtime_artifact_attempt\".\"manifest_json\", '$.captureStatus') = 'complete' OR json_array_length(\"runtime_artifact_attempt\".\"manifest_json\", '$.files') = 0) AND json_extract(\"runtime_artifact_attempt\".\"manifest_json\", '$.sourceEventId') IS \"runtime_artifact_attempt\".\"source_event_id\" AND json_extract(\"runtime_artifact_attempt\".\"manifest_json\", '$.semanticHash') IS \"runtime_artifact_attempt\".\"semantic_hash\" AND json_type(\"runtime_artifact_attempt\".\"manifest_json\", '$.files') IS 'array' AND \"runtime_artifact_attempt\".\"manifest_sha256\" IS NOT NULL AND length(\"runtime_artifact_attempt\".\"manifest_sha256\") = 64 AND \"runtime_artifact_attempt\".\"manifest_sha256\" = lower(\"runtime_artifact_attempt\".\"manifest_sha256\") AND \"runtime_artifact_attempt\".\"manifest_sha256\" NOT GLOB '*[^0-9a-f]*')" + }, + "runtime_artifact_attempt_owned_keys_check": { + "name": "runtime_artifact_attempt_owned_keys_check", + "value": "json_valid(\"runtime_artifact_attempt\".\"owned_object_keys_json\") = 1 AND json_type(\"runtime_artifact_attempt\".\"owned_object_keys_json\") IS 'array'" + }, + "runtime_artifact_attempt_semantic_hash_check": { + "name": "runtime_artifact_attempt_semantic_hash_check", + "value": "length(\"runtime_artifact_attempt\".\"semantic_hash\") = 64 AND \"runtime_artifact_attempt\".\"semantic_hash\" = lower(\"runtime_artifact_attempt\".\"semantic_hash\") AND \"runtime_artifact_attempt\".\"semantic_hash\" NOT GLOB '*[^0-9a-f]*'" + }, + "runtime_artifact_attempt_status_check": { + "name": "runtime_artifact_attempt_status_check", + "value": "(\"runtime_artifact_attempt\".\"status\" = 'staging' AND \"runtime_artifact_attempt\".\"manifest_json\" IS NULL AND \"runtime_artifact_attempt\".\"accepted_event_id\" IS NULL AND \"runtime_artifact_attempt\".\"expires_at\" IS NOT NULL AND \"runtime_artifact_attempt\".\"delete_after\" IS NULL) OR (\"runtime_artifact_attempt\".\"status\" = 'staged' AND \"runtime_artifact_attempt\".\"manifest_json\" IS NOT NULL AND \"runtime_artifact_attempt\".\"accepted_event_id\" IS NULL AND \"runtime_artifact_attempt\".\"expires_at\" IS NOT NULL AND \"runtime_artifact_attempt\".\"delete_after\" IS NULL) OR (\"runtime_artifact_attempt\".\"status\" = 'accepted' AND \"runtime_artifact_attempt\".\"manifest_json\" IS NOT NULL AND \"runtime_artifact_attempt\".\"accepted_event_id\" IS NOT NULL AND \"runtime_artifact_attempt\".\"expires_at\" IS NULL AND \"runtime_artifact_attempt\".\"delete_after\" IS NULL AND json_array_length(\"runtime_artifact_attempt\".\"owned_object_keys_json\") = 0) OR (\"runtime_artifact_attempt\".\"status\" = 'deleting' AND \"runtime_artifact_attempt\".\"accepted_event_id\" IS NULL AND \"runtime_artifact_attempt\".\"delete_after\" IS NOT NULL)" + }, + "runtime_artifact_attempt_time_check": { + "name": "runtime_artifact_attempt_time_check", + "value": "\"runtime_artifact_attempt\".\"driver_generation\" >= 0 AND (\"runtime_artifact_attempt\".\"expires_at\" IS NULL OR \"runtime_artifact_attempt\".\"expires_at\" >= \"runtime_artifact_attempt\".\"created_at\") AND (\"runtime_artifact_attempt\".\"delete_after\" IS NULL OR \"runtime_artifact_attempt\".\"delete_after\" >= \"runtime_artifact_attempt\".\"created_at\") AND \"runtime_artifact_attempt\".\"updated_at\" >= \"runtime_artifact_attempt\".\"created_at\"" + } + } + }, + "session_artifact_head": { + "name": "session_artifact_head", + "columns": { + "file_id": { + "name": "file_id", + "type": "text CHECK (\"file_id\" = upper(\"file_id\") AND length(\"file_id\") = 26 AND substr(\"file_id\", 1, 1) GLOB '[0-7]' AND \"file_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "runtime_event_seq": { + "name": "runtime_event_seq", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "session_id": { + "name": "session_id", + "type": "text CHECK (\"session_id\" = upper(\"session_id\") AND length(\"session_id\") = 26 AND substr(\"session_id\", 1, 1) GLOB '[0-7]' AND \"session_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "source_event_id": { + "name": "source_event_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "source_path": { + "name": "source_path", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "session_artifact_head_session_path_idx": { + "name": "session_artifact_head_session_path_idx", + "columns": ["session_id", "source_path"], + "isUnique": true + }, + "session_artifact_head_session_seq_idx": { + "name": "session_artifact_head_session_seq_idx", + "columns": ["session_id", "runtime_event_seq", "source_path"], + "isUnique": false + } + }, + "foreignKeys": { + "session_artifact_head_session_id_session_id_fk": { + "name": "session_artifact_head_session_id_session_id_fk", + "tableFrom": "session_artifact_head", + "tableTo": "session", + "columnsFrom": ["session_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": { + "session_artifact_head_path_check": { + "name": "session_artifact_head_path_check", + "value": "length(\"session_artifact_head\".\"source_path\") > 8 AND substr(\"session_artifact_head\".\"source_path\", 1, 8) = 'outputs/' AND instr(\"session_artifact_head\".\"source_path\", char(0)) = 0 AND instr(\"session_artifact_head\".\"source_path\", '\\') = 0 AND \"session_artifact_head\".\"source_path\" NOT LIKE '%//%' AND \"session_artifact_head\".\"source_path\" NOT LIKE '%/./%' AND \"session_artifact_head\".\"source_path\" NOT LIKE '%/.' AND \"session_artifact_head\".\"source_path\" NOT LIKE '%/../%' AND \"session_artifact_head\".\"source_path\" NOT LIKE '%/..'" + }, + "session_artifact_head_seq_check": { + "name": "session_artifact_head_seq_check", + "value": "\"session_artifact_head\".\"runtime_event_seq\" >= 0 AND \"session_artifact_head\".\"updated_at\" >= 0" + } + } + }, + "mcp_credential": { + "name": "mcp_credential", + "columns": { + "account_id": { + "name": "account_id", + "type": "text CHECK (\"account_id\" = upper(\"account_id\") AND length(\"account_id\") = 26 AND substr(\"account_id\", 1, 1) GLOB '[0-7]' AND \"account_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "agent_id": { + "name": "agent_id", + "type": "text CHECK (\"agent_id\" = upper(\"agent_id\") AND length(\"agent_id\") = 26 AND substr(\"agent_id\", 1, 1) GLOB '[0-7]' AND \"agent_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "auth_type": { + "name": "auth_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "id": { + "name": "id", + "type": "text CHECK (\"id\" = upper(\"id\") AND length(\"id\") = 26 AND substr(\"id\", 1, 1) GLOB '[0-7]' AND \"id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "last_refreshed_at": { + "name": "last_refreshed_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "oauth_client_id": { + "name": "oauth_client_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "oauth_client_secret_secret_id": { + "name": "oauth_client_secret_secret_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "app_id": { + "name": "app_id", + "type": "text CHECK (\"app_id\" = upper(\"app_id\") AND length(\"app_id\") = 26 AND substr(\"app_id\", 1, 1) GLOB '[0-7]' AND \"app_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "refresh_secret_id": { + "name": "refresh_secret_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "scope_values_json": { + "name": "scope_values_json", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "secret_id": { + "name": "secret_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "server_id": { + "name": "server_id", + "type": "text CHECK (\"server_id\" = upper(\"server_id\") AND length(\"server_id\") = 26 AND substr(\"server_id\", 1, 1) GLOB '[0-7]' AND \"server_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "subject_label": { + "name": "subject_label", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "mcp_credential_server_scope_status_idx": { + "name": "mcp_credential_server_scope_status_idx", + "columns": ["server_id", "scope", "status"], + "isUnique": false + }, + "mcp_credential_app_scope_status_idx": { + "name": "mcp_credential_app_scope_status_idx", + "columns": ["app_id", "scope", "status"], + "isUnique": false + }, + "mcp_credential_app_scope_idx": { + "name": "mcp_credential_app_scope_idx", + "columns": ["server_id", "scope"], + "isUnique": true, + "where": "\"mcp_credential\".\"scope\" = 'app'" + }, + "mcp_credential_agent_scope_idx": { + "name": "mcp_credential_agent_scope_idx", + "columns": ["server_id", "agent_id", "scope"], + "isUnique": true, + "where": "\"mcp_credential\".\"scope\" = 'agent' AND \"mcp_credential\".\"agent_id\" IS NOT NULL" + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": { + "mcp_credential_scope_shape_check": { + "name": "mcp_credential_scope_shape_check", + "value": "\n (\"mcp_credential\".\"scope\" = 'app' AND \"mcp_credential\".\"account_id\" IS NULL AND \"mcp_credential\".\"agent_id\" IS NULL)\n OR (\"mcp_credential\".\"scope\" = 'agent' AND \"mcp_credential\".\"account_id\" IS NULL AND \"mcp_credential\".\"agent_id\" IS NOT NULL)\n " + }, + "mcp_credential_scope_values_json_check": { + "name": "mcp_credential_scope_values_json_check", + "value": "\n \"mcp_credential\".\"scope_values_json\" IS NULL\n OR (json_valid(\"mcp_credential\".\"scope_values_json\") AND json_type(\"mcp_credential\".\"scope_values_json\") = 'array')\n " + }, + "mcp_credential_bearer_shape_check": { + "name": "mcp_credential_bearer_shape_check", + "value": "\n \"mcp_credential\".\"auth_type\" != 'bearer'\n OR (\n \"mcp_credential\".\"oauth_client_id\" IS NULL\n AND \"mcp_credential\".\"oauth_client_secret_secret_id\" IS NULL\n AND \"mcp_credential\".\"refresh_secret_id\" IS NULL\n )\n " + } + } + }, + "mcp_oauth_flow": { + "name": "mcp_oauth_flow", + "columns": { + "authorization_endpoint": { + "name": "authorization_endpoint", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "cleanup_after": { + "name": "cleanup_after", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "code_verifier": { + "name": "code_verifier", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "completed_at": { + "name": "completed_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "id": { + "name": "id", + "type": "text CHECK (\"id\" = upper(\"id\") AND length(\"id\") = 26 AND substr(\"id\", 1, 1) GLOB '[0-7]' AND \"id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "initiator_account_id": { + "name": "initiator_account_id", + "type": "text CHECK (\"initiator_account_id\" = upper(\"initiator_account_id\") AND length(\"initiator_account_id\") = 26 AND substr(\"initiator_account_id\", 1, 1) GLOB '[0-7]' AND \"initiator_account_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "oauth_client_id": { + "name": "oauth_client_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "oauth_client_secret_secret_id": { + "name": "oauth_client_secret_secret_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "app_id": { + "name": "app_id", + "type": "text CHECK (\"app_id\" = upper(\"app_id\") AND length(\"app_id\") = 26 AND substr(\"app_id\", 1, 1) GLOB '[0-7]' AND \"app_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "registration_endpoint": { + "name": "registration_endpoint", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "return_url": { + "name": "return_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "scope_values_json": { + "name": "scope_values_json", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "server_id": { + "name": "server_id", + "type": "text CHECK (\"server_id\" = upper(\"server_id\") AND length(\"server_id\") = 26 AND substr(\"server_id\", 1, 1) GLOB '[0-7]' AND \"server_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "subject_label": { + "name": "subject_label", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "token_endpoint": { + "name": "token_endpoint", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "mcp_oauth_flow_status_cleanup_after_idx": { + "name": "mcp_oauth_flow_status_cleanup_after_idx", + "columns": ["status", "cleanup_after"], + "isUnique": false + }, + "mcp_oauth_flow_expires_at_idx": { + "name": "mcp_oauth_flow_expires_at_idx", + "columns": ["expires_at"], + "isUnique": false + }, + "mcp_oauth_flow_server_account_idx": { + "name": "mcp_oauth_flow_server_account_idx", + "columns": ["server_id", "initiator_account_id"], + "isUnique": false + }, + "mcp_oauth_flow_app_server_account_idx": { + "name": "mcp_oauth_flow_app_server_account_idx", + "columns": ["app_id", "server_id", "initiator_account_id"], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": { + "mcp_oauth_flow_scope_values_json_check": { + "name": "mcp_oauth_flow_scope_values_json_check", + "value": "\n \"mcp_oauth_flow\".\"scope_values_json\" IS NULL\n OR (json_valid(\"mcp_oauth_flow\".\"scope_values_json\") AND json_type(\"mcp_oauth_flow\".\"scope_values_json\") = 'array')\n " + } + } + }, + "mcp_server": { + "name": "mcp_server", + "columns": { + "auth_type": { + "name": "auth_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "byo_client_id": { + "name": "byo_client_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "byo_client_secret_secret_id": { + "name": "byo_client_secret_secret_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "credential_scope": { + "name": "credential_scope", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "enabled": { + "name": "enabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "icon_url": { + "name": "icon_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "id": { + "name": "id", + "type": "text CHECK (\"id\" = upper(\"id\") AND length(\"id\") = 26 AND substr(\"id\", 1, 1) GLOB '[0-7]' AND \"id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "oauth_metadata_json": { + "name": "oauth_metadata_json", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "owner_account_id": { + "name": "owner_account_id", + "type": "text CHECK (\"owner_account_id\" = upper(\"owner_account_id\") AND length(\"owner_account_id\") = 26 AND substr(\"owner_account_id\", 1, 1) GLOB '[0-7]' AND \"owner_account_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "app_id": { + "name": "app_id", + "type": "text CHECK (\"app_id\" = upper(\"app_id\") AND length(\"app_id\") = 26 AND substr(\"app_id\", 1, 1) GLOB '[0-7]' AND \"app_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "mcp_server_app_enabled_idx": { + "name": "mcp_server_app_enabled_idx", + "columns": ["app_id", "enabled"], + "isUnique": false + }, + "mcp_server_owner_app_idx": { + "name": "mcp_server_owner_app_idx", + "columns": ["owner_account_id", "app_id"], + "isUnique": false + }, + "mcp_server_app_url_idx": { + "name": "mcp_server_app_url_idx", + "columns": ["app_id", "url"], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": { + "mcp_server_source_scope_check": { + "name": "mcp_server_source_scope_check", + "value": "\"mcp_server\".\"source\" = 'app' AND \"mcp_server\".\"credential_scope\" = 'app'" + } + } + }, + "vault_secret": { + "name": "vault_secret", + "columns": { + "algorithm": { + "name": "algorithm", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'AES-GCM'" + }, + "ciphertext": { + "name": "ciphertext", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "ciphertext_iv": { + "name": "ciphertext_iv", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "id": { + "name": "id", + "type": "text CHECK (\"id\" = upper(\"id\") AND length(\"id\") = 26 AND substr(\"id\", 1, 1) GLOB '[0-7]' AND \"id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "wrapped_dek": { + "name": "wrapped_dek", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "wrapped_dek_iv": { + "name": "wrapped_dek_iv", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "vault_secret_kind_created_at_idx": { + "name": "vault_secret_kind_created_at_idx", + "columns": ["kind", "created_at"], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "organization": { + "name": "organization", + "columns": { + "avatar_url": { + "name": "avatar_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "creator_account_id": { + "name": "creator_account_id", + "type": "text CHECK (\"creator_account_id\" = upper(\"creator_account_id\") AND length(\"creator_account_id\") = 26 AND substr(\"creator_account_id\", 1, 1) GLOB '[0-7]' AND \"creator_account_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "id": { + "name": "id", + "type": "text CHECK (\"id\" = upper(\"id\") AND length(\"id\") = 26 AND substr(\"id\", 1, 1) GLOB '[0-7]' AND \"id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "organization_creator_account_idx": { + "name": "organization_creator_account_idx", + "columns": ["creator_account_id"], + "isUnique": true, + "where": "\"organization\".\"creator_account_id\" IS NOT NULL" + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "app_deployment_run": { + "name": "app_deployment_run", + "columns": { + "app_id": { + "name": "app_id", + "type": "text CHECK (\"app_id\" = upper(\"app_id\") AND length(\"app_id\") = 26 AND substr(\"app_id\", 1, 1) GLOB '[0-7]' AND \"app_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "deployment_id": { + "name": "deployment_id", + "type": "text CHECK (\"deployment_id\" = upper(\"deployment_id\") AND length(\"deployment_id\") = 26 AND substr(\"deployment_id\", 1, 1) GLOB '[0-7]' AND \"deployment_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "error_code": { + "name": "error_code", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "external_deployment_id": { + "name": "external_deployment_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "external_project_id": { + "name": "external_project_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "external_version_id": { + "name": "external_version_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "generated_wrangler_config_json": { + "name": "generated_wrangler_config_json", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "id": { + "name": "id", + "type": "text CHECK (\"id\" = upper(\"id\") AND length(\"id\") = 26 AND substr(\"id\", 1, 1) GLOB '[0-7]' AND \"id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "mosoo_config_json": { + "name": "mosoo_config_json", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "plan_json": { + "name": "plan_json", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "source_branch": { + "name": "source_branch", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "source_commit_sha": { + "name": "source_commit_sha", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "target_kind": { + "name": "target_kind", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "target_project_name": { + "name": "target_project_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "target_script_name": { + "name": "target_script_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "app_deployment_run_app_id_idx": { + "name": "app_deployment_run_app_id_idx", + "columns": ["app_id", "id"], + "isUnique": false + }, + "app_deployment_run_deployment_id_idx": { + "name": "app_deployment_run_deployment_id_idx", + "columns": ["deployment_id", "id"], + "isUnique": false + }, + "app_deployment_run_active_app_idx": { + "name": "app_deployment_run_active_app_idx", + "columns": ["app_id"], + "isUnique": true, + "where": "\"app_deployment_run\".\"status\" IN ('queued', 'preparing', 'building', 'submitting', 'submitted', 'activating')" + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": { + "app_deployment_run_status_check": { + "name": "app_deployment_run_status_check", + "value": "\"app_deployment_run\".\"status\" IN ('queued', 'preparing', 'building', 'submitting', 'submitted', 'activating', 'success', 'failed')" + }, + "app_deployment_run_target_kind_check": { + "name": "app_deployment_run_target_kind_check", + "value": "\"app_deployment_run\".\"target_kind\" IS NULL OR \"app_deployment_run\".\"target_kind\" IN ('cloudflare_pages', 'cloudflare_worker')" + } + } + }, + "app_deployment_secret": { + "name": "app_deployment_secret", + "columns": { + "app_id": { + "name": "app_id", + "type": "text CHECK (\"app_id\" = upper(\"app_id\") AND length(\"app_id\") = 26 AND substr(\"app_id\", 1, 1) GLOB '[0-7]' AND \"app_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "vault_secret_id": { + "name": "vault_secret_id", + "type": "text CHECK (\"vault_secret_id\" = upper(\"vault_secret_id\") AND length(\"vault_secret_id\") = 26 AND substr(\"vault_secret_id\", 1, 1) GLOB '[0-7]' AND \"vault_secret_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "app_deployment_secret_app_name_idx": { + "name": "app_deployment_secret_app_name_idx", + "columns": ["app_id", "name"], + "isUnique": true + }, + "app_deployment_secret_vault_secret_idx": { + "name": "app_deployment_secret_vault_secret_idx", + "columns": ["vault_secret_id"], + "isUnique": true + } + }, + "foreignKeys": { + "app_deployment_secret_vault_secret_id_vault_secret_id_fk": { + "name": "app_deployment_secret_vault_secret_id_vault_secret_id_fk", + "tableFrom": "app_deployment_secret", + "tableTo": "vault_secret", + "columnsFrom": ["vault_secret_id"], + "columnsTo": ["id"], + "onDelete": "restrict", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "app_deployment": { + "name": "app_deployment", + "columns": { + "app_id": { + "name": "app_id", + "type": "text CHECK (\"app_id\" = upper(\"app_id\") AND length(\"app_id\") = 26 AND substr(\"app_id\", 1, 1) GLOB '[0-7]' AND \"app_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "default_branch": { + "name": "default_branch", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "deleted_at": { + "name": "deleted_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "id": { + "name": "id", + "type": "text CHECK (\"id\" = upper(\"id\") AND length(\"id\") = 26 AND substr(\"id\", 1, 1) GLOB '[0-7]' AND \"id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "last_successful_url": { + "name": "last_successful_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "latest_run_id": { + "name": "latest_run_id", + "type": "text CHECK (\"latest_run_id\" = upper(\"latest_run_id\") AND length(\"latest_run_id\") = 26 AND substr(\"latest_run_id\", 1, 1) GLOB '[0-7]' AND \"latest_run_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "mosoo_subdomain": { + "name": "mosoo_subdomain", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "owner_account_id": { + "name": "owner_account_id", + "type": "text CHECK (\"owner_account_id\" = upper(\"owner_account_id\") AND length(\"owner_account_id\") = 26 AND substr(\"owner_account_id\", 1, 1) GLOB '[0-7]' AND \"owner_account_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "repo_name": { + "name": "repo_name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "repo_owner": { + "name": "repo_owner", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "repo_url": { + "name": "repo_url", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "source_kind": { + "name": "source_kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "app_deployment_active_app_idx": { + "name": "app_deployment_active_app_idx", + "columns": ["app_id"], + "isUnique": true, + "where": "\"app_deployment\".\"deleted_at\" IS NULL" + }, + "app_deployment_active_subdomain_idx": { + "name": "app_deployment_active_subdomain_idx", + "columns": ["mosoo_subdomain"], + "isUnique": true, + "where": "\"app_deployment\".\"deleted_at\" IS NULL" + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": { + "app_deployment_source_kind_check": { + "name": "app_deployment_source_kind_check", + "value": "\"app_deployment\".\"source_kind\" IN ('github_public')" + } + } + }, + "app": { + "name": "app", + "columns": { + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "default_environment_id": { + "name": "default_environment_id", + "type": "text CHECK (\"default_environment_id\" = upper(\"default_environment_id\") AND length(\"default_environment_id\") = 26 AND substr(\"default_environment_id\", 1, 1) GLOB '[0-7]' AND \"default_environment_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "id": { + "name": "id", + "type": "text CHECK (\"id\" = upper(\"id\") AND length(\"id\") = 26 AND substr(\"id\", 1, 1) GLOB '[0-7]' AND \"id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "organization_id": { + "name": "organization_id", + "type": "text CHECK (\"organization_id\" = upper(\"organization_id\") AND length(\"organization_id\") = 26 AND substr(\"organization_id\", 1, 1) GLOB '[0-7]' AND \"organization_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "owner_account_id": { + "name": "owner_account_id", + "type": "text CHECK (\"owner_account_id\" = upper(\"owner_account_id\") AND length(\"owner_account_id\") = 26 AND substr(\"owner_account_id\", 1, 1) GLOB '[0-7]' AND \"owner_account_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "bound_agent_call_idempotency_key": { + "name": "bound_agent_call_idempotency_key", + "columns": { + "body_hash": { + "name": "body_hash", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "id": { + "name": "id", + "type": "text CHECK (\"id\" = upper(\"id\") AND length(\"id\") = 26 AND substr(\"id\", 1, 1) GLOB '[0-7]' AND \"id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "idempotency_key": { + "name": "idempotency_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "run_id": { + "name": "run_id", + "type": "text CHECK (\"run_id\" = upper(\"run_id\") AND length(\"run_id\") = 26 AND substr(\"run_id\", 1, 1) GLOB '[0-7]' AND \"run_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "session_id": { + "name": "session_id", + "type": "text CHECK (\"session_id\" = upper(\"session_id\") AND length(\"session_id\") = 26 AND substr(\"session_id\", 1, 1) GLOB '[0-7]' AND \"session_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "subject_hash": { + "name": "subject_hash", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "bound_agent_call_idempotency_subject_key_idx": { + "name": "bound_agent_call_idempotency_subject_key_idx", + "columns": ["subject_hash", "idempotency_key"], + "isUnique": true + }, + "bound_agent_call_idempotency_updated_idx": { + "name": "bound_agent_call_idempotency_updated_idx", + "columns": ["updated_at"], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "public_api_idempotency_key": { + "name": "public_api_idempotency_key", + "columns": { + "body_hash": { + "name": "body_hash", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "id": { + "name": "id", + "type": "text CHECK (\"id\" = upper(\"id\") AND length(\"id\") = 26 AND substr(\"id\", 1, 1) GLOB '[0-7]' AND \"id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "idempotency_key": { + "name": "idempotency_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "method": { + "name": "method", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "response_json": { + "name": "response_json", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "response_status": { + "name": "response_status", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "route": { + "name": "route", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "token_id": { + "name": "token_id", + "type": "text CHECK (\"token_id\" = upper(\"token_id\") AND length(\"token_id\") = 26 AND substr(\"token_id\", 1, 1) GLOB '[0-7]' AND \"token_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "public_api_idempotency_token_key_idx": { + "name": "public_api_idempotency_token_key_idx", + "columns": ["token_id", "idempotency_key"], + "isUnique": true + }, + "public_api_idempotency_updated_idx": { + "name": "public_api_idempotency_updated_idx", + "columns": ["updated_at"], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "public_api_rate_limit_window": { + "name": "public_api_rate_limit_window", + "columns": { + "bucket_key": { + "name": "bucket_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "request_count": { + "name": "request_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "shard": { + "name": "shard", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "window_start": { + "name": "window_start", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "public_api_rate_limit_window_updated_idx": { + "name": "public_api_rate_limit_window_updated_idx", + "columns": ["updated_at"], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "public_api_rate_limit_window_bucket_key_window_start_shard_pk": { + "columns": ["bucket_key", "window_start", "shard"], + "name": "public_api_rate_limit_window_bucket_key_window_start_shard_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "driver_command": { + "name": "driver_command", + "columns": { + "acked_at": { + "name": "acked_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "completed_at": { + "name": "completed_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "delivery_connection_id": { + "name": "delivery_connection_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "driver_generation": { + "name": "driver_generation", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "driver_instance_id": { + "name": "driver_instance_id", + "type": "text CHECK (\"driver_instance_id\" = upper(\"driver_instance_id\") AND length(\"driver_instance_id\") = 26 AND substr(\"driver_instance_id\", 1, 1) GLOB '[0-7]' AND \"driver_instance_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "error_json": { + "name": "error_json", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "id": { + "name": "id", + "type": "text CHECK (\"id\" = upper(\"id\") AND length(\"id\") = 26 AND substr(\"id\", 1, 1) GLOB '[0-7]' AND \"id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "issued_at": { + "name": "issued_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "payload_json": { + "name": "payload_json", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "result_json": { + "name": "result_json", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "seq": { + "name": "seq", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "driver_command_instance_seq_idx": { + "name": "driver_command_instance_seq_idx", + "columns": ["driver_instance_id", "seq"], + "isUnique": true + }, + "driver_command_instance_status_idx": { + "name": "driver_command_instance_status_idx", + "columns": ["driver_instance_id", "status", "expires_at"], + "isUnique": false + } + }, + "foreignKeys": { + "driver_command_driver_instance_id_driver_instance_id_fk": { + "name": "driver_command_driver_instance_id_driver_instance_id_fk", + "tableFrom": "driver_command", + "tableTo": "driver_instance", + "columnsFrom": ["driver_instance_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": { + "driver_command_generation_check": { + "name": "driver_command_generation_check", + "value": "\"driver_command\".\"driver_generation\" IS NULL OR (typeof(\"driver_command\".\"driver_generation\") = 'integer' AND \"driver_command\".\"driver_generation\" BETWEEN 0 AND 9007199254740991)" + }, + "driver_command_nonterminal_generation_check": { + "name": "driver_command_nonterminal_generation_check", + "value": "\"driver_command\".\"status\" IN ('completed', 'failed', 'expired', 'cancelled') OR \"driver_command\".\"driver_generation\" IS NOT NULL" + } + } + }, + "driver_instance_mcp_grant": { + "name": "driver_instance_mcp_grant", + "columns": { + "auth_type": { + "name": "auth_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "authorization_state": { + "name": "authorization_state", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "can_invalidate": { + "name": "can_invalidate", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "can_refresh": { + "name": "can_refresh", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "credential_id": { + "name": "credential_id", + "type": "text CHECK (\"credential_id\" = upper(\"credential_id\") AND length(\"credential_id\") = 26 AND substr(\"credential_id\", 1, 1) GLOB '[0-7]' AND \"credential_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "driver_instance_id": { + "name": "driver_instance_id", + "type": "text CHECK (\"driver_instance_id\" = upper(\"driver_instance_id\") AND length(\"driver_instance_id\") = 26 AND substr(\"driver_instance_id\", 1, 1) GLOB '[0-7]' AND \"driver_instance_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "app_id": { + "name": "app_id", + "type": "text CHECK (\"app_id\" = upper(\"app_id\") AND length(\"app_id\") = 26 AND substr(\"app_id\", 1, 1) GLOB '[0-7]' AND \"app_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "server_id": { + "name": "server_id", + "type": "text CHECK (\"server_id\" = upper(\"server_id\") AND length(\"server_id\") = 26 AND substr(\"server_id\", 1, 1) GLOB '[0-7]' AND \"server_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "driver_instance_mcp_grant_instance_server_idx": { + "name": "driver_instance_mcp_grant_instance_server_idx", + "columns": ["driver_instance_id", "server_id"], + "isUnique": true + }, + "driver_instance_mcp_grant_instance_credential_idx": { + "name": "driver_instance_mcp_grant_instance_credential_idx", + "columns": ["driver_instance_id", "credential_id"], + "isUnique": false + } + }, + "foreignKeys": { + "driver_instance_mcp_grant_driver_instance_id_driver_instance_id_fk": { + "name": "driver_instance_mcp_grant_driver_instance_id_driver_instance_id_fk", + "tableFrom": "driver_instance_mcp_grant", + "tableTo": "driver_instance", + "columnsFrom": ["driver_instance_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "driver_instance": { + "name": "driver_instance", + "columns": { + "boot_token_expires_at": { + "name": "boot_token_expires_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "boot_token_hash": { + "name": "boot_token_hash", + "type": "blob", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "boot_token_used_at": { + "name": "boot_token_used_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "close_code": { + "name": "close_code", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "close_reason": { + "name": "close_reason", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "connection_id": { + "name": "connection_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "command_seq_cursor": { + "name": "command_seq_cursor", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "driver_pid": { + "name": "driver_pid", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "driver_started_at": { + "name": "driver_started_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "driver_version": { + "name": "driver_version", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "heartbeat_count": { + "name": "heartbeat_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "generation": { + "name": "generation", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "id": { + "name": "id", + "type": "text CHECK (\"id\" = upper(\"id\") AND length(\"id\") = 26 AND substr(\"id\", 1, 1) GLOB '[0-7]' AND \"id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "last_heartbeat_at": { + "name": "last_heartbeat_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "process_id": { + "name": "process_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "protocol": { + "name": "protocol", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "protocol_version": { + "name": "protocol_version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "restart_count": { + "name": "restart_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "runtime": { + "name": "runtime", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "sandbox_id": { + "name": "sandbox_id", + "type": "text CHECK (\"sandbox_id\" = upper(\"sandbox_id\") AND length(\"sandbox_id\") = 26 AND substr(\"sandbox_id\", 1, 1) GLOB '[0-7]' AND \"sandbox_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "sandbox_session_id": { + "name": "sandbox_session_id", + "type": "text CHECK (\"sandbox_session_id\" = upper(\"sandbox_session_id\") AND length(\"sandbox_session_id\") = 26 AND substr(\"sandbox_session_id\", 1, 1) GLOB '[0-7]' AND \"sandbox_session_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status_changed_at": { + "name": "status_changed_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "status_event": { + "name": "status_event", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'driver.provision'" + }, + "status_operation_id": { + "name": "status_operation_id", + "type": "text CHECK (\"status_operation_id\" = upper(\"status_operation_id\") AND length(\"status_operation_id\") = 26 AND substr(\"status_operation_id\", 1, 1) GLOB '[0-7]' AND \"status_operation_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status_seq": { + "name": "status_seq", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "status_source": { + "name": "status_source", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'system'" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "driver_instance_completed_idx": { + "name": "driver_instance_completed_idx", + "columns": ["expires_at", "status"], + "isUnique": false + }, + "driver_instance_connection_idx": { + "name": "driver_instance_connection_idx", + "columns": ["connection_id"], + "isUnique": true, + "where": "\"driver_instance\".\"connection_id\" IS NOT NULL" + }, + "driver_instance_boot_token_expiry_idx": { + "name": "driver_instance_boot_token_expiry_idx", + "columns": ["status", "boot_token_expires_at"], + "isUnique": false, + "where": "\"driver_instance\".\"boot_token_used_at\" IS NULL" + }, + "driver_instance_boot_token_hash_idx": { + "name": "driver_instance_boot_token_hash_idx", + "columns": ["boot_token_hash"], + "isUnique": true + }, + "driver_instance_sandbox_session_idx": { + "name": "driver_instance_sandbox_session_idx", + "columns": ["sandbox_id", "sandbox_session_id", "status", "updated_at"], + "isUnique": false + }, + "driver_instance_live_sandbox_session_idx": { + "name": "driver_instance_live_sandbox_session_idx", + "columns": ["sandbox_id", "sandbox_session_id"], + "isUnique": true, + "where": "\"driver_instance\".\"status\" IN ('provisioning', 'connecting', 'ready', 'stopping')" + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": { + "driver_instance_status_check": { + "name": "driver_instance_status_check", + "value": "\"driver_instance\".\"status\" IN ('provisioning', 'connecting', 'ready', 'stopping', 'stopped', 'failed')" + }, + "driver_instance_status_seq_check": { + "name": "driver_instance_status_seq_check", + "value": "\"driver_instance\".\"status_seq\" >= 0" + } + } + }, + "external_tool_effect_attempt": { + "name": "external_tool_effect_attempt", + "columns": { + "attempt": { + "name": "attempt", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "claim_token": { + "name": "claim_token", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "completed_at": { + "name": "completed_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "effect_id": { + "name": "effect_id", + "type": "text CHECK (\"effect_id\" = upper(\"effect_id\") AND length(\"effect_id\") = 26 AND substr(\"effect_id\", 1, 1) GLOB '[0-7]' AND \"effect_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "provider_receipt_json": { + "name": "provider_receipt_json", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "result_json": { + "name": "result_json", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "external_tool_effect_attempt_status_idx": { + "name": "external_tool_effect_attempt_status_idx", + "columns": ["status", "created_at"], + "isUnique": false + } + }, + "foreignKeys": { + "external_tool_effect_attempt_effect_id_external_tool_effect_id_fk": { + "name": "external_tool_effect_attempt_effect_id_external_tool_effect_id_fk", + "tableFrom": "external_tool_effect_attempt", + "tableTo": "external_tool_effect", + "columnsFrom": ["effect_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "external_tool_effect_attempt_effect_id_attempt_pk": { + "columns": ["effect_id", "attempt"], + "name": "external_tool_effect_attempt_effect_id_attempt_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": { + "external_tool_effect_attempt_status_check": { + "name": "external_tool_effect_attempt_status_check", + "value": "\"external_tool_effect_attempt\".\"status\" IN ('claimed', 'succeeded', 'unknown')" + }, + "external_tool_effect_attempt_claim_token_uuid_check": { + "name": "external_tool_effect_attempt_claim_token_uuid_check", + "value": "length(\"external_tool_effect_attempt\".\"claim_token\") = 36 AND length(replace(\"external_tool_effect_attempt\".\"claim_token\", '-', '')) = 32 AND \"external_tool_effect_attempt\".\"claim_token\" = lower(\"external_tool_effect_attempt\".\"claim_token\") AND substr(\"external_tool_effect_attempt\".\"claim_token\", 9, 1) = '-' AND substr(\"external_tool_effect_attempt\".\"claim_token\", 14, 1) = '-' AND substr(\"external_tool_effect_attempt\".\"claim_token\", 15, 1) = '4' AND substr(\"external_tool_effect_attempt\".\"claim_token\", 19, 1) = '-' AND substr(\"external_tool_effect_attempt\".\"claim_token\", 20, 1) GLOB '[89ab]' AND substr(\"external_tool_effect_attempt\".\"claim_token\", 24, 1) = '-' AND replace(\"external_tool_effect_attempt\".\"claim_token\", '-', '') NOT GLOB '*[^0-9a-f]*'" + } + } + }, + "external_tool_effect": { + "name": "external_tool_effect", + "columns": { + "attempt_count": { + "name": "attempt_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "claim_token": { + "name": "claim_token", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "command_id": { + "name": "command_id", + "type": "text CHECK (\"command_id\" = upper(\"command_id\") AND length(\"command_id\") = 26 AND substr(\"command_id\", 1, 1) GLOB '[0-7]' AND \"command_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "driver_instance_id": { + "name": "driver_instance_id", + "type": "text CHECK (\"driver_instance_id\" = upper(\"driver_instance_id\") AND length(\"driver_instance_id\") = 26 AND substr(\"driver_instance_id\", 1, 1) GLOB '[0-7]' AND \"driver_instance_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "id": { + "name": "id", + "type": "text CHECK (\"id\" = upper(\"id\") AND length(\"id\") = 26 AND substr(\"id\", 1, 1) GLOB '[0-7]' AND \"id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "idempotency_key": { + "name": "idempotency_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "provider_receipt_json": { + "name": "provider_receipt_json", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "result_json": { + "name": "result_json", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "server_id": { + "name": "server_id", + "type": "text CHECK (\"server_id\" = upper(\"server_id\") AND length(\"server_id\") = 26 AND substr(\"server_id\", 1, 1) GLOB '[0-7]' AND \"server_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "session_run_id": { + "name": "session_run_id", + "type": "text CHECK (\"session_run_id\" = upper(\"session_run_id\") AND length(\"session_run_id\") = 26 AND substr(\"session_run_id\", 1, 1) GLOB '[0-7]' AND \"session_run_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "tool_name": { + "name": "tool_name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "external_tool_effect_command_idx": { + "name": "external_tool_effect_command_idx", + "columns": ["command_id"], + "isUnique": true + }, + "external_tool_effect_idempotency_key_idx": { + "name": "external_tool_effect_idempotency_key_idx", + "columns": ["idempotency_key"], + "isUnique": true + }, + "external_tool_effect_run_status_idx": { + "name": "external_tool_effect_run_status_idx", + "columns": ["session_run_id", "status", "id"], + "isUnique": false + }, + "external_tool_effect_driver_status_idx": { + "name": "external_tool_effect_driver_status_idx", + "columns": ["driver_instance_id", "status"], + "isUnique": false + } + }, + "foreignKeys": { + "external_tool_effect_command_id_driver_command_id_fk": { + "name": "external_tool_effect_command_id_driver_command_id_fk", + "tableFrom": "external_tool_effect", + "tableTo": "driver_command", + "columnsFrom": ["command_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "external_tool_effect_driver_instance_id_driver_instance_id_fk": { + "name": "external_tool_effect_driver_instance_id_driver_instance_id_fk", + "tableFrom": "external_tool_effect", + "tableTo": "driver_instance", + "columnsFrom": ["driver_instance_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "external_tool_effect_session_run_id_session_run_id_fk": { + "name": "external_tool_effect_session_run_id_session_run_id_fk", + "tableFrom": "external_tool_effect", + "tableTo": "session_run", + "columnsFrom": ["session_run_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": { + "external_tool_effect_status_check": { + "name": "external_tool_effect_status_check", + "value": "\"external_tool_effect\".\"status\" IN ('intent', 'claimed', 'succeeded', 'unknown')" + }, + "external_tool_effect_claim_token_uuid_check": { + "name": "external_tool_effect_claim_token_uuid_check", + "value": "\"external_tool_effect\".\"claim_token\" IS NULL OR (length(\"external_tool_effect\".\"claim_token\") = 36 AND length(replace(\"external_tool_effect\".\"claim_token\", '-', '')) = 32 AND \"external_tool_effect\".\"claim_token\" = lower(\"external_tool_effect\".\"claim_token\") AND substr(\"external_tool_effect\".\"claim_token\", 9, 1) = '-' AND substr(\"external_tool_effect\".\"claim_token\", 14, 1) = '-' AND substr(\"external_tool_effect\".\"claim_token\", 15, 1) = '4' AND substr(\"external_tool_effect\".\"claim_token\", 19, 1) = '-' AND substr(\"external_tool_effect\".\"claim_token\", 20, 1) GLOB '[89ab]' AND substr(\"external_tool_effect\".\"claim_token\", 24, 1) = '-' AND replace(\"external_tool_effect\".\"claim_token\", '-', '') NOT GLOB '*[^0-9a-f]*')" + } + } + }, + "native_resume_ref": { + "name": "native_resume_ref", + "columns": { + "committed_session_run_id": { + "name": "committed_session_run_id", + "type": "text CHECK (\"committed_session_run_id\" = upper(\"committed_session_run_id\") AND length(\"committed_session_run_id\") = 26 AND substr(\"committed_session_run_id\", 1, 1) GLOB '[0-7]' AND \"committed_session_run_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "committed_value": { + "name": "committed_value", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "observed_driver_instance_id": { + "name": "observed_driver_instance_id", + "type": "text CHECK (\"observed_driver_instance_id\" = upper(\"observed_driver_instance_id\") AND length(\"observed_driver_instance_id\") = 26 AND substr(\"observed_driver_instance_id\", 1, 1) GLOB '[0-7]' AND \"observed_driver_instance_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "observed_event_seq": { + "name": "observed_event_seq", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "observed_session_run_id": { + "name": "observed_session_run_id", + "type": "text CHECK (\"observed_session_run_id\" = upper(\"observed_session_run_id\") AND length(\"observed_session_run_id\") = 26 AND substr(\"observed_session_run_id\", 1, 1) GLOB '[0-7]' AND \"observed_session_run_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "runtime_id": { + "name": "runtime_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "session_id": { + "name": "session_id", + "type": "text CHECK (\"session_id\" = upper(\"session_id\") AND length(\"session_id\") = 26 AND substr(\"session_id\", 1, 1) GLOB '[0-7]' AND \"session_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "native_resume_ref_runtime_updated_idx": { + "name": "native_resume_ref_runtime_updated_idx", + "columns": ["runtime_id", "updated_at"], + "isUnique": false + } + }, + "foreignKeys": { + "native_resume_ref_session_id_session_id_fk": { + "name": "native_resume_ref_session_id_session_id_fk", + "tableFrom": "native_resume_ref", + "tableTo": "session", + "columnsFrom": ["session_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": { + "native_resume_ref_observed_event_seq_check": { + "name": "native_resume_ref_observed_event_seq_check", + "value": "\"native_resume_ref\".\"observed_event_seq\" >= 0" + } + } + }, + "sandbox_backup": { + "name": "sandbox_backup", + "columns": { + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "dir": { + "name": "dir", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "id": { + "name": "id", + "type": "text CHECK (\"id\" = upper(\"id\") AND length(\"id\") = 26 AND substr(\"id\", 1, 1) GLOB '[0-7]' AND \"id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "keep": { + "name": "keep", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "sandbox_id": { + "name": "sandbox_id", + "type": "text CHECK (\"sandbox_id\" = upper(\"sandbox_id\") AND length(\"sandbox_id\") = 26 AND substr(\"sandbox_id\", 1, 1) GLOB '[0-7]' AND \"sandbox_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "session_run_id": { + "name": "session_run_id", + "type": "text CHECK (\"session_run_id\" = upper(\"session_run_id\") AND length(\"session_run_id\") = 26 AND substr(\"session_run_id\", 1, 1) GLOB '[0-7]' AND \"session_run_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "ttl_seconds": { + "name": "ttl_seconds", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "sandbox_backup_sandbox_status_created_idx": { + "name": "sandbox_backup_sandbox_status_created_idx", + "columns": ["sandbox_id", "status", "created_at"], + "isUnique": false + }, + "sandbox_backup_terminal_checkpoint_idx": { + "name": "sandbox_backup_terminal_checkpoint_idx", + "columns": ["sandbox_id", "dir", "session_run_id"], + "isUnique": true, + "where": "\"sandbox_backup\".\"session_run_id\" IS NOT NULL AND \"sandbox_backup\".\"status\" = 'ready'" + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "sandbox_session": { + "name": "sandbox_session", + "columns": { + "cloudflare_session_id": { + "name": "cloudflare_session_id", + "type": "text CHECK (\"cloudflare_session_id\" = upper(\"cloudflare_session_id\") AND length(\"cloudflare_session_id\") = 26 AND substr(\"cloudflare_session_id\", 1, 1) GLOB '[0-7]' AND \"cloudflare_session_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "cwd": { + "name": "cwd", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "origin_json": { + "name": "origin_json", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "sandbox_id": { + "name": "sandbox_id", + "type": "text CHECK (\"sandbox_id\" = upper(\"sandbox_id\") AND length(\"sandbox_id\") = 26 AND substr(\"sandbox_id\", 1, 1) GLOB '[0-7]' AND \"sandbox_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "session_id": { + "name": "session_id", + "type": "text CHECK (\"session_id\" = upper(\"session_id\") AND length(\"session_id\") = 26 AND substr(\"session_id\", 1, 1) GLOB '[0-7]' AND \"session_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "sandbox_session_sandbox_status_idx": { + "name": "sandbox_session_sandbox_status_idx", + "columns": ["sandbox_id", "status", "updated_at"], + "isUnique": false + }, + "sandbox_session_cloudflare_session_idx": { + "name": "sandbox_session_cloudflare_session_idx", + "columns": ["cloudflare_session_id"], + "isUnique": true + } + }, + "foreignKeys": { + "sandbox_session_session_id_session_id_fk": { + "name": "sandbox_session_session_id_session_id_fk", + "tableFrom": "sandbox_session", + "tableTo": "session", + "columnsFrom": ["session_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "sandbox": { + "name": "sandbox", + "columns": { + "agent_id": { + "name": "agent_id", + "type": "text CHECK (\"agent_id\" = upper(\"agent_id\") AND length(\"agent_id\") = 26 AND substr(\"agent_id\", 1, 1) GLOB '[0-7]' AND \"agent_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "app_id": { + "name": "app_id", + "type": "text CHECK (\"app_id\" = upper(\"app_id\") AND length(\"app_id\") = 26 AND substr(\"app_id\", 1, 1) GLOB '[0-7]' AND \"app_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "bind_mount_ready": { + "name": "bind_mount_ready", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "claim_expires_at": { + "name": "claim_expires_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "claim_owner": { + "name": "claim_owner", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "global_mounts_json": { + "name": "global_mounts_json", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'[]'" + }, + "id": { + "name": "id", + "type": "text CHECK (\"id\" = upper(\"id\") AND length(\"id\") = 26 AND substr(\"id\", 1, 1) GLOB '[0-7]' AND \"id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "inactive_deadline_at": { + "name": "inactive_deadline_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "last_backup_id": { + "name": "last_backup_id", + "type": "text CHECK (\"last_backup_id\" = upper(\"last_backup_id\") AND length(\"last_backup_id\") = 26 AND substr(\"last_backup_id\", 1, 1) GLOB '[0-7]' AND \"last_backup_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_error_code": { + "name": "last_error_code", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_restore_backup_id": { + "name": "last_restore_backup_id", + "type": "text CHECK (\"last_restore_backup_id\" = upper(\"last_restore_backup_id\") AND length(\"last_restore_backup_id\") = 26 AND substr(\"last_restore_backup_id\", 1, 1) GLOB '[0-7]' AND \"last_restore_backup_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "owner_account_id": { + "name": "owner_account_id", + "type": "text CHECK (\"owner_account_id\" = upper(\"owner_account_id\") AND length(\"owner_account_id\") = 26 AND substr(\"owner_account_id\", 1, 1) GLOB '[0-7]' AND \"owner_account_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status_changed_at": { + "name": "status_changed_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "status_event": { + "name": "status_event", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'runtime_subject.cold'" + }, + "status_operation_id": { + "name": "status_operation_id", + "type": "text CHECK (\"status_operation_id\" = upper(\"status_operation_id\") AND length(\"status_operation_id\") = 26 AND substr(\"status_operation_id\", 1, 1) GLOB '[0-7]' AND \"status_operation_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status_seq": { + "name": "status_seq", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "status_source": { + "name": "status_source", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'system'" + }, + "subject_id": { + "name": "subject_id", + "type": "text CHECK (\"subject_id\" = upper(\"subject_id\") AND length(\"subject_id\") = 26 AND substr(\"subject_id\", 1, 1) GLOB '[0-7]' AND \"subject_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "subject_kind": { + "name": "subject_kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "sandbox_subject_idx": { + "name": "sandbox_subject_idx", + "columns": ["kind", "subject_kind", "subject_id"], + "isUnique": true + }, + "sandbox_status_deadline_idx": { + "name": "sandbox_status_deadline_idx", + "columns": ["status", "inactive_deadline_at", "updated_at"], + "isUnique": false + }, + "sandbox_claim_idx": { + "name": "sandbox_claim_idx", + "columns": ["claim_expires_at", "claim_owner"], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": { + "sandbox_status_check": { + "name": "sandbox_status_check", + "value": "\"sandbox\".\"status\" IN ('cold', 'restoring', 'active', 'backing_up', 'destroying', 'error')" + }, + "sandbox_status_seq_check": { + "name": "sandbox_status_seq_check", + "value": "\"sandbox\".\"status_seq\" >= 0" + } + } + }, + "session_message": { + "name": "session_message", + "columns": { + "content_text": { + "name": "content_text", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_by_account_id": { + "name": "created_by_account_id", + "type": "text CHECK (\"created_by_account_id\" = upper(\"created_by_account_id\") AND length(\"created_by_account_id\") = 26 AND substr(\"created_by_account_id\", 1, 1) GLOB '[0-7]' AND \"created_by_account_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "id": { + "name": "id", + "type": "text CHECK (\"id\" = upper(\"id\") AND length(\"id\") = 26 AND substr(\"id\", 1, 1) GLOB '[0-7]' AND \"id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "plan_json": { + "name": "plan_json", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "projection_format": { + "name": "projection_format", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'materialized'" + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "segments_json": { + "name": "segments_json", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "seq": { + "name": "seq", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "session_id": { + "name": "session_id", + "type": "text CHECK (\"session_id\" = upper(\"session_id\") AND length(\"session_id\") = 26 AND substr(\"session_id\", 1, 1) GLOB '[0-7]' AND \"session_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "session_run_id": { + "name": "session_run_id", + "type": "text CHECK (\"session_run_id\" = upper(\"session_run_id\") AND length(\"session_run_id\") = 26 AND substr(\"session_run_id\", 1, 1) GLOB '[0-7]' AND \"session_run_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "session_message_session_seq_idx": { + "name": "session_message_session_seq_idx", + "columns": ["session_id", "seq"], + "isUnique": true + }, + "session_message_run_idx": { + "name": "session_message_run_idx", + "columns": ["session_run_id"], + "isUnique": false + } + }, + "foreignKeys": { + "session_message_session_id_session_id_fk": { + "name": "session_message_session_id_session_id_fk", + "tableFrom": "session_message", + "tableTo": "session", + "columnsFrom": ["session_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": { + "session_message_projection_format_check": { + "name": "session_message_projection_format_check", + "value": "\"session_message\".\"projection_format\" IN ('materialized', 'event_stream_v3')" + }, + "session_message_event_stream_v3_check": { + "name": "session_message_event_stream_v3_check", + "value": "\"session_message\".\"projection_format\" <> 'event_stream_v3' OR (\"session_message\".\"role\" = 'assistant' AND \"session_message\".\"session_run_id\" IS NOT NULL AND \"session_message\".\"content_text\" = '' AND \"session_message\".\"plan_json\" IS NULL AND \"session_message\".\"segments_json\" IS NULL)" + } + } + }, + "session": { + "name": "session", + "columns": { + "agent_id": { + "name": "agent_id", + "type": "text CHECK (\"agent_id\" = upper(\"agent_id\") AND length(\"agent_id\") = 26 AND substr(\"agent_id\", 1, 1) GLOB '[0-7]' AND \"agent_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "archived_at": { + "name": "archived_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "auto_title_event_seq": { + "name": "auto_title_event_seq", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "cleanup_operation_kind": { + "name": "cleanup_operation_kind", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "end_user_id": { + "name": "end_user_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "attributed_user_id": { + "name": "attributed_user_id", + "type": "text CHECK (\"attributed_user_id\" = upper(\"attributed_user_id\") AND length(\"attributed_user_id\") = 26 AND substr(\"attributed_user_id\", 1, 1) GLOB '[0-7]' AND \"attributed_user_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "creator_account_id": { + "name": "creator_account_id", + "type": "text CHECK (\"creator_account_id\" = upper(\"creator_account_id\") AND length(\"creator_account_id\") = 26 AND substr(\"creator_account_id\", 1, 1) GLOB '[0-7]' AND \"creator_account_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "deployment_version_id": { + "name": "deployment_version_id", + "type": "text CHECK (\"deployment_version_id\" = upper(\"deployment_version_id\") AND length(\"deployment_version_id\") = 26 AND substr(\"deployment_version_id\", 1, 1) GLOB '[0-7]' AND \"deployment_version_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "deployment_version_number": { + "name": "deployment_version_number", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "id": { + "name": "id", + "type": "text CHECK (\"id\" = upper(\"id\") AND length(\"id\") = 26 AND substr(\"id\", 1, 1) GLOB '[0-7]' AND \"id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "last_message_at": { + "name": "last_message_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_run_id": { + "name": "last_run_id", + "type": "text CHECK (\"last_run_id\" = upper(\"last_run_id\") AND length(\"last_run_id\") = 26 AND substr(\"last_run_id\", 1, 1) GLOB '[0-7]' AND \"last_run_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "message_seq_cursor": { + "name": "message_seq_cursor", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "metadata_json": { + "name": "metadata_json", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'{}'" + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "app_id": { + "name": "app_id", + "type": "text CHECK (\"app_id\" = upper(\"app_id\") AND length(\"app_id\") = 26 AND substr(\"app_id\", 1, 1) GLOB '[0-7]' AND \"app_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "renamed": { + "name": "renamed", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "runtime_id": { + "name": "runtime_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status_operation_id": { + "name": "status_operation_id", + "type": "text CHECK (\"status_operation_id\" = upper(\"status_operation_id\") AND length(\"status_operation_id\") = 26 AND substr(\"status_operation_id\", 1, 1) GLOB '[0-7]' AND \"status_operation_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status_seq": { + "name": "status_seq", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "runtime_event_seq_cursor": { + "name": "runtime_event_seq_cursor", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "runtime_provisioning_heartbeat_at": { + "name": "runtime_provisioning_heartbeat_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "runtime_provisioning_operation_id": { + "name": "runtime_provisioning_operation_id", + "type": "text CHECK (\"runtime_provisioning_operation_id\" = upper(\"runtime_provisioning_operation_id\") AND length(\"runtime_provisioning_operation_id\") = 26 AND substr(\"runtime_provisioning_operation_id\", 1, 1) GLOB '[0-7]' AND \"runtime_provisioning_operation_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "runtime_provisioning_run_id": { + "name": "runtime_provisioning_run_id", + "type": "text CHECK (\"runtime_provisioning_run_id\" = upper(\"runtime_provisioning_run_id\") AND length(\"runtime_provisioning_run_id\") = 26 AND substr(\"runtime_provisioning_run_id\", 1, 1) GLOB '[0-7]' AND \"runtime_provisioning_run_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "runtime_provisioning_sandbox_id": { + "name": "runtime_provisioning_sandbox_id", + "type": "text CHECK (\"runtime_provisioning_sandbox_id\" = upper(\"runtime_provisioning_sandbox_id\") AND length(\"runtime_provisioning_sandbox_id\") = 26 AND substr(\"runtime_provisioning_sandbox_id\", 1, 1) GLOB '[0-7]' AND \"runtime_provisioning_sandbox_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'preview'" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "workspace_checkpoint_required": { + "name": "workspace_checkpoint_required", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + } + }, + "indexes": { + "session_agent_updated_idx": { + "name": "session_agent_updated_idx", + "columns": ["agent_id", "updated_at", "id"], + "isUnique": false + }, + "session_app_creator_archived_updated_idx": { + "name": "session_app_creator_archived_updated_idx", + "columns": ["app_id", "creator_account_id", "archived_at", "updated_at", "id"], + "isUnique": false + }, + "session_app_attributed_archived_updated_idx": { + "name": "session_app_attributed_archived_updated_idx", + "columns": ["app_id", "attributed_user_id", "archived_at", "updated_at", "id"], + "isUnique": false + }, + "session_app_creator_type_archived_updated_idx": { + "name": "session_app_creator_type_archived_updated_idx", + "columns": ["app_id", "creator_account_id", "type", "archived_at", "updated_at", "id"], + "isUnique": false + }, + "session_app_attributed_type_archived_updated_idx": { + "name": "session_app_attributed_type_archived_updated_idx", + "columns": ["app_id", "attributed_user_id", "type", "archived_at", "updated_at", "id"], + "isUnique": false + }, + "session_status_operation_updated_idx": { + "name": "session_status_operation_updated_idx", + "columns": ["status", "status_operation_id", "updated_at"], + "isUnique": false + }, + "session_cleanup_operation_updated_idx": { + "name": "session_cleanup_operation_updated_idx", + "columns": ["cleanup_operation_kind", "status", "updated_at", "id"], + "isUnique": false + }, + "session_runtime_provisioning_heartbeat_idx": { + "name": "session_runtime_provisioning_heartbeat_idx", + "columns": ["runtime_provisioning_heartbeat_at", "id"], + "isUnique": false + }, + "session_status_updated_idx": { + "name": "session_status_updated_idx", + "columns": ["status", "updated_at", "id"], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": { + "session_cleanup_operation_kind_check": { + "name": "session_cleanup_operation_kind_check", + "value": "\"session\".\"cleanup_operation_kind\" IS NULL OR (\"session\".\"cleanup_operation_kind\" IN ('archive', 'delete') AND \"session\".\"archived_at\" IS NOT NULL AND \"session\".\"status\" IN ('IDLE', 'RESCHEDULING') AND (\"session\".\"status_operation_id\" IS NOT NULL OR (\"session\".\"cleanup_operation_kind\" = 'archive' AND \"session\".\"status\" = 'IDLE')))" + }, + "session_runtime_provisioning_lease_check": { + "name": "session_runtime_provisioning_lease_check", + "value": "(\"session\".\"runtime_provisioning_operation_id\" IS NULL AND \"session\".\"runtime_provisioning_run_id\" IS NULL AND \"session\".\"runtime_provisioning_sandbox_id\" IS NULL AND \"session\".\"runtime_provisioning_heartbeat_at\" IS NULL) OR (\"session\".\"runtime_provisioning_operation_id\" IS NOT NULL AND \"session\".\"runtime_provisioning_sandbox_id\" IS NOT NULL AND \"session\".\"runtime_provisioning_heartbeat_at\" IS NOT NULL AND typeof(\"session\".\"runtime_provisioning_heartbeat_at\") = 'integer' AND \"session\".\"runtime_provisioning_heartbeat_at\" >= 0 AND \"session\".\"archived_at\" IS NULL AND \"session\".\"cleanup_operation_kind\" IS NULL AND \"session\".\"status_operation_id\" IS NULL)" + }, + "session_status_check": { + "name": "session_status_check", + "value": "\"session\".\"status\" IN ('IDLE', 'RUNNING', 'RESCHEDULING', 'TERMINATED')" + }, + "session_auto_title_event_seq_check": { + "name": "session_auto_title_event_seq_check", + "value": "\"session\".\"auto_title_event_seq\" IS NULL OR \"session\".\"auto_title_event_seq\" >= 0" + }, + "session_status_seq_check": { + "name": "session_status_seq_check", + "value": "\"session\".\"status_seq\" >= 0" + } + } + }, + "session_execution_snapshot": { + "name": "session_execution_snapshot", + "columns": { + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "plan_json": { + "name": "plan_json", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "session_id": { + "name": "session_id", + "type": "text CHECK (\"session_id\" = upper(\"session_id\") AND length(\"session_id\") = 26 AND substr(\"session_id\", 1, 1) GLOB '[0-7]' AND \"session_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": true, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "session_execution_snapshot_session_id_session_id_fk": { + "name": "session_execution_snapshot_session_id_session_id_fk", + "tableFrom": "session_execution_snapshot", + "tableTo": "session", + "columnsFrom": ["session_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "session_run_skill": { + "name": "session_run_skill", + "columns": { + "blob_sha256": { + "name": "blob_sha256", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "materialization_status": { + "name": "materialization_status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "mount_path": { + "name": "mount_path", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "resolution_mode": { + "name": "resolution_mode", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "session_run_id": { + "name": "session_run_id", + "type": "text CHECK (\"session_run_id\" = upper(\"session_run_id\") AND length(\"session_run_id\") = 26 AND substr(\"session_run_id\", 1, 1) GLOB '[0-7]' AND \"session_run_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "skill_id": { + "name": "skill_id", + "type": "text CHECK (\"skill_id\" = upper(\"skill_id\") AND length(\"skill_id\") = 26 AND substr(\"skill_id\", 1, 1) GLOB '[0-7]' AND \"skill_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "skill_name": { + "name": "skill_name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "snapshot_id": { + "name": "snapshot_id", + "type": "text CHECK (\"snapshot_id\" = upper(\"snapshot_id\") AND length(\"snapshot_id\") = 26 AND substr(\"snapshot_id\", 1, 1) GLOB '[0-7]' AND \"snapshot_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "warning_code": { + "name": "warning_code", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "session_run_skill_run_resolution_idx": { + "name": "session_run_skill_run_resolution_idx", + "columns": ["session_run_id", "resolution_mode"], + "isUnique": false + } + }, + "foreignKeys": { + "session_run_skill_session_run_id_session_run_id_fk": { + "name": "session_run_skill_session_run_id_session_run_id_fk", + "tableFrom": "session_run_skill", + "tableTo": "session_run", + "columnsFrom": ["session_run_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "session_run_skill_session_run_id_skill_id_pk": { + "columns": ["session_run_id", "skill_id"], + "name": "session_run_skill_session_run_id_skill_id_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "session_run": { + "name": "session_run", + "columns": { + "agent_id": { + "name": "agent_id", + "type": "text CHECK (\"agent_id\" = upper(\"agent_id\") AND length(\"agent_id\") = 26 AND substr(\"agent_id\", 1, 1) GLOB '[0-7]' AND \"agent_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "bound_capability_agent_id": { + "name": "bound_capability_agent_id", + "type": "text CHECK (\"bound_capability_agent_id\" = upper(\"bound_capability_agent_id\") AND length(\"bound_capability_agent_id\") = 26 AND substr(\"bound_capability_agent_id\", 1, 1) GLOB '[0-7]' AND \"bound_capability_agent_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "bound_capability_app_id": { + "name": "bound_capability_app_id", + "type": "text CHECK (\"bound_capability_app_id\" = upper(\"bound_capability_app_id\") AND length(\"bound_capability_app_id\") = 26 AND substr(\"bound_capability_app_id\", 1, 1) GLOB '[0-7]' AND \"bound_capability_app_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "bound_capability_binding_env": { + "name": "bound_capability_binding_env", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "bound_capability_binding_name": { + "name": "bound_capability_binding_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "bound_capability_deployment_id": { + "name": "bound_capability_deployment_id", + "type": "text CHECK (\"bound_capability_deployment_id\" = upper(\"bound_capability_deployment_id\") AND length(\"bound_capability_deployment_id\") = 26 AND substr(\"bound_capability_deployment_id\", 1, 1) GLOB '[0-7]' AND \"bound_capability_deployment_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "bound_capability_deployment_run_id": { + "name": "bound_capability_deployment_run_id", + "type": "text CHECK (\"bound_capability_deployment_run_id\" = upper(\"bound_capability_deployment_run_id\") AND length(\"bound_capability_deployment_run_id\") = 26 AND substr(\"bound_capability_deployment_run_id\", 1, 1) GLOB '[0-7]' AND \"bound_capability_deployment_run_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "completed_at": { + "name": "completed_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_by_account_id": { + "name": "created_by_account_id", + "type": "text CHECK (\"created_by_account_id\" = upper(\"created_by_account_id\") AND length(\"created_by_account_id\") = 26 AND substr(\"created_by_account_id\", 1, 1) GLOB '[0-7]' AND \"created_by_account_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "deployment_version_id": { + "name": "deployment_version_id", + "type": "text CHECK (\"deployment_version_id\" = upper(\"deployment_version_id\") AND length(\"deployment_version_id\") = 26 AND substr(\"deployment_version_id\", 1, 1) GLOB '[0-7]' AND \"deployment_version_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "deployment_version_number": { + "name": "deployment_version_number", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "driver_instance_id": { + "name": "driver_instance_id", + "type": "text CHECK (\"driver_instance_id\" = upper(\"driver_instance_id\") AND length(\"driver_instance_id\") = 26 AND substr(\"driver_instance_id\", 1, 1) GLOB '[0-7]' AND \"driver_instance_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "error_code": { + "name": "error_code", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "error_details_json": { + "name": "error_details_json", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "error_retryable": { + "name": "error_retryable", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "id": { + "name": "id", + "type": "text CHECK (\"id\" = upper(\"id\") AND length(\"id\") = 26 AND substr(\"id\", 1, 1) GLOB '[0-7]' AND \"id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "runtime_id": { + "name": "runtime_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "session_id": { + "name": "session_id", + "type": "text CHECK (\"session_id\" = upper(\"session_id\") AND length(\"session_id\") = 26 AND substr(\"session_id\", 1, 1) GLOB '[0-7]' AND \"session_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "started_at": { + "name": "started_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status_changed_at": { + "name": "status_changed_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "status_event": { + "name": "status_event", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'run.queue'" + }, + "status_operation_id": { + "name": "status_operation_id", + "type": "text CHECK (\"status_operation_id\" = upper(\"status_operation_id\") AND length(\"status_operation_id\") = 26 AND substr(\"status_operation_id\", 1, 1) GLOB '[0-7]' AND \"status_operation_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status_seq": { + "name": "status_seq", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "status_source": { + "name": "status_source", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'system'" + }, + "terminal_reconciliation_attempted_at": { + "name": "terminal_reconciliation_attempted_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "trace_id": { + "name": "trace_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "trigger": { + "name": "trigger", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "session_run_driver_instance_idx": { + "name": "session_run_driver_instance_idx", + "columns": ["driver_instance_id", "created_at"], + "isUnique": false + }, + "session_run_active_driver_lease_idx": { + "name": "session_run_active_driver_lease_idx", + "columns": ["driver_instance_id"], + "isUnique": true, + "where": "\"session_run\".\"driver_instance_id\" IS NOT NULL AND \"session_run\".\"status\" IN ('queued', 'booting', 'running', 'waiting_input')" + }, + "session_run_session_created_at_idx": { + "name": "session_run_session_created_at_idx", + "columns": ["session_id", "created_at"], + "isUnique": false + }, + "session_run_session_status_idx": { + "name": "session_run_session_status_idx", + "columns": ["session_id", "status"], + "isUnique": false + }, + "session_run_terminal_reconciliation_attempt_idx": { + "name": "session_run_terminal_reconciliation_attempt_idx", + "columns": ["coalesce(\"terminal_reconciliation_attempted_at\", \"updated_at\")", "id"], + "isUnique": false + } + }, + "foreignKeys": { + "session_run_session_id_session_id_fk": { + "name": "session_run_session_id_session_id_fk", + "tableFrom": "session_run", + "tableTo": "session", + "columnsFrom": ["session_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": { + "session_run_error_retryable_check": { + "name": "session_run_error_retryable_check", + "value": "\"session_run\".\"error_retryable\" IS NULL OR (\"session_run\".\"error_retryable\" IN (false, true) AND \"session_run\".\"error_code\" IS NOT NULL AND \"session_run\".\"error_details_json\" IS NOT NULL AND \"session_run\".\"error_message\" IS NOT NULL)" + }, + "session_run_status_check": { + "name": "session_run_status_check", + "value": "\"session_run\".\"status\" IN ('queued', 'booting', 'running', 'waiting_input', 'completed', 'failed', 'cancelled', 'expired')" + }, + "session_run_status_seq_check": { + "name": "session_run_status_seq_check", + "value": "\"session_run\".\"status_seq\" >= 0" + }, + "session_run_terminal_reconciliation_attempted_at_check": { + "name": "session_run_terminal_reconciliation_attempted_at_check", + "value": "\"session_run\".\"terminal_reconciliation_attempted_at\" IS NULL OR \"session_run\".\"terminal_reconciliation_attempted_at\" >= 0" + } + } + }, + "session_agent_task_snapshot": { + "name": "session_agent_task_snapshot", + "columns": { + "driver_instance_id": { + "name": "driver_instance_id", + "type": "text CHECK (\"driver_instance_id\" = upper(\"driver_instance_id\") AND length(\"driver_instance_id\") = 26 AND substr(\"driver_instance_id\", 1, 1) GLOB '[0-7]' AND \"driver_instance_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "run_id": { + "name": "run_id", + "type": "text CHECK (\"run_id\" = upper(\"run_id\") AND length(\"run_id\") = 26 AND substr(\"run_id\", 1, 1) GLOB '[0-7]' AND \"run_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "seq": { + "name": "seq", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "session_id": { + "name": "session_id", + "type": "text CHECK (\"session_id\" = upper(\"session_id\") AND length(\"session_id\") = 26 AND substr(\"session_id\", 1, 1) GLOB '[0-7]' AND \"session_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tasks_json": { + "name": "tasks_json", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "session_agent_task_snapshot_run_id_session_run_id_fk": { + "name": "session_agent_task_snapshot_run_id_session_run_id_fk", + "tableFrom": "session_agent_task_snapshot", + "tableTo": "session_run", + "columnsFrom": ["run_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "session_agent_task_snapshot_session_id_session_id_fk": { + "name": "session_agent_task_snapshot_session_id_session_id_fk", + "tableFrom": "session_agent_task_snapshot", + "tableTo": "session", + "columnsFrom": ["session_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "session_event": { + "name": "session_event", + "columns": { + "agent_id": { + "name": "agent_id", + "type": "text CHECK (\"agent_id\" = upper(\"agent_id\") AND length(\"agent_id\") = 26 AND substr(\"agent_id\", 1, 1) GLOB '[0-7]' AND \"agent_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "artifact_attempt_id": { + "name": "artifact_attempt_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "artifact_manifest_json": { + "name": "artifact_manifest_json", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "artifact_manifest_sha256": { + "name": "artifact_manifest_sha256", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "content_text": { + "name": "content_text", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "ended_at": { + "name": "ended_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "event_type": { + "name": "event_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "family": { + "name": "family", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "id": { + "name": "id", + "type": "text CHECK (\"id\" = upper(\"id\") AND length(\"id\") = 26 AND substr(\"id\", 1, 1) GLOB '[0-7]' AND \"id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "mcp_command_id": { + "name": "mcp_command_id", + "type": "text CHECK (\"mcp_command_id\" = upper(\"mcp_command_id\") AND length(\"mcp_command_id\") = 26 AND substr(\"mcp_command_id\", 1, 1) GLOB '[0-7]' AND \"mcp_command_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "occurred_at": { + "name": "occurred_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "process_status": { + "name": "process_status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "process_type": { + "name": "process_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "run_id": { + "name": "run_id", + "type": "text CHECK (\"run_id\" = upper(\"run_id\") AND length(\"run_id\") = 26 AND substr(\"run_id\", 1, 1) GLOB '[0-7]' AND \"run_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "runtime_operation_event_json": { + "name": "runtime_operation_event_json", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "semantic_hash": { + "name": "semantic_hash", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "seq": { + "name": "seq", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "session_id": { + "name": "session_id", + "type": "text CHECK (\"session_id\" = upper(\"session_id\") AND length(\"session_id\") = 26 AND substr(\"session_id\", 1, 1) GLOB '[0-7]' AND \"session_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "source_event_id": { + "name": "source_event_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "stream_id": { + "name": "stream_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "terminal_event_json": { + "name": "terminal_event_json", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "tool_call_id": { + "name": "tool_call_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "tool_input_delta_json": { + "name": "tool_input_delta_json", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "tool_input_json": { + "name": "tool_input_json", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "tool_name": { + "name": "tool_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "tool_output_delta_text": { + "name": "tool_output_delta_text", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "tool_output_text": { + "name": "tool_output_text", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "tool_parent_message_id": { + "name": "tool_parent_message_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "tool_result_message_id": { + "name": "tool_result_message_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "tool_status": { + "name": "tool_status", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "tokens": { + "name": "tokens", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "trace_id": { + "name": "trace_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "visibility": { + "name": "visibility", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "session_event_agent_family_created_idx": { + "name": "session_event_agent_family_created_idx", + "columns": ["agent_id", "family", "created_at", "id"], + "isUnique": false + }, + "session_event_artifact_attempt_idx": { + "name": "session_event_artifact_attempt_idx", + "columns": ["artifact_attempt_id"], + "isUnique": true, + "where": "\"session_event\".\"artifact_attempt_id\" IS NOT NULL" + }, + "session_event_agent_visibility_created_idx": { + "name": "session_event_agent_visibility_created_idx", + "columns": ["agent_id", "visibility", "created_at", "id"], + "isUnique": false + }, + "session_event_agent_created_idx": { + "name": "session_event_agent_created_idx", + "columns": ["agent_id", "created_at", "id"], + "isUnique": false + }, + "session_event_session_visibility_seq_idx": { + "name": "session_event_session_visibility_seq_idx", + "columns": ["session_id", "visibility", "seq"], + "isUnique": false + }, + "session_event_run_event_type_idx": { + "name": "session_event_run_event_type_idx", + "columns": ["run_id", "event_type"], + "isUnique": false + }, + "session_event_run_stream_process_seq_idx": { + "name": "session_event_run_stream_process_seq_idx", + "columns": ["run_id", "stream_id", "process_type", "seq"], + "isUnique": false + }, + "session_event_run_tool_call_seq_idx": { + "name": "session_event_run_tool_call_seq_idx", + "columns": ["run_id", "tool_call_id", "seq"], + "isUnique": false + }, + "session_event_session_seq_idx": { + "name": "session_event_session_seq_idx", + "columns": ["session_id", "seq"], + "isUnique": true + }, + "session_event_session_source_idx": { + "name": "session_event_session_source_idx", + "columns": ["session_id", "source_event_id"], + "isUnique": true + }, + "session_event_run_terminal_winner_idx": { + "name": "session_event_run_terminal_winner_idx", + "columns": ["session_id", "run_id"], + "isUnique": true, + "where": "\"session_event\".\"semantic_hash\" IS NOT NULL AND \"session_event\".\"run_id\" IS NOT NULL AND \"session_event\".\"event_type\" IN ('run.cancelled', 'run.completed', 'run.failed')" + }, + "session_event_mcp_terminal_winner_idx": { + "name": "session_event_mcp_terminal_winner_idx", + "columns": ["session_id", "mcp_command_id"], + "isUnique": true, + "where": "\"session_event\".\"mcp_command_id\" IS NOT NULL" + } + }, + "foreignKeys": { + "session_event_session_id_session_id_fk": { + "name": "session_event_session_id_session_id_fk", + "tableFrom": "session_event", + "tableTo": "session", + "columnsFrom": ["session_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": { + "session_event_artifact_manifest_check": { + "name": "session_event_artifact_manifest_check", + "value": "(\"session_event\".\"artifact_attempt_id\" IS NULL AND \"session_event\".\"artifact_manifest_json\" IS NULL AND \"session_event\".\"artifact_manifest_sha256\" IS NULL) OR (\"session_event\".\"artifact_attempt_id\" IS NOT NULL AND \"session_event\".\"artifact_manifest_json\" IS NOT NULL AND json_valid(\"session_event\".\"artifact_manifest_json\") = 1 AND json_extract(\"session_event\".\"artifact_manifest_json\", '$.version') IS 1 AND json_type(\"session_event\".\"artifact_manifest_json\", '$.captureStatus') IS 'text' AND json_extract(\"session_event\".\"artifact_manifest_json\", '$.captureStatus') IN ('complete', 'omitted_file_limit', 'omitted_runtime_unavailable', 'omitted_size_limit', 'omitted_source_changed', 'omitted_source_missing') AND json_type(\"session_event\".\"artifact_manifest_json\", '$.mode') IS 'text' AND json_extract(\"session_event\".\"artifact_manifest_json\", '$.mode') IN ('delta', 'snapshot') AND (json_extract(\"session_event\".\"artifact_manifest_json\", '$.captureStatus') = 'complete' OR json_array_length(\"session_event\".\"artifact_manifest_json\", '$.files') = 0) AND json_extract(\"session_event\".\"artifact_manifest_json\", '$.sourceEventId') IS \"session_event\".\"source_event_id\" AND json_extract(\"session_event\".\"artifact_manifest_json\", '$.semanticHash') IS \"session_event\".\"semantic_hash\" AND json_type(\"session_event\".\"artifact_manifest_json\", '$.files') IS 'array' AND \"session_event\".\"artifact_manifest_sha256\" IS NOT NULL AND length(\"session_event\".\"artifact_manifest_sha256\") = 64 AND \"session_event\".\"artifact_manifest_sha256\" = lower(\"session_event\".\"artifact_manifest_sha256\") AND \"session_event\".\"artifact_manifest_sha256\" NOT GLOB '*[^0-9a-f]*' AND \"session_event\".\"semantic_hash\" IS NOT NULL AND \"session_event\".\"event_type\" IN ('file.change.updated', 'file.changed', 'run.completed'))" + }, + "session_event_mcp_command_check": { + "name": "session_event_mcp_command_check", + "value": "\"session_event\".\"mcp_command_id\" IS NULL OR (\"session_event\".\"event_type\" = 'tool.call.updated' AND \"session_event\".\"tool_status\" IS NOT NULL AND \"session_event\".\"tool_status\" IN ('completed', 'failed', 'cancelled'))" + }, + "session_event_runtime_operation_event_json_check": { + "name": "session_event_runtime_operation_event_json_check", + "value": "\"session_event\".\"runtime_operation_event_json\" IS NULL OR (json_valid(\"session_event\".\"runtime_operation_event_json\") = 1 AND json_extract(\"session_event\".\"runtime_operation_event_json\", '$.kind') IS 'agent.task.updated' AND json_type(\"session_event\".\"runtime_operation_event_json\", '$.payload') IS 'object' AND json_extract(\"session_event\".\"runtime_operation_event_json\", '$.payload.status') IN ('updating', 'ready') AND \"session_event\".\"semantic_hash\" IS NOT NULL AND \"session_event\".\"event_type\" = 'agent.task.updated')" + }, + "session_event_semantic_hash_check": { + "name": "session_event_semantic_hash_check", + "value": "\"session_event\".\"semantic_hash\" IS NULL OR (length(\"session_event\".\"semantic_hash\") = 64 AND \"session_event\".\"semantic_hash\" = lower(\"session_event\".\"semantic_hash\") AND \"session_event\".\"semantic_hash\" NOT GLOB '*[^0-9a-f]*')" + }, + "session_event_terminal_event_json_check": { + "name": "session_event_terminal_event_json_check", + "value": "(\"session_event\".\"terminal_event_json\" IS NULL AND NOT (\"session_event\".\"semantic_hash\" IS NOT NULL AND \"session_event\".\"event_type\" IN ('run.cancelled', 'run.completed', 'run.failed'))) OR (\"session_event\".\"terminal_event_json\" IS NOT NULL AND json_valid(\"session_event\".\"terminal_event_json\") = 1 AND \"session_event\".\"semantic_hash\" IS NOT NULL AND \"session_event\".\"event_type\" IN ('run.cancelled', 'run.completed', 'run.failed'))" + }, + "session_event_tool_input_kind_check": { + "name": "session_event_tool_input_kind_check", + "value": "\"session_event\".\"tool_input_delta_json\" IS NULL OR \"session_event\".\"tool_input_json\" IS NULL" + }, + "session_event_tool_output_kind_check": { + "name": "session_event_tool_output_kind_check", + "value": "\"session_event\".\"tool_output_delta_text\" IS NULL OR \"session_event\".\"tool_output_text\" IS NULL" + }, + "session_event_tool_status_check": { + "name": "session_event_tool_status_check", + "value": "\"session_event\".\"tool_status\" IS NULL OR \"session_event\".\"tool_status\" IN ('running', 'completed', 'failed', 'cancelled')" + } + } + }, + "session_model_call": { + "name": "session_model_call", + "columns": { + "cache_creation_tokens": { + "name": "cache_creation_tokens", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "cache_read_tokens": { + "name": "cache_read_tokens", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "call_key": { + "name": "call_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "completed_at": { + "name": "completed_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "cost_currency": { + "name": "cost_currency", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "driver_instance_id": { + "name": "driver_instance_id", + "type": "text CHECK (\"driver_instance_id\" = upper(\"driver_instance_id\") AND length(\"driver_instance_id\") = 26 AND substr(\"driver_instance_id\", 1, 1) GLOB '[0-7]' AND \"driver_instance_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "error_code": { + "name": "error_code", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "id": { + "name": "id", + "type": "text CHECK (\"id\" = upper(\"id\") AND length(\"id\") = 26 AND substr(\"id\", 1, 1) GLOB '[0-7]' AND \"id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "input_tokens": { + "name": "input_tokens", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "metadata_json": { + "name": "metadata_json", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "native_call_id": { + "name": "native_call_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "output_tokens": { + "name": "output_tokens", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "source_event_seq": { + "name": "source_event_seq", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "session_id": { + "name": "session_id", + "type": "text CHECK (\"session_id\" = upper(\"session_id\") AND length(\"session_id\") = 26 AND substr(\"session_id\", 1, 1) GLOB '[0-7]' AND \"session_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "session_run_id": { + "name": "session_run_id", + "type": "text CHECK (\"session_run_id\" = upper(\"session_run_id\") AND length(\"session_run_id\") = 26 AND substr(\"session_run_id\", 1, 1) GLOB '[0-7]' AND \"session_run_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "started_at": { + "name": "started_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "total_cost_usd_micros": { + "name": "total_cost_usd_micros", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "trace_id": { + "name": "trace_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "session_model_call_run_created_idx": { + "name": "session_model_call_run_created_idx", + "columns": ["session_run_id", "created_at"], + "isUnique": false + }, + "session_model_call_session_created_idx": { + "name": "session_model_call_session_created_idx", + "columns": ["session_id", "created_at"], + "isUnique": false + }, + "session_model_call_run_key_idx": { + "name": "session_model_call_run_key_idx", + "columns": ["session_run_id", "call_key"], + "isUnique": true + }, + "session_model_call_native_idx": { + "name": "session_model_call_native_idx", + "columns": ["driver_instance_id", "native_call_id"], + "isUnique": true + } + }, + "foreignKeys": { + "session_model_call_session_id_session_id_fk": { + "name": "session_model_call_session_id_session_id_fk", + "tableFrom": "session_model_call", + "tableTo": "session", + "columnsFrom": ["session_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "session_model_call_session_run_id_session_run_id_fk": { + "name": "session_model_call_session_run_id_session_run_id_fk", + "tableFrom": "session_model_call", + "tableTo": "session_run", + "columnsFrom": ["session_run_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": { + "session_model_call_source_event_seq_check": { + "name": "session_model_call_source_event_seq_check", + "value": "\"session_model_call\".\"source_event_seq\" >= 0" + } + } + }, + "session_permission_request": { + "name": "session_permission_request", + "columns": { + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "driver_instance_id": { + "name": "driver_instance_id", + "type": "text CHECK (\"driver_instance_id\" = upper(\"driver_instance_id\") AND length(\"driver_instance_id\") = 26 AND substr(\"driver_instance_id\", 1, 1) GLOB '[0-7]' AND \"driver_instance_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "raw_input": { + "name": "raw_input", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "request_id": { + "name": "request_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "run_id": { + "name": "run_id", + "type": "text CHECK (\"run_id\" = upper(\"run_id\") AND length(\"run_id\") = 26 AND substr(\"run_id\", 1, 1) GLOB '[0-7]' AND \"run_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "session_id": { + "name": "session_id", + "type": "text CHECK (\"session_id\" = upper(\"session_id\") AND length(\"session_id\") = 26 AND substr(\"session_id\", 1, 1) GLOB '[0-7]' AND \"session_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "tool_call_id": { + "name": "tool_call_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "tool_kind": { + "name": "tool_kind", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "session_permission_request_run_idx": { + "name": "session_permission_request_run_idx", + "columns": ["session_id", "run_id"], + "isUnique": false + } + }, + "foreignKeys": { + "session_permission_request_session_id_session_id_fk": { + "name": "session_permission_request_session_id_session_id_fk", + "tableFrom": "session_permission_request", + "tableTo": "session", + "columnsFrom": ["session_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "session_permission_request_session_id_request_id_pk": { + "columns": ["session_id", "request_id"], + "name": "session_permission_request_session_id_request_id_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "session_readiness_snapshot": { + "name": "session_readiness_snapshot", + "columns": { + "readiness_json": { + "name": "readiness_json", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "session_id": { + "name": "session_id", + "type": "text CHECK (\"session_id\" = upper(\"session_id\") AND length(\"session_id\") = 26 AND substr(\"session_id\", 1, 1) GLOB '[0-7]' AND \"session_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "session_readiness_snapshot_session_id_session_id_fk": { + "name": "session_readiness_snapshot_session_id_session_id_fk", + "tableFrom": "session_readiness_snapshot", + "tableTo": "session", + "columnsFrom": ["session_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "skill_snapshot_entry": { + "name": "skill_snapshot_entry", + "columns": { + "entry_kind": { + "name": "entry_kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "is_executable": { + "name": "is_executable", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "mime_type": { + "name": "mime_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "path": { + "name": "path", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "sha256": { + "name": "sha256", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "size": { + "name": "size", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "snapshot_id": { + "name": "snapshot_id", + "type": "text CHECK (\"snapshot_id\" = upper(\"snapshot_id\") AND length(\"snapshot_id\") = 26 AND substr(\"snapshot_id\", 1, 1) GLOB '[0-7]' AND \"snapshot_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "skill_snapshot_entry_snapshot_id_path_pk": { + "columns": ["snapshot_id", "path"], + "name": "skill_snapshot_entry_snapshot_id_path_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "skill_snapshot": { + "name": "skill_snapshot", + "columns": { + "author": { + "name": "author", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "blob_key": { + "name": "blob_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "blob_sha256": { + "name": "blob_sha256", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "blob_size": { + "name": "blob_size", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "id": { + "name": "id", + "type": "text CHECK (\"id\" = upper(\"id\") AND length(\"id\") = 26 AND substr(\"id\", 1, 1) GLOB '[0-7]' AND \"id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "app_id": { + "name": "app_id", + "type": "text CHECK (\"app_id\" = upper(\"app_id\") AND length(\"app_id\") = 26 AND substr(\"app_id\", 1, 1) GLOB '[0-7]' AND \"app_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "skill_markdown_path": { + "name": "skill_markdown_path", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "uncompressed_size": { + "name": "uncompressed_size", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "version": { + "name": "version", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "skill_snapshot_app_created_at_idx": { + "name": "skill_snapshot_app_created_at_idx", + "columns": ["app_id", "created_at"], + "isUnique": false + }, + "skill_snapshot_blob_sha256_idx": { + "name": "skill_snapshot_blob_sha256_idx", + "columns": ["app_id", "blob_sha256"], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "skill": { + "name": "skill", + "columns": { + "author": { + "name": "author", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "current_snapshot_id": { + "name": "current_snapshot_id", + "type": "text CHECK (\"current_snapshot_id\" = upper(\"current_snapshot_id\") AND length(\"current_snapshot_id\") = 26 AND substr(\"current_snapshot_id\", 1, 1) GLOB '[0-7]' AND \"current_snapshot_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "forked_from_owner_name": { + "name": "forked_from_owner_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "forked_from_skill_id": { + "name": "forked_from_skill_id", + "type": "text CHECK (\"forked_from_skill_id\" = upper(\"forked_from_skill_id\") AND length(\"forked_from_skill_id\") = 26 AND substr(\"forked_from_skill_id\", 1, 1) GLOB '[0-7]' AND \"forked_from_skill_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "forked_from_skill_name": { + "name": "forked_from_skill_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "id": { + "name": "id", + "type": "text CHECK (\"id\" = upper(\"id\") AND length(\"id\") = 26 AND substr(\"id\", 1, 1) GLOB '[0-7]' AND \"id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "owner_account_id": { + "name": "owner_account_id", + "type": "text CHECK (\"owner_account_id\" = upper(\"owner_account_id\") AND length(\"owner_account_id\") = 26 AND substr(\"owner_account_id\", 1, 1) GLOB '[0-7]' AND \"owner_account_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "app_id": { + "name": "app_id", + "type": "text CHECK (\"app_id\" = upper(\"app_id\") AND length(\"app_id\") = 26 AND substr(\"app_id\", 1, 1) GLOB '[0-7]' AND \"app_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "source_kind": { + "name": "source_kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "version": { + "name": "version", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "skill_app_updated_at_idx": { + "name": "skill_app_updated_at_idx", + "columns": ["app_id", "updated_at"], + "isUnique": false + }, + "skill_owner_account_updated_at_idx": { + "name": "skill_owner_account_updated_at_idx", + "columns": ["owner_account_id", "updated_at"], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "account": { + "name": "account", + "columns": { + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "email_verified": { + "name": "email_verified", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "id": { + "name": "id", + "type": "text CHECK (\"id\" = upper(\"id\") AND length(\"id\") = 26 AND substr(\"id\", 1, 1) GLOB '[0-7]' AND \"id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "image_url": { + "name": "image_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_active_organization_id": { + "name": "last_active_organization_id", + "type": "text CHECK (\"last_active_organization_id\" = upper(\"last_active_organization_id\") AND length(\"last_active_organization_id\") = 26 AND substr(\"last_active_organization_id\", 1, 1) GLOB '[0-7]' AND \"last_active_organization_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "system_agent_model": { + "name": "system_agent_model", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "account_email_idx": { + "name": "account_email_idx", + "columns": ["email"], + "isUnique": true + }, + "account_last_active_organization_idx": { + "name": "account_last_active_organization_idx", + "columns": ["last_active_organization_id"], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "usage_daily_rollup": { + "name": "usage_daily_rollup", + "columns": { + "actor_user_id": { + "name": "actor_user_id", + "type": "text CHECK (\"actor_user_id\" = upper(\"actor_user_id\") AND length(\"actor_user_id\") = 26 AND substr(\"actor_user_id\", 1, 1) GLOB '[0-7]' AND \"actor_user_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "agent_id": { + "name": "agent_id", + "type": "text CHECK (\"agent_id\" = upper(\"agent_id\") AND length(\"agent_id\") = 26 AND substr(\"agent_id\", 1, 1) GLOB '[0-7]' AND \"agent_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "agent_owner_user_id": { + "name": "agent_owner_user_id", + "type": "text CHECK (\"agent_owner_user_id\" = upper(\"agent_owner_user_id\") AND length(\"agent_owner_user_id\") = 26 AND substr(\"agent_owner_user_id\", 1, 1) GLOB '[0-7]' AND \"agent_owner_user_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "agent_publication_state_at_run": { + "name": "agent_publication_state_at_run", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "cache_creation_tokens": { + "name": "cache_creation_tokens", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "cache_read_tokens": { + "name": "cache_read_tokens", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "date": { + "name": "date", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "input_tokens": { + "name": "input_tokens", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "organization_id": { + "name": "organization_id", + "type": "text CHECK (\"organization_id\" = upper(\"organization_id\") AND length(\"organization_id\") = 26 AND substr(\"organization_id\", 1, 1) GLOB '[0-7]' AND \"organization_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "app_id": { + "name": "app_id", + "type": "text CHECK (\"app_id\" = upper(\"app_id\") AND length(\"app_id\") = 26 AND substr(\"app_id\", 1, 1) GLOB '[0-7]' AND \"app_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "output_tokens": { + "name": "output_tokens", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "request_count": { + "name": "request_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "run_purpose": { + "name": "run_purpose", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "total_cost_usd_micros": { + "name": "total_cost_usd_micros", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "unpriced_request_count": { + "name": "unpriced_request_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "usage_daily_rollup_app_date_idx": { + "name": "usage_daily_rollup_app_date_idx", + "columns": ["app_id", "date"], + "isUnique": false + }, + "usage_daily_rollup_organization_date_idx": { + "name": "usage_daily_rollup_organization_date_idx", + "columns": ["organization_id", "date"], + "isUnique": false + }, + "usage_daily_rollup_agent_date_idx": { + "name": "usage_daily_rollup_agent_date_idx", + "columns": ["agent_id", "date"], + "isUnique": false + }, + "usage_daily_rollup_actor_date_idx": { + "name": "usage_daily_rollup_actor_date_idx", + "columns": ["actor_user_id", "date"], + "isUnique": false + }, + "usage_daily_rollup_owner_date_idx": { + "name": "usage_daily_rollup_owner_date_idx", + "columns": ["agent_owner_user_id", "date"], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "usage_daily_rollup_organization_id_app_id_agent_id_actor_user_id_agent_owner_user_id_date_agent_publication_state_at_run_run_purpose_provider_model_pk": { + "columns": [ + "organization_id", + "app_id", + "agent_id", + "actor_user_id", + "agent_owner_user_id", + "date", + "agent_publication_state_at_run", + "run_purpose", + "provider", + "model" + ], + "name": "usage_daily_rollup_organization_id_app_id_agent_id_actor_user_id_agent_owner_user_id_date_agent_publication_state_at_run_run_purpose_provider_model_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "usage_event_rollup_receipt": { + "name": "usage_event_rollup_receipt", + "columns": { + "rolled_up_at": { + "name": "rolled_up_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "source_event_id": { + "name": "source_event_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "usage_event_rollup_receipt_rolled_up_at_idx": { + "name": "usage_event_rollup_receipt_rolled_up_at_idx", + "columns": ["rolled_up_at"], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "usage_event_rollup_receipt_source_source_event_id_pk": { + "columns": ["source", "source_event_id"], + "name": "usage_event_rollup_receipt_source_source_event_id_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "usage_event": { + "name": "usage_event", + "columns": { + "actor_user_id": { + "name": "actor_user_id", + "type": "text CHECK (\"actor_user_id\" = upper(\"actor_user_id\") AND length(\"actor_user_id\") = 26 AND substr(\"actor_user_id\", 1, 1) GLOB '[0-7]' AND \"actor_user_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "agent_id": { + "name": "agent_id", + "type": "text CHECK (\"agent_id\" = upper(\"agent_id\") AND length(\"agent_id\") = 26 AND substr(\"agent_id\", 1, 1) GLOB '[0-7]' AND \"agent_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "agent_owner_user_id": { + "name": "agent_owner_user_id", + "type": "text CHECK (\"agent_owner_user_id\" = upper(\"agent_owner_user_id\") AND length(\"agent_owner_user_id\") = 26 AND substr(\"agent_owner_user_id\", 1, 1) GLOB '[0-7]' AND \"agent_owner_user_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "agent_publication_state_at_run": { + "name": "agent_publication_state_at_run", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "agent_revision_id": { + "name": "agent_revision_id", + "type": "text CHECK (\"agent_revision_id\" = upper(\"agent_revision_id\") AND length(\"agent_revision_id\") = 26 AND substr(\"agent_revision_id\", 1, 1) GLOB '[0-7]' AND \"agent_revision_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "cache_creation_tokens": { + "name": "cache_creation_tokens", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "cache_read_tokens": { + "name": "cache_read_tokens", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "id": { + "name": "id", + "type": "text CHECK (\"id\" = upper(\"id\") AND length(\"id\") = 26 AND substr(\"id\", 1, 1) GLOB '[0-7]' AND \"id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "input_tokens": { + "name": "input_tokens", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "organization_id": { + "name": "organization_id", + "type": "text CHECK (\"organization_id\" = upper(\"organization_id\") AND length(\"organization_id\") = 26 AND substr(\"organization_id\", 1, 1) GLOB '[0-7]' AND \"organization_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "app_id": { + "name": "app_id", + "type": "text CHECK (\"app_id\" = upper(\"app_id\") AND length(\"app_id\") = 26 AND substr(\"app_id\", 1, 1) GLOB '[0-7]' AND \"app_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "output_tokens": { + "name": "output_tokens", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "price_snapshot_json": { + "name": "price_snapshot_json", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "pricing_status": { + "name": "pricing_status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "run_purpose": { + "name": "run_purpose", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "runtime_id": { + "name": "runtime_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "session_id": { + "name": "session_id", + "type": "text CHECK (\"session_id\" = upper(\"session_id\") AND length(\"session_id\") = 26 AND substr(\"session_id\", 1, 1) GLOB '[0-7]' AND \"session_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "session_run_id": { + "name": "session_run_id", + "type": "text CHECK (\"session_run_id\" = upper(\"session_run_id\") AND length(\"session_run_id\") = 26 AND substr(\"session_run_id\", 1, 1) GLOB '[0-7]' AND \"session_run_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "source_event_id": { + "name": "source_event_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "source_event_seq": { + "name": "source_event_seq", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "total_cost_usd_micros": { + "name": "total_cost_usd_micros", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "usage_contract": { + "name": "usage_contract", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "usage_event_app_created_idx": { + "name": "usage_event_app_created_idx", + "columns": ["app_id", "created_at"], + "isUnique": false + }, + "usage_event_organization_created_idx": { + "name": "usage_event_organization_created_idx", + "columns": ["organization_id", "created_at"], + "isUnique": false + }, + "usage_event_agent_created_idx": { + "name": "usage_event_agent_created_idx", + "columns": ["agent_id", "created_at"], + "isUnique": false + }, + "usage_event_actor_created_idx": { + "name": "usage_event_actor_created_idx", + "columns": ["actor_user_id", "created_at"], + "isUnique": false + }, + "usage_event_owner_created_idx": { + "name": "usage_event_owner_created_idx", + "columns": ["agent_owner_user_id", "created_at"], + "isUnique": false + }, + "usage_event_session_run_idx": { + "name": "usage_event_session_run_idx", + "columns": ["session_run_id"], + "isUnique": false + }, + "usage_event_source_event_idx": { + "name": "usage_event_source_event_idx", + "columns": ["source", "source_event_id"], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": { + "usage_event_source_event_seq_check": { + "name": "usage_event_source_event_seq_check", + "value": "\"usage_event\".\"source_event_seq\" >= 0" + } + } + }, + "vendor_credential": { + "name": "vendor_credential", + "columns": { + "api_base": { + "name": "api_base", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "api_key_secret_id": { + "name": "api_key_secret_id", + "type": "text CHECK (\"api_key_secret_id\" = upper(\"api_key_secret_id\") AND length(\"api_key_secret_id\") = 26 AND substr(\"api_key_secret_id\", 1, 1) GLOB '[0-7]' AND \"api_key_secret_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "id": { + "name": "id", + "type": "text CHECK (\"id\" = upper(\"id\") AND length(\"id\") = 26 AND substr(\"id\", 1, 1) GLOB '[0-7]' AND \"id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "is_default": { + "name": "is_default", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "models": { + "name": "models", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "app_id": { + "name": "app_id", + "type": "text CHECK (\"app_id\" = upper(\"app_id\") AND length(\"app_id\") = 26 AND substr(\"app_id\", 1, 1) GLOB '[0-7]' AND \"app_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "vendor_id": { + "name": "vendor_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "vendor_credential_app_vendor_idx": { + "name": "vendor_credential_app_vendor_idx", + "columns": ["app_id", "vendor_id"], + "isUnique": false + }, + "vendor_credential_app_vendor_name_idx": { + "name": "vendor_credential_app_vendor_name_idx", + "columns": ["app_id", "vendor_id", "name"], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + } + }, + "views": {}, + "enums": {}, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + }, + "internal": { + "indexes": { + "file_record_listing_idx": { + "columns": { + "lower(\"name\")": { + "isExpression": true + } + } + }, + "session_run_terminal_reconciliation_attempt_idx": { + "columns": { + "coalesce(\"terminal_reconciliation_attempted_at\", \"updated_at\")": { + "isExpression": true + } + } + } + } + } +} diff --git a/pkgs/db/drizzle/meta/0019_snapshot.json b/pkgs/db/drizzle/meta/0019_snapshot.json new file mode 100644 index 00000000..5a44a67b --- /dev/null +++ b/pkgs/db/drizzle/meta/0019_snapshot.json @@ -0,0 +1,7922 @@ +{ + "version": "6", + "dialect": "sqlite", + "id": "9cf52036-2892-44fe-b0bc-4d45eef1d17f", + "prevId": "d52f396d-150e-422a-8233-a53ca29c3cfc", + "tables": { + "agent_deployment_version": { + "name": "agent_deployment_version", + "columns": { + "agent_id": { + "name": "agent_id", + "type": "text CHECK (\"agent_id\" = upper(\"agent_id\") AND length(\"agent_id\") = 26 AND substr(\"agent_id\", 1, 1) GLOB '[0-7]' AND \"agent_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "config_json": { + "name": "config_json", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_by_account_id": { + "name": "created_by_account_id", + "type": "text CHECK (\"created_by_account_id\" = upper(\"created_by_account_id\") AND length(\"created_by_account_id\") = 26 AND substr(\"created_by_account_id\", 1, 1) GLOB '[0-7]' AND \"created_by_account_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "environment_id": { + "name": "environment_id", + "type": "text CHECK (\"environment_id\" = upper(\"environment_id\") AND length(\"environment_id\") = 26 AND substr(\"environment_id\", 1, 1) GLOB '[0-7]' AND \"environment_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "id": { + "name": "id", + "type": "text CHECK (\"id\" = upper(\"id\") AND length(\"id\") = 26 AND substr(\"id\", 1, 1) GLOB '[0-7]' AND \"id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "mcp_bindings_json": { + "name": "mcp_bindings_json", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "prompt": { + "name": "prompt", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "runtime_id": { + "name": "runtime_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "skills_json": { + "name": "skills_json", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "summary": { + "name": "summary", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "version_number": { + "name": "version_number", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "agent_deployment_version_agent_number_idx": { + "name": "agent_deployment_version_agent_number_idx", + "columns": ["agent_id", "version_number"], + "isUnique": true + }, + "agent_deployment_version_agent_created_idx": { + "name": "agent_deployment_version_agent_created_idx", + "columns": ["agent_id", "created_at"], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "agent_mcp_binding": { + "name": "agent_mcp_binding", + "columns": { + "agent_credential_id": { + "name": "agent_credential_id", + "type": "text CHECK (\"agent_credential_id\" = upper(\"agent_credential_id\") AND length(\"agent_credential_id\") = 26 AND substr(\"agent_credential_id\", 1, 1) GLOB '[0-7]' AND \"agent_credential_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "agent_id": { + "name": "agent_id", + "type": "text CHECK (\"agent_id\" = upper(\"agent_id\") AND length(\"agent_id\") = 26 AND substr(\"agent_id\", 1, 1) GLOB '[0-7]' AND \"agent_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "credential_mode": { + "name": "credential_mode", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'runtime_resolved'" + }, + "enabled": { + "name": "enabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "id": { + "name": "id", + "type": "text CHECK (\"id\" = upper(\"id\") AND length(\"id\") = 26 AND substr(\"id\", 1, 1) GLOB '[0-7]' AND \"id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "server_id": { + "name": "server_id", + "type": "text CHECK (\"server_id\" = upper(\"server_id\") AND length(\"server_id\") = 26 AND substr(\"server_id\", 1, 1) GLOB '[0-7]' AND \"server_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "agent_mcp_binding_agent_sort_idx": { + "name": "agent_mcp_binding_agent_sort_idx", + "columns": ["agent_id", "sort_order"], + "isUnique": true + }, + "agent_mcp_binding_server_idx": { + "name": "agent_mcp_binding_server_idx", + "columns": ["server_id"], + "isUnique": false + }, + "agent_mcp_binding_profile_server_idx": { + "name": "agent_mcp_binding_profile_server_idx", + "columns": ["agent_id", "server_id"], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": { + "agent_mcp_binding_agent_credential_shape_check": { + "name": "agent_mcp_binding_agent_credential_shape_check", + "value": "\n (\"agent_mcp_binding\".\"credential_mode\" = 'agent_bound' AND \"agent_mcp_binding\".\"agent_credential_id\" IS NOT NULL)\n OR (\"agent_mcp_binding\".\"credential_mode\" = 'runtime_resolved' AND \"agent_mcp_binding\".\"agent_credential_id\" IS NULL)\n " + } + } + }, + "agent_skill": { + "name": "agent_skill", + "columns": { + "agent_id": { + "name": "agent_id", + "type": "text CHECK (\"agent_id\" = upper(\"agent_id\") AND length(\"agent_id\") = 26 AND substr(\"agent_id\", 1, 1) GLOB '[0-7]' AND \"agent_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "skill_id": { + "name": "skill_id", + "type": "text CHECK (\"skill_id\" = upper(\"skill_id\") AND length(\"skill_id\") = 26 AND substr(\"skill_id\", 1, 1) GLOB '[0-7]' AND \"skill_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "agent_skill_agent_sort_idx": { + "name": "agent_skill_agent_sort_idx", + "columns": ["agent_id", "sort_order"], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "agent_skill_agent_id_skill_id_pk": { + "columns": ["agent_id", "skill_id"], + "name": "agent_skill_agent_id_skill_id_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "agent": { + "name": "agent", + "columns": { + "config_json": { + "name": "config_json", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "environment_id": { + "name": "environment_id", + "type": "text CHECK (\"environment_id\" = upper(\"environment_id\") AND length(\"environment_id\") = 26 AND substr(\"environment_id\", 1, 1) GLOB '[0-7]' AND \"environment_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "id": { + "name": "id", + "type": "text CHECK (\"id\" = upper(\"id\") AND length(\"id\") = 26 AND substr(\"id\", 1, 1) GLOB '[0-7]' AND \"id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'pet'" + }, + "live_deployment_version_id": { + "name": "live_deployment_version_id", + "type": "text CHECK (\"live_deployment_version_id\" = upper(\"live_deployment_version_id\") AND length(\"live_deployment_version_id\") = 26 AND substr(\"live_deployment_version_id\", 1, 1) GLOB '[0-7]' AND \"live_deployment_version_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "owner_account_id": { + "name": "owner_account_id", + "type": "text CHECK (\"owner_account_id\" = upper(\"owner_account_id\") AND length(\"owner_account_id\") = 26 AND substr(\"owner_account_id\", 1, 1) GLOB '[0-7]' AND \"owner_account_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "app_id": { + "name": "app_id", + "type": "text CHECK (\"app_id\" = upper(\"app_id\") AND length(\"app_id\") = 26 AND substr(\"app_id\", 1, 1) GLOB '[0-7]' AND \"app_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "prompt": { + "name": "prompt", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "runtime_id": { + "name": "runtime_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'draft'" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "visibility": { + "name": "visibility", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'private'" + } + }, + "indexes": { + "agent_app_owner_account_idx": { + "name": "agent_app_owner_account_idx", + "columns": ["app_id", "owner_account_id"], + "isUnique": false + }, + "agent_app_status_idx": { + "name": "agent_app_status_idx", + "columns": ["app_id", "status"], + "isUnique": false + }, + "agent_environment_idx": { + "name": "agent_environment_idx", + "columns": ["environment_id"], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": { + "agent_published_live_deployment_version_check": { + "name": "agent_published_live_deployment_version_check", + "value": "\"agent\".\"status\" <> 'published' OR \"agent\".\"live_deployment_version_id\" IS NOT NULL" + } + } + }, + "api_command": { + "name": "api_command", + "columns": { + "attempt_count": { + "name": "attempt_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "claim_expires_at": { + "name": "claim_expires_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "claim_owner": { + "name": "claim_owner", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "completed_at": { + "name": "completed_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "dedupe_key": { + "name": "dedupe_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "delivery_generation": { + "name": "delivery_generation", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 1 + }, + "id": { + "name": "id", + "type": "text CHECK (\"id\" = upper(\"id\") AND length(\"id\") = 26 AND substr(\"id\", 1, 1) GLOB '[0-7]' AND \"id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "last_error_code": { + "name": "last_error_code", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_error_message": { + "name": "last_error_message", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "payload_json": { + "name": "payload_json", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "api_command_dedupe_idx": { + "name": "api_command_dedupe_idx", + "columns": ["dedupe_key"], + "isUnique": true + }, + "api_command_status_updated_idx": { + "name": "api_command_status_updated_idx", + "columns": ["status", "updated_at"], + "isUnique": false + }, + "api_command_claim_idx": { + "name": "api_command_claim_idx", + "columns": ["status", "claim_expires_at"], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": { + "api_command_delivery_generation_check": { + "name": "api_command_delivery_generation_check", + "value": "typeof(\"api_command\".\"delivery_generation\") = 'integer' AND \"api_command\".\"delivery_generation\" BETWEEN 1 AND 9007199254740991" + } + } + }, + "auth_account": { + "name": "auth_account", + "columns": { + "access_token": { + "name": "access_token", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "access_token_expires_at": { + "name": "access_token_expires_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "provider_account_id": { + "name": "provider_account_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "id_token": { + "name": "id_token", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "refresh_token": { + "name": "refresh_token", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "refresh_token_expires_at": { + "name": "refresh_token_expires_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "account_id": { + "name": "account_id", + "type": "text CHECK (\"account_id\" = upper(\"account_id\") AND length(\"account_id\") = 26 AND substr(\"account_id\", 1, 1) GLOB '[0-7]' AND \"account_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "auth_account_provider_account_idx": { + "name": "auth_account_provider_account_idx", + "columns": ["provider_id", "provider_account_id"], + "isUnique": true + }, + "auth_account_account_id_idx": { + "name": "auth_account_account_id_idx", + "columns": ["account_id"], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "auth_session": { + "name": "auth_session", + "columns": { + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "ip_address": { + "name": "ip_address", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "account_id": { + "name": "account_id", + "type": "text CHECK (\"account_id\" = upper(\"account_id\") AND length(\"account_id\") = 26 AND substr(\"account_id\", 1, 1) GLOB '[0-7]' AND \"account_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "auth_session_expires_at_idx": { + "name": "auth_session_expires_at_idx", + "columns": ["expires_at"], + "isUnique": false + }, + "auth_session_token_idx": { + "name": "auth_session_token_idx", + "columns": ["token"], + "isUnique": true + }, + "auth_session_account_id_idx": { + "name": "auth_session_account_id_idx", + "columns": ["account_id"], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "auth_verification": { + "name": "auth_verification", + "columns": { + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "identifier": { + "name": "identifier", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "auth_verification_expires_at_idx": { + "name": "auth_verification_expires_at_idx", + "columns": ["expires_at"], + "isUnique": false + }, + "auth_verification_identifier_idx": { + "name": "auth_verification_identifier_idx", + "columns": ["identifier"], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "cli_oauth_flow": { + "name": "cli_oauth_flow", + "columns": { + "account_id": { + "name": "account_id", + "type": "text CHECK (\"account_id\" = upper(\"account_id\") AND length(\"account_id\") = 26 AND substr(\"account_id\", 1, 1) GLOB '[0-7]' AND \"account_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "authorized_at": { + "name": "authorized_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "completed_at": { + "name": "completed_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "device_code_hash": { + "name": "device_code_hash", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "hostname": { + "name": "hostname", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "id": { + "name": "id", + "type": "text CHECK (\"id\" = upper(\"id\") AND length(\"id\") = 26 AND substr(\"id\", 1, 1) GLOB '[0-7]' AND \"id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_code": { + "name": "user_code", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "cli_oauth_flow_status_expires_idx": { + "name": "cli_oauth_flow_status_expires_idx", + "columns": ["status", "expires_at"], + "isUnique": false + }, + "cli_oauth_flow_device_code_hash_idx": { + "name": "cli_oauth_flow_device_code_hash_idx", + "columns": ["device_code_hash"], + "isUnique": true + }, + "cli_oauth_flow_user_code_idx": { + "name": "cli_oauth_flow_user_code_idx", + "columns": ["user_code"], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "personal_access_token": { + "name": "personal_access_token", + "columns": { + "account_id": { + "name": "account_id", + "type": "text CHECK (\"account_id\" = upper(\"account_id\") AND length(\"account_id\") = 26 AND substr(\"account_id\", 1, 1) GLOB '[0-7]' AND \"account_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "id": { + "name": "id", + "type": "text CHECK (\"id\" = upper(\"id\") AND length(\"id\") = 26 AND substr(\"id\", 1, 1) GLOB '[0-7]' AND \"id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "last_used_at": { + "name": "last_used_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "revoked_at": { + "name": "revoked_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "token_hash": { + "name": "token_hash", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "personal_access_token_account_created_idx": { + "name": "personal_access_token_account_created_idx", + "columns": ["account_id", "created_at"], + "isUnique": false + }, + "personal_access_token_hash_idx": { + "name": "personal_access_token_hash_idx", + "columns": ["token_hash"], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "email_log": { + "name": "email_log", + "columns": { + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "recipient_domain": { + "name": "recipient_domain", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "recipient_masked": { + "name": "recipient_masked", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "subject": { + "name": "subject", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "email_log_created_at_idx": { + "name": "email_log_created_at_idx", + "columns": ["created_at"], + "isUnique": false + }, + "email_log_type_status_idx": { + "name": "email_log_type_status_idx", + "columns": ["type", "status"], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "environment_package_artifact_backup_staging": { + "name": "environment_package_artifact_backup_staging", + "columns": { + "actual_backup_id": { + "name": "actual_backup_id", + "type": "text CHECK (\"actual_backup_id\" = upper(\"actual_backup_id\") AND length(\"actual_backup_id\") = 26 AND substr(\"actual_backup_id\", 1, 1) GLOB '[0-7]' AND \"actual_backup_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "app_id": { + "name": "app_id", + "type": "text CHECK (\"app_id\" = upper(\"app_id\") AND length(\"app_id\") = 26 AND substr(\"app_id\", 1, 1) GLOB '[0-7]' AND \"app_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "attempt_count": { + "name": "attempt_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "claim_owner": { + "name": "claim_owner", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "command_id": { + "name": "command_id", + "type": "text CHECK (\"command_id\" = upper(\"command_id\") AND length(\"command_id\") = 26 AND substr(\"command_id\", 1, 1) GLOB '[0-7]' AND \"command_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "delivery_generation": { + "name": "delivery_generation", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "dir": { + "name": "dir", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "input_digest": { + "name": "input_digest", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "paths_json": { + "name": "paths_json", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "environment_package_artifact_backup_staging_actual_idx": { + "name": "environment_package_artifact_backup_staging_actual_idx", + "columns": ["actual_backup_id"], + "isUnique": true, + "where": "\"environment_package_artifact_backup_staging\".\"actual_backup_id\" IS NOT NULL" + }, + "environment_package_artifact_backup_staging_intent_idx": { + "name": "environment_package_artifact_backup_staging_intent_idx", + "columns": ["app_id", "input_digest"], + "isUnique": true + }, + "environment_package_artifact_backup_staging_updated_idx": { + "name": "environment_package_artifact_backup_staging_updated_idx", + "columns": ["updated_at", "command_id"], + "isUnique": false + } + }, + "foreignKeys": { + "environment_package_artifact_backup_staging_command_id_api_command_id_fk": { + "name": "environment_package_artifact_backup_staging_command_id_api_command_id_fk", + "tableFrom": "environment_package_artifact_backup_staging", + "tableTo": "api_command", + "columnsFrom": ["command_id"], + "columnsTo": ["id"], + "onDelete": "restrict", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": { + "environment_package_artifact_backup_staging_attempt_check": { + "name": "environment_package_artifact_backup_staging_attempt_check", + "value": "typeof(\"environment_package_artifact_backup_staging\".\"attempt_count\") = 'integer' AND \"environment_package_artifact_backup_staging\".\"attempt_count\" BETWEEN 1 AND 9007199254740991" + }, + "environment_package_artifact_backup_staging_claim_owner_check": { + "name": "environment_package_artifact_backup_staging_claim_owner_check", + "value": "typeof(\"environment_package_artifact_backup_staging\".\"claim_owner\") = 'text' AND length(\"environment_package_artifact_backup_staging\".\"claim_owner\") > 0" + }, + "environment_package_artifact_backup_staging_delivery_check": { + "name": "environment_package_artifact_backup_staging_delivery_check", + "value": "typeof(\"environment_package_artifact_backup_staging\".\"delivery_generation\") = 'integer' AND \"environment_package_artifact_backup_staging\".\"delivery_generation\" BETWEEN 1 AND 9007199254740991" + }, + "environment_package_artifact_backup_staging_digest_check": { + "name": "environment_package_artifact_backup_staging_digest_check", + "value": "length(\"environment_package_artifact_backup_staging\".\"input_digest\") = 64 AND \"environment_package_artifact_backup_staging\".\"input_digest\" = lower(\"environment_package_artifact_backup_staging\".\"input_digest\") AND \"environment_package_artifact_backup_staging\".\"input_digest\" NOT GLOB '*[^0-9a-f]*'" + }, + "environment_package_artifact_backup_staging_dir_check": { + "name": "environment_package_artifact_backup_staging_dir_check", + "value": "typeof(\"environment_package_artifact_backup_staging\".\"dir\") = 'text' AND length(\"environment_package_artifact_backup_staging\".\"dir\") > 0" + }, + "environment_package_artifact_backup_staging_paths_check": { + "name": "environment_package_artifact_backup_staging_paths_check", + "value": "json_valid(\"environment_package_artifact_backup_staging\".\"paths_json\") = 1 AND json_type(\"environment_package_artifact_backup_staging\".\"paths_json\") = 'object' AND json_type(\"environment_package_artifact_backup_staging\".\"paths_json\", '$.executable') = 'array' AND json_type(\"environment_package_artifact_backup_staging\".\"paths_json\", '$.node') = 'array' AND json_type(\"environment_package_artifact_backup_staging\".\"paths_json\", '$.python') = 'array'" + }, + "environment_package_artifact_backup_staging_time_check": { + "name": "environment_package_artifact_backup_staging_time_check", + "value": "typeof(\"environment_package_artifact_backup_staging\".\"created_at\") = 'integer' AND \"environment_package_artifact_backup_staging\".\"created_at\" BETWEEN 0 AND 9007199254740991 AND typeof(\"environment_package_artifact_backup_staging\".\"updated_at\") = 'integer' AND \"environment_package_artifact_backup_staging\".\"updated_at\" BETWEEN \"environment_package_artifact_backup_staging\".\"created_at\" AND 9007199254740991" + } + } + }, + "environment_revision": { + "name": "environment_revision", + "columns": { + "allow_mcp_servers": { + "name": "allow_mcp_servers", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "allow_package_managers": { + "name": "allow_package_managers", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "allowed_hosts_json": { + "name": "allowed_hosts_json", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_by_account_id": { + "name": "created_by_account_id", + "type": "text CHECK (\"created_by_account_id\" = upper(\"created_by_account_id\") AND length(\"created_by_account_id\") = 26 AND substr(\"created_by_account_id\", 1, 1) GLOB '[0-7]' AND \"created_by_account_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "env_vars_json": { + "name": "env_vars_json", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "environment_id": { + "name": "environment_id", + "type": "text CHECK (\"environment_id\" = upper(\"environment_id\") AND length(\"environment_id\") = 26 AND substr(\"environment_id\", 1, 1) GLOB '[0-7]' AND \"environment_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "id": { + "name": "id", + "type": "text CHECK (\"id\" = upper(\"id\") AND length(\"id\") = 26 AND substr(\"id\", 1, 1) GLOB '[0-7]' AND \"id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "network_policy": { + "name": "network_policy", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "packages_json": { + "name": "packages_json", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "app_id": { + "name": "app_id", + "type": "text CHECK (\"app_id\" = upper(\"app_id\") AND length(\"app_id\") = 26 AND substr(\"app_id\", 1, 1) GLOB '[0-7]' AND \"app_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "setup_script": { + "name": "setup_script", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "environment_revision_environment_created_at_idx": { + "name": "environment_revision_environment_created_at_idx", + "columns": ["environment_id", "created_at"], + "isUnique": false + }, + "environment_revision_app_created_at_idx": { + "name": "environment_revision_app_created_at_idx", + "columns": ["app_id", "created_at"], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": { + "environment_revision_network_policy_check": { + "name": "environment_revision_network_policy_check", + "value": "\"environment_revision\".\"network_policy\" IN ('full', 'limited')" + } + } + }, + "environment": { + "name": "environment", + "columns": { + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "current_revision_id": { + "name": "current_revision_id", + "type": "text CHECK (\"current_revision_id\" = upper(\"current_revision_id\") AND length(\"current_revision_id\") = 26 AND substr(\"current_revision_id\", 1, 1) GLOB '[0-7]' AND \"current_revision_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "forked_from_environment_id": { + "name": "forked_from_environment_id", + "type": "text CHECK (\"forked_from_environment_id\" = upper(\"forked_from_environment_id\") AND length(\"forked_from_environment_id\") = 26 AND substr(\"forked_from_environment_id\", 1, 1) GLOB '[0-7]' AND \"forked_from_environment_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "forked_from_environment_name": { + "name": "forked_from_environment_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "forked_from_owner_name": { + "name": "forked_from_owner_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "id": { + "name": "id", + "type": "text CHECK (\"id\" = upper(\"id\") AND length(\"id\") = 26 AND substr(\"id\", 1, 1) GLOB '[0-7]' AND \"id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "owner_account_id": { + "name": "owner_account_id", + "type": "text CHECK (\"owner_account_id\" = upper(\"owner_account_id\") AND length(\"owner_account_id\") = 26 AND substr(\"owner_account_id\", 1, 1) GLOB '[0-7]' AND \"owner_account_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "app_id": { + "name": "app_id", + "type": "text CHECK (\"app_id\" = upper(\"app_id\") AND length(\"app_id\") = 26 AND substr(\"app_id\", 1, 1) GLOB '[0-7]' AND \"app_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "environment_app_updated_at_idx": { + "name": "environment_app_updated_at_idx", + "columns": ["app_id", "updated_at"], + "isUnique": false + }, + "environment_owner_updated_at_idx": { + "name": "environment_owner_updated_at_idx", + "columns": ["owner_account_id", "updated_at"], + "isUnique": false + }, + "environment_owner_name_idx": { + "name": "environment_owner_name_idx", + "columns": ["app_id", "owner_account_id", "name"], + "isUnique": true, + "where": "\"environment\".\"owner_account_id\" IS NOT NULL" + }, + "environment_system_default_idx": { + "name": "environment_system_default_idx", + "columns": ["app_id"], + "isUnique": true, + "where": "\"environment\".\"owner_account_id\" IS NULL" + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "file_record": { + "name": "file_record", + "columns": { + "committed": { + "name": "committed", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_by_account_id": { + "name": "created_by_account_id", + "type": "text CHECK (\"created_by_account_id\" = upper(\"created_by_account_id\") AND length(\"created_by_account_id\") = 26 AND substr(\"created_by_account_id\", 1, 1) GLOB '[0-7]' AND \"created_by_account_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "etag": { + "name": "etag", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "id": { + "name": "id", + "type": "text CHECK (\"id\" = upper(\"id\") AND length(\"id\") = 26 AND substr(\"id\", 1, 1) GLOB '[0-7]' AND \"id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "mime_type": { + "name": "mime_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "object_key": { + "name": "object_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "owner_id": { + "name": "owner_id", + "type": "text CHECK (\"owner_id\" = upper(\"owner_id\") AND length(\"owner_id\") = 26 AND substr(\"owner_id\", 1, 1) GLOB '[0-7]' AND \"owner_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "owner_kind": { + "name": "owner_kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "parent_path": { + "name": "parent_path", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "path": { + "name": "path", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "purpose": { + "name": "purpose", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "runtime_event_seq": { + "name": "runtime_event_seq", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "scope_id": { + "name": "scope_id", + "type": "text CHECK (\"scope_id\" = upper(\"scope_id\") AND length(\"scope_id\") = 26 AND substr(\"scope_id\", 1, 1) GLOB '[0-7]' AND \"scope_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "scope_kind": { + "name": "scope_kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "session_kind": { + "name": "session_kind", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "size": { + "name": "size", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "version": { + "name": "version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "file_record_runtime_event_seq_idx": { + "name": "file_record_runtime_event_seq_idx", + "columns": ["scope_id", "runtime_event_seq"], + "isUnique": false + }, + "file_record_object_key_idx": { + "name": "file_record_object_key_idx", + "columns": ["object_key"], + "isUnique": true + }, + "file_record_unscoped_parent_path_name_status_idx": { + "name": "file_record_unscoped_parent_path_name_status_idx", + "columns": ["scope_kind", "parent_path", "name", "status"], + "isUnique": true, + "where": "\"file_record\".\"scope_id\" IS NULL" + }, + "file_record_scoped_parent_path_name_status_idx": { + "name": "file_record_scoped_parent_path_name_status_idx", + "columns": ["scope_kind", "scope_id", "parent_path", "name", "status"], + "isUnique": true + }, + "file_record_unscoped_pending_path_idx": { + "name": "file_record_unscoped_pending_path_idx", + "columns": ["scope_kind", "path"], + "isUnique": true, + "where": "\"file_record\".\"status\" = 'pending' AND \"file_record\".\"scope_id\" IS NULL" + }, + "file_record_scoped_pending_path_idx": { + "name": "file_record_scoped_pending_path_idx", + "columns": ["scope_kind", "scope_id", "path"], + "isUnique": true, + "where": "\"file_record\".\"status\" = 'pending' AND \"file_record\".\"scope_id\" IS NOT NULL" + }, + "file_record_unscoped_ready_path_idx": { + "name": "file_record_unscoped_ready_path_idx", + "columns": ["scope_kind", "path"], + "isUnique": true, + "where": "\"file_record\".\"status\" = 'ready' AND \"file_record\".\"scope_id\" IS NULL" + }, + "file_record_scoped_ready_path_idx": { + "name": "file_record_scoped_ready_path_idx", + "columns": ["scope_kind", "scope_id", "path"], + "isUnique": true, + "where": "\"file_record\".\"status\" = 'ready' AND \"file_record\".\"scope_id\" IS NOT NULL" + }, + "file_record_governance_idx": { + "name": "file_record_governance_idx", + "columns": ["purpose", "owner_kind", "owner_id", "status", "expires_at"], + "isUnique": false + }, + "file_record_listing_idx": { + "name": "file_record_listing_idx", + "columns": ["scope_kind", "scope_id", "parent_path", "status", "lower(\"name\")"], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": { + "file_record_runtime_event_seq_check": { + "name": "file_record_runtime_event_seq_check", + "value": "\"file_record\".\"runtime_event_seq\" IS NULL OR \"file_record\".\"runtime_event_seq\" >= 0" + } + } + }, + "file_upload": { + "name": "file_upload", + "columns": { + "content_type": { + "name": "content_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_by_account_id": { + "name": "created_by_account_id", + "type": "text CHECK (\"created_by_account_id\" = upper(\"created_by_account_id\") AND length(\"created_by_account_id\") = 26 AND substr(\"created_by_account_id\", 1, 1) GLOB '[0-7]' AND \"created_by_account_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expected_size": { + "name": "expected_size", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "file_id": { + "name": "file_id", + "type": "text CHECK (\"file_id\" = upper(\"file_id\") AND length(\"file_id\") = 26 AND substr(\"file_id\", 1, 1) GLOB '[0-7]' AND \"file_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "id": { + "name": "id", + "type": "text CHECK (\"id\" = upper(\"id\") AND length(\"id\") = 26 AND substr(\"id\", 1, 1) GLOB '[0-7]' AND \"id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "if_match_etag": { + "name": "if_match_etag", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "multipart_upload_id": { + "name": "multipart_upload_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "overwrite": { + "name": "overwrite", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "part_size": { + "name": "part_size", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "scope_id": { + "name": "scope_id", + "type": "text CHECK (\"scope_id\" = upper(\"scope_id\") AND length(\"scope_id\") = 26 AND substr(\"scope_id\", 1, 1) GLOB '[0-7]' AND \"scope_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "scope_kind": { + "name": "scope_kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "strategy": { + "name": "strategy", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "file_upload_file_id_idx": { + "name": "file_upload_file_id_idx", + "columns": ["file_id"], + "isUnique": true + }, + "file_upload_status_expires_idx": { + "name": "file_upload_status_expires_idx", + "columns": ["status", "expires_at"], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "file_version": { + "name": "file_version", + "columns": { + "committed": { + "name": "committed", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "committed_at": { + "name": "committed_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_by_account_id": { + "name": "created_by_account_id", + "type": "text CHECK (\"created_by_account_id\" = upper(\"created_by_account_id\") AND length(\"created_by_account_id\") = 26 AND substr(\"created_by_account_id\", 1, 1) GLOB '[0-7]' AND \"created_by_account_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "file_id": { + "name": "file_id", + "type": "text CHECK (\"file_id\" = upper(\"file_id\") AND length(\"file_id\") = 26 AND substr(\"file_id\", 1, 1) GLOB '[0-7]' AND \"file_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "id": { + "name": "id", + "type": "text CHECK (\"id\" = upper(\"id\") AND length(\"id\") = 26 AND substr(\"id\", 1, 1) GLOB '[0-7]' AND \"id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "mime_type": { + "name": "mime_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "object_key": { + "name": "object_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "path": { + "name": "path", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "reason": { + "name": "reason", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "scope_id": { + "name": "scope_id", + "type": "text CHECK (\"scope_id\" = upper(\"scope_id\") AND length(\"scope_id\") = 26 AND substr(\"scope_id\", 1, 1) GLOB '[0-7]' AND \"scope_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "scope_kind": { + "name": "scope_kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "size": { + "name": "size", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "source_etag": { + "name": "source_etag", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "source_object_key": { + "name": "source_object_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "version": { + "name": "version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "file_version_object_key_idx": { + "name": "file_version_object_key_idx", + "columns": ["object_key"], + "isUnique": true + }, + "file_version_scope_path_created_idx": { + "name": "file_version_scope_path_created_idx", + "columns": ["scope_kind", "scope_id", "path", "created_at"], + "isUnique": false + }, + "file_version_file_created_idx": { + "name": "file_version_file_created_idx", + "columns": ["file_id", "created_at"], + "isUnique": false + }, + "file_version_pending_idx": { + "name": "file_version_pending_idx", + "columns": ["committed", "created_at"], + "isUnique": false, + "where": "\"file_version\".\"committed\" = 0" + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "runtime_artifact_attempt": { + "name": "runtime_artifact_attempt", + "columns": { + "accepted_event_id": { + "name": "accepted_event_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_by_account_id": { + "name": "created_by_account_id", + "type": "text CHECK (\"created_by_account_id\" = upper(\"created_by_account_id\") AND length(\"created_by_account_id\") = 26 AND substr(\"created_by_account_id\", 1, 1) GLOB '[0-7]' AND \"created_by_account_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "delete_after": { + "name": "delete_after", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "driver_connection_id": { + "name": "driver_connection_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "driver_generation": { + "name": "driver_generation", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "driver_instance_id": { + "name": "driver_instance_id", + "type": "text CHECK (\"driver_instance_id\" = upper(\"driver_instance_id\") AND length(\"driver_instance_id\") = 26 AND substr(\"driver_instance_id\", 1, 1) GLOB '[0-7]' AND \"driver_instance_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "event_type": { + "name": "event_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "manifest_json": { + "name": "manifest_json", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "manifest_sha256": { + "name": "manifest_sha256", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "owned_object_keys_json": { + "name": "owned_object_keys_json", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'[]'" + }, + "run_id": { + "name": "run_id", + "type": "text CHECK (\"run_id\" = upper(\"run_id\") AND length(\"run_id\") = 26 AND substr(\"run_id\", 1, 1) GLOB '[0-7]' AND \"run_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "semantic_hash": { + "name": "semantic_hash", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "session_id": { + "name": "session_id", + "type": "text CHECK (\"session_id\" = upper(\"session_id\") AND length(\"session_id\") = 26 AND substr(\"session_id\", 1, 1) GLOB '[0-7]' AND \"session_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "source_event_id": { + "name": "source_event_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "runtime_artifact_attempt_accepted_event_idx": { + "name": "runtime_artifact_attempt_accepted_event_idx", + "columns": ["accepted_event_id"], + "isUnique": true, + "where": "\"runtime_artifact_attempt\".\"accepted_event_id\" IS NOT NULL" + }, + "runtime_artifact_attempt_cleanup_idx": { + "name": "runtime_artifact_attempt_cleanup_idx", + "columns": ["status", "expires_at", "updated_at", "id"], + "isUnique": false + }, + "runtime_artifact_attempt_session_status_idx": { + "name": "runtime_artifact_attempt_session_status_idx", + "columns": ["session_id", "status", "id"], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": { + "runtime_artifact_attempt_manifest_check": { + "name": "runtime_artifact_attempt_manifest_check", + "value": "(\"runtime_artifact_attempt\".\"manifest_json\" IS NULL AND \"runtime_artifact_attempt\".\"manifest_sha256\" IS NULL) OR (\"runtime_artifact_attempt\".\"manifest_json\" IS NOT NULL AND json_valid(\"runtime_artifact_attempt\".\"manifest_json\") = 1 AND json_extract(\"runtime_artifact_attempt\".\"manifest_json\", '$.version') IS 1 AND json_type(\"runtime_artifact_attempt\".\"manifest_json\", '$.captureStatus') IS 'text' AND json_extract(\"runtime_artifact_attempt\".\"manifest_json\", '$.captureStatus') IN ('complete', 'omitted_file_limit', 'omitted_runtime_unavailable', 'omitted_size_limit', 'omitted_source_changed', 'omitted_source_missing') AND json_type(\"runtime_artifact_attempt\".\"manifest_json\", '$.mode') IS 'text' AND json_extract(\"runtime_artifact_attempt\".\"manifest_json\", '$.mode') IN ('delta', 'snapshot') AND (json_extract(\"runtime_artifact_attempt\".\"manifest_json\", '$.captureStatus') = 'complete' OR json_array_length(\"runtime_artifact_attempt\".\"manifest_json\", '$.files') = 0) AND json_extract(\"runtime_artifact_attempt\".\"manifest_json\", '$.sourceEventId') IS \"runtime_artifact_attempt\".\"source_event_id\" AND json_extract(\"runtime_artifact_attempt\".\"manifest_json\", '$.semanticHash') IS \"runtime_artifact_attempt\".\"semantic_hash\" AND json_type(\"runtime_artifact_attempt\".\"manifest_json\", '$.files') IS 'array' AND \"runtime_artifact_attempt\".\"manifest_sha256\" IS NOT NULL AND length(\"runtime_artifact_attempt\".\"manifest_sha256\") = 64 AND \"runtime_artifact_attempt\".\"manifest_sha256\" = lower(\"runtime_artifact_attempt\".\"manifest_sha256\") AND \"runtime_artifact_attempt\".\"manifest_sha256\" NOT GLOB '*[^0-9a-f]*')" + }, + "runtime_artifact_attempt_owned_keys_check": { + "name": "runtime_artifact_attempt_owned_keys_check", + "value": "json_valid(\"runtime_artifact_attempt\".\"owned_object_keys_json\") = 1 AND json_type(\"runtime_artifact_attempt\".\"owned_object_keys_json\") IS 'array'" + }, + "runtime_artifact_attempt_semantic_hash_check": { + "name": "runtime_artifact_attempt_semantic_hash_check", + "value": "length(\"runtime_artifact_attempt\".\"semantic_hash\") = 64 AND \"runtime_artifact_attempt\".\"semantic_hash\" = lower(\"runtime_artifact_attempt\".\"semantic_hash\") AND \"runtime_artifact_attempt\".\"semantic_hash\" NOT GLOB '*[^0-9a-f]*'" + }, + "runtime_artifact_attempt_status_check": { + "name": "runtime_artifact_attempt_status_check", + "value": "(\"runtime_artifact_attempt\".\"status\" = 'staging' AND \"runtime_artifact_attempt\".\"manifest_json\" IS NULL AND \"runtime_artifact_attempt\".\"accepted_event_id\" IS NULL AND \"runtime_artifact_attempt\".\"expires_at\" IS NOT NULL AND \"runtime_artifact_attempt\".\"delete_after\" IS NULL) OR (\"runtime_artifact_attempt\".\"status\" = 'staged' AND \"runtime_artifact_attempt\".\"manifest_json\" IS NOT NULL AND \"runtime_artifact_attempt\".\"accepted_event_id\" IS NULL AND \"runtime_artifact_attempt\".\"expires_at\" IS NOT NULL AND \"runtime_artifact_attempt\".\"delete_after\" IS NULL) OR (\"runtime_artifact_attempt\".\"status\" = 'accepted' AND \"runtime_artifact_attempt\".\"manifest_json\" IS NOT NULL AND \"runtime_artifact_attempt\".\"accepted_event_id\" IS NOT NULL AND \"runtime_artifact_attempt\".\"expires_at\" IS NULL AND \"runtime_artifact_attempt\".\"delete_after\" IS NULL AND json_array_length(\"runtime_artifact_attempt\".\"owned_object_keys_json\") = 0) OR (\"runtime_artifact_attempt\".\"status\" = 'deleting' AND \"runtime_artifact_attempt\".\"accepted_event_id\" IS NULL AND \"runtime_artifact_attempt\".\"delete_after\" IS NOT NULL)" + }, + "runtime_artifact_attempt_time_check": { + "name": "runtime_artifact_attempt_time_check", + "value": "\"runtime_artifact_attempt\".\"driver_generation\" >= 0 AND (\"runtime_artifact_attempt\".\"expires_at\" IS NULL OR \"runtime_artifact_attempt\".\"expires_at\" >= \"runtime_artifact_attempt\".\"created_at\") AND (\"runtime_artifact_attempt\".\"delete_after\" IS NULL OR \"runtime_artifact_attempt\".\"delete_after\" >= \"runtime_artifact_attempt\".\"created_at\") AND \"runtime_artifact_attempt\".\"updated_at\" >= \"runtime_artifact_attempt\".\"created_at\"" + } + } + }, + "session_artifact_head": { + "name": "session_artifact_head", + "columns": { + "file_id": { + "name": "file_id", + "type": "text CHECK (\"file_id\" = upper(\"file_id\") AND length(\"file_id\") = 26 AND substr(\"file_id\", 1, 1) GLOB '[0-7]' AND \"file_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "runtime_event_seq": { + "name": "runtime_event_seq", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "session_id": { + "name": "session_id", + "type": "text CHECK (\"session_id\" = upper(\"session_id\") AND length(\"session_id\") = 26 AND substr(\"session_id\", 1, 1) GLOB '[0-7]' AND \"session_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "source_event_id": { + "name": "source_event_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "source_path": { + "name": "source_path", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "session_artifact_head_session_path_idx": { + "name": "session_artifact_head_session_path_idx", + "columns": ["session_id", "source_path"], + "isUnique": true + }, + "session_artifact_head_session_seq_idx": { + "name": "session_artifact_head_session_seq_idx", + "columns": ["session_id", "runtime_event_seq", "source_path"], + "isUnique": false + } + }, + "foreignKeys": { + "session_artifact_head_session_id_session_id_fk": { + "name": "session_artifact_head_session_id_session_id_fk", + "tableFrom": "session_artifact_head", + "tableTo": "session", + "columnsFrom": ["session_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": { + "session_artifact_head_path_check": { + "name": "session_artifact_head_path_check", + "value": "length(\"session_artifact_head\".\"source_path\") > 8 AND substr(\"session_artifact_head\".\"source_path\", 1, 8) = 'outputs/' AND instr(\"session_artifact_head\".\"source_path\", char(0)) = 0 AND instr(\"session_artifact_head\".\"source_path\", '\\') = 0 AND \"session_artifact_head\".\"source_path\" NOT LIKE '%//%' AND \"session_artifact_head\".\"source_path\" NOT LIKE '%/./%' AND \"session_artifact_head\".\"source_path\" NOT LIKE '%/.' AND \"session_artifact_head\".\"source_path\" NOT LIKE '%/../%' AND \"session_artifact_head\".\"source_path\" NOT LIKE '%/..'" + }, + "session_artifact_head_seq_check": { + "name": "session_artifact_head_seq_check", + "value": "\"session_artifact_head\".\"runtime_event_seq\" >= 0 AND \"session_artifact_head\".\"updated_at\" >= 0" + } + } + }, + "mcp_credential": { + "name": "mcp_credential", + "columns": { + "account_id": { + "name": "account_id", + "type": "text CHECK (\"account_id\" = upper(\"account_id\") AND length(\"account_id\") = 26 AND substr(\"account_id\", 1, 1) GLOB '[0-7]' AND \"account_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "agent_id": { + "name": "agent_id", + "type": "text CHECK (\"agent_id\" = upper(\"agent_id\") AND length(\"agent_id\") = 26 AND substr(\"agent_id\", 1, 1) GLOB '[0-7]' AND \"agent_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "auth_type": { + "name": "auth_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "id": { + "name": "id", + "type": "text CHECK (\"id\" = upper(\"id\") AND length(\"id\") = 26 AND substr(\"id\", 1, 1) GLOB '[0-7]' AND \"id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "last_refreshed_at": { + "name": "last_refreshed_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "oauth_client_id": { + "name": "oauth_client_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "oauth_client_secret_secret_id": { + "name": "oauth_client_secret_secret_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "app_id": { + "name": "app_id", + "type": "text CHECK (\"app_id\" = upper(\"app_id\") AND length(\"app_id\") = 26 AND substr(\"app_id\", 1, 1) GLOB '[0-7]' AND \"app_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "refresh_secret_id": { + "name": "refresh_secret_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "scope_values_json": { + "name": "scope_values_json", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "secret_id": { + "name": "secret_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "server_id": { + "name": "server_id", + "type": "text CHECK (\"server_id\" = upper(\"server_id\") AND length(\"server_id\") = 26 AND substr(\"server_id\", 1, 1) GLOB '[0-7]' AND \"server_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "subject_label": { + "name": "subject_label", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "mcp_credential_server_scope_status_idx": { + "name": "mcp_credential_server_scope_status_idx", + "columns": ["server_id", "scope", "status"], + "isUnique": false + }, + "mcp_credential_app_scope_status_idx": { + "name": "mcp_credential_app_scope_status_idx", + "columns": ["app_id", "scope", "status"], + "isUnique": false + }, + "mcp_credential_app_scope_idx": { + "name": "mcp_credential_app_scope_idx", + "columns": ["server_id", "scope"], + "isUnique": true, + "where": "\"mcp_credential\".\"scope\" = 'app'" + }, + "mcp_credential_agent_scope_idx": { + "name": "mcp_credential_agent_scope_idx", + "columns": ["server_id", "agent_id", "scope"], + "isUnique": true, + "where": "\"mcp_credential\".\"scope\" = 'agent' AND \"mcp_credential\".\"agent_id\" IS NOT NULL" + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": { + "mcp_credential_scope_shape_check": { + "name": "mcp_credential_scope_shape_check", + "value": "\n (\"mcp_credential\".\"scope\" = 'app' AND \"mcp_credential\".\"account_id\" IS NULL AND \"mcp_credential\".\"agent_id\" IS NULL)\n OR (\"mcp_credential\".\"scope\" = 'agent' AND \"mcp_credential\".\"account_id\" IS NULL AND \"mcp_credential\".\"agent_id\" IS NOT NULL)\n " + }, + "mcp_credential_scope_values_json_check": { + "name": "mcp_credential_scope_values_json_check", + "value": "\n \"mcp_credential\".\"scope_values_json\" IS NULL\n OR (json_valid(\"mcp_credential\".\"scope_values_json\") AND json_type(\"mcp_credential\".\"scope_values_json\") = 'array')\n " + }, + "mcp_credential_bearer_shape_check": { + "name": "mcp_credential_bearer_shape_check", + "value": "\n \"mcp_credential\".\"auth_type\" != 'bearer'\n OR (\n \"mcp_credential\".\"oauth_client_id\" IS NULL\n AND \"mcp_credential\".\"oauth_client_secret_secret_id\" IS NULL\n AND \"mcp_credential\".\"refresh_secret_id\" IS NULL\n )\n " + } + } + }, + "mcp_oauth_flow": { + "name": "mcp_oauth_flow", + "columns": { + "authorization_endpoint": { + "name": "authorization_endpoint", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "cleanup_after": { + "name": "cleanup_after", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "code_verifier": { + "name": "code_verifier", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "completed_at": { + "name": "completed_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "id": { + "name": "id", + "type": "text CHECK (\"id\" = upper(\"id\") AND length(\"id\") = 26 AND substr(\"id\", 1, 1) GLOB '[0-7]' AND \"id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "initiator_account_id": { + "name": "initiator_account_id", + "type": "text CHECK (\"initiator_account_id\" = upper(\"initiator_account_id\") AND length(\"initiator_account_id\") = 26 AND substr(\"initiator_account_id\", 1, 1) GLOB '[0-7]' AND \"initiator_account_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "oauth_client_id": { + "name": "oauth_client_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "oauth_client_secret_secret_id": { + "name": "oauth_client_secret_secret_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "app_id": { + "name": "app_id", + "type": "text CHECK (\"app_id\" = upper(\"app_id\") AND length(\"app_id\") = 26 AND substr(\"app_id\", 1, 1) GLOB '[0-7]' AND \"app_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "registration_endpoint": { + "name": "registration_endpoint", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "return_url": { + "name": "return_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "scope_values_json": { + "name": "scope_values_json", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "server_id": { + "name": "server_id", + "type": "text CHECK (\"server_id\" = upper(\"server_id\") AND length(\"server_id\") = 26 AND substr(\"server_id\", 1, 1) GLOB '[0-7]' AND \"server_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "subject_label": { + "name": "subject_label", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "token_endpoint": { + "name": "token_endpoint", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "mcp_oauth_flow_status_cleanup_after_idx": { + "name": "mcp_oauth_flow_status_cleanup_after_idx", + "columns": ["status", "cleanup_after"], + "isUnique": false + }, + "mcp_oauth_flow_expires_at_idx": { + "name": "mcp_oauth_flow_expires_at_idx", + "columns": ["expires_at"], + "isUnique": false + }, + "mcp_oauth_flow_server_account_idx": { + "name": "mcp_oauth_flow_server_account_idx", + "columns": ["server_id", "initiator_account_id"], + "isUnique": false + }, + "mcp_oauth_flow_app_server_account_idx": { + "name": "mcp_oauth_flow_app_server_account_idx", + "columns": ["app_id", "server_id", "initiator_account_id"], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": { + "mcp_oauth_flow_scope_values_json_check": { + "name": "mcp_oauth_flow_scope_values_json_check", + "value": "\n \"mcp_oauth_flow\".\"scope_values_json\" IS NULL\n OR (json_valid(\"mcp_oauth_flow\".\"scope_values_json\") AND json_type(\"mcp_oauth_flow\".\"scope_values_json\") = 'array')\n " + } + } + }, + "mcp_server": { + "name": "mcp_server", + "columns": { + "auth_type": { + "name": "auth_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "byo_client_id": { + "name": "byo_client_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "byo_client_secret_secret_id": { + "name": "byo_client_secret_secret_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "credential_scope": { + "name": "credential_scope", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "enabled": { + "name": "enabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "icon_url": { + "name": "icon_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "id": { + "name": "id", + "type": "text CHECK (\"id\" = upper(\"id\") AND length(\"id\") = 26 AND substr(\"id\", 1, 1) GLOB '[0-7]' AND \"id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "oauth_metadata_json": { + "name": "oauth_metadata_json", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "owner_account_id": { + "name": "owner_account_id", + "type": "text CHECK (\"owner_account_id\" = upper(\"owner_account_id\") AND length(\"owner_account_id\") = 26 AND substr(\"owner_account_id\", 1, 1) GLOB '[0-7]' AND \"owner_account_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "app_id": { + "name": "app_id", + "type": "text CHECK (\"app_id\" = upper(\"app_id\") AND length(\"app_id\") = 26 AND substr(\"app_id\", 1, 1) GLOB '[0-7]' AND \"app_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "mcp_server_app_enabled_idx": { + "name": "mcp_server_app_enabled_idx", + "columns": ["app_id", "enabled"], + "isUnique": false + }, + "mcp_server_owner_app_idx": { + "name": "mcp_server_owner_app_idx", + "columns": ["owner_account_id", "app_id"], + "isUnique": false + }, + "mcp_server_app_url_idx": { + "name": "mcp_server_app_url_idx", + "columns": ["app_id", "url"], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": { + "mcp_server_source_scope_check": { + "name": "mcp_server_source_scope_check", + "value": "\"mcp_server\".\"source\" = 'app' AND \"mcp_server\".\"credential_scope\" = 'app'" + } + } + }, + "vault_secret": { + "name": "vault_secret", + "columns": { + "algorithm": { + "name": "algorithm", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'AES-GCM'" + }, + "ciphertext": { + "name": "ciphertext", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "ciphertext_iv": { + "name": "ciphertext_iv", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "id": { + "name": "id", + "type": "text CHECK (\"id\" = upper(\"id\") AND length(\"id\") = 26 AND substr(\"id\", 1, 1) GLOB '[0-7]' AND \"id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "wrapped_dek": { + "name": "wrapped_dek", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "wrapped_dek_iv": { + "name": "wrapped_dek_iv", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "vault_secret_kind_created_at_idx": { + "name": "vault_secret_kind_created_at_idx", + "columns": ["kind", "created_at"], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "organization": { + "name": "organization", + "columns": { + "avatar_url": { + "name": "avatar_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "creator_account_id": { + "name": "creator_account_id", + "type": "text CHECK (\"creator_account_id\" = upper(\"creator_account_id\") AND length(\"creator_account_id\") = 26 AND substr(\"creator_account_id\", 1, 1) GLOB '[0-7]' AND \"creator_account_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "id": { + "name": "id", + "type": "text CHECK (\"id\" = upper(\"id\") AND length(\"id\") = 26 AND substr(\"id\", 1, 1) GLOB '[0-7]' AND \"id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "organization_creator_account_idx": { + "name": "organization_creator_account_idx", + "columns": ["creator_account_id"], + "isUnique": true, + "where": "\"organization\".\"creator_account_id\" IS NOT NULL" + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "app_deployment_script": { + "name": "app_deployment_script", + "columns": { + "attempt_count": { + "name": "attempt_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "command_id": { + "name": "command_id", + "type": "text CHECK (\"command_id\" = upper(\"command_id\") AND length(\"command_id\") = 26 AND substr(\"command_id\", 1, 1) GLOB '[0-7]' AND \"command_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "delivery_generation": { + "name": "delivery_generation", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "deployment_id": { + "name": "deployment_id", + "type": "text CHECK (\"deployment_id\" = upper(\"deployment_id\") AND length(\"deployment_id\") = 26 AND substr(\"deployment_id\", 1, 1) GLOB '[0-7]' AND \"deployment_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "external_deleted_at": { + "name": "external_deleted_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_reconciled_at": { + "name": "last_reconciled_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "next_reconcile_at": { + "name": "next_reconcile_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "reconcile_count": { + "name": "reconcile_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "reconcile_expires_at": { + "name": "reconcile_expires_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "reconcile_owner": { + "name": "reconcile_owner", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "registered_at": { + "name": "registered_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "registered_claim_owner": { + "name": "registered_claim_owner", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "retire_after": { + "name": "retire_after", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "run_id": { + "name": "run_id", + "type": "text CHECK (\"run_id\" = upper(\"run_id\") AND length(\"run_id\") = 26 AND substr(\"run_id\", 1, 1) GLOB '[0-7]' AND \"run_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "script_name": { + "name": "script_name", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "upload_started_at": { + "name": "upload_started_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "app_deployment_script_reconcile_idx": { + "name": "app_deployment_script_reconcile_idx", + "columns": ["next_reconcile_at", "script_name"], + "isUnique": false + } + }, + "foreignKeys": { + "app_deployment_script_command_id_api_command_id_fk": { + "name": "app_deployment_script_command_id_api_command_id_fk", + "tableFrom": "app_deployment_script", + "tableTo": "api_command", + "columnsFrom": ["command_id"], + "columnsTo": ["id"], + "onDelete": "restrict", + "onUpdate": "no action" + }, + "app_deployment_script_deployment_id_app_deployment_id_fk": { + "name": "app_deployment_script_deployment_id_app_deployment_id_fk", + "tableFrom": "app_deployment_script", + "tableTo": "app_deployment", + "columnsFrom": ["deployment_id"], + "columnsTo": ["id"], + "onDelete": "restrict", + "onUpdate": "no action" + }, + "app_deployment_script_run_id_app_deployment_run_id_fk": { + "name": "app_deployment_script_run_id_app_deployment_run_id_fk", + "tableFrom": "app_deployment_script", + "tableTo": "app_deployment_run", + "columnsFrom": ["run_id"], + "columnsTo": ["id"], + "onDelete": "restrict", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": { + "app_deployment_script_attempt_check": { + "name": "app_deployment_script_attempt_check", + "value": "typeof(\"app_deployment_script\".\"attempt_count\") = 'integer' AND \"app_deployment_script\".\"attempt_count\" BETWEEN 1 AND 9007199254740991" + }, + "app_deployment_script_delivery_check": { + "name": "app_deployment_script_delivery_check", + "value": "typeof(\"app_deployment_script\".\"delivery_generation\") = 'integer' AND \"app_deployment_script\".\"delivery_generation\" BETWEEN 1 AND 9007199254740991" + }, + "app_deployment_script_name_check": { + "name": "app_deployment_script_name_check", + "value": "typeof(\"app_deployment_script\".\"script_name\") = 'text' AND length(\"app_deployment_script\".\"script_name\") BETWEEN 36 AND 63 AND substr(\"app_deployment_script\".\"script_name\", 1, 4) = 'app-' AND \"app_deployment_script\".\"script_name\" NOT GLOB '*[^0-9a-z-]*' AND substr(\"app_deployment_script\".\"script_name\", -1, 1) GLOB '[0-9a-z]'" + }, + "app_deployment_script_registered_owner_check": { + "name": "app_deployment_script_registered_owner_check", + "value": "typeof(\"app_deployment_script\".\"registered_claim_owner\") = 'text' AND length(\"app_deployment_script\".\"registered_claim_owner\") > 0" + }, + "app_deployment_script_reconcile_count_check": { + "name": "app_deployment_script_reconcile_count_check", + "value": "typeof(\"app_deployment_script\".\"reconcile_count\") = 'integer' AND \"app_deployment_script\".\"reconcile_count\" BETWEEN 0 AND 9007199254740991" + }, + "app_deployment_script_reconcile_lease_check": { + "name": "app_deployment_script_reconcile_lease_check", + "value": "(\"app_deployment_script\".\"reconcile_owner\" IS NULL AND \"app_deployment_script\".\"reconcile_expires_at\" IS NULL) OR (typeof(\"app_deployment_script\".\"reconcile_owner\") = 'text' AND length(\"app_deployment_script\".\"reconcile_owner\") > 0 AND typeof(\"app_deployment_script\".\"reconcile_expires_at\") = 'integer' AND \"app_deployment_script\".\"reconcile_expires_at\" BETWEEN 0 AND 9007199254740991)" + }, + "app_deployment_script_time_check": { + "name": "app_deployment_script_time_check", + "value": "typeof(\"app_deployment_script\".\"registered_at\") = 'integer' AND \"app_deployment_script\".\"registered_at\" BETWEEN 0 AND 9007199254740991 AND (\"app_deployment_script\".\"upload_started_at\" IS NULL OR (typeof(\"app_deployment_script\".\"upload_started_at\") = 'integer' AND \"app_deployment_script\".\"upload_started_at\" BETWEEN \"app_deployment_script\".\"registered_at\" AND 9007199254740991)) AND (\"app_deployment_script\".\"retire_after\" IS NULL OR (typeof(\"app_deployment_script\".\"retire_after\") = 'integer' AND \"app_deployment_script\".\"retire_after\" BETWEEN \"app_deployment_script\".\"registered_at\" AND 9007199254740991)) AND (\"app_deployment_script\".\"next_reconcile_at\" IS NULL OR (typeof(\"app_deployment_script\".\"next_reconcile_at\") = 'integer' AND \"app_deployment_script\".\"next_reconcile_at\" BETWEEN \"app_deployment_script\".\"registered_at\" AND 9007199254740991)) AND (\"app_deployment_script\".\"last_reconciled_at\" IS NULL OR (typeof(\"app_deployment_script\".\"last_reconciled_at\") = 'integer' AND \"app_deployment_script\".\"last_reconciled_at\" BETWEEN \"app_deployment_script\".\"registered_at\" AND 9007199254740991)) AND (\"app_deployment_script\".\"external_deleted_at\" IS NULL OR (\"app_deployment_script\".\"retire_after\" IS NOT NULL AND typeof(\"app_deployment_script\".\"external_deleted_at\") = 'integer' AND \"app_deployment_script\".\"external_deleted_at\" BETWEEN \"app_deployment_script\".\"registered_at\" AND 9007199254740991))" + } + } + }, + "app_deployment_run": { + "name": "app_deployment_run", + "columns": { + "app_id": { + "name": "app_id", + "type": "text CHECK (\"app_id\" = upper(\"app_id\") AND length(\"app_id\") = 26 AND substr(\"app_id\", 1, 1) GLOB '[0-7]' AND \"app_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "deployment_id": { + "name": "deployment_id", + "type": "text CHECK (\"deployment_id\" = upper(\"deployment_id\") AND length(\"deployment_id\") = 26 AND substr(\"deployment_id\", 1, 1) GLOB '[0-7]' AND \"deployment_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "error_code": { + "name": "error_code", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "external_deployment_id": { + "name": "external_deployment_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "external_project_id": { + "name": "external_project_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "external_version_id": { + "name": "external_version_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "generated_wrangler_config_json": { + "name": "generated_wrangler_config_json", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "id": { + "name": "id", + "type": "text CHECK (\"id\" = upper(\"id\") AND length(\"id\") = 26 AND substr(\"id\", 1, 1) GLOB '[0-7]' AND \"id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "mosoo_config_json": { + "name": "mosoo_config_json", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "plan_json": { + "name": "plan_json", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "source_branch": { + "name": "source_branch", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "source_commit_sha": { + "name": "source_commit_sha", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "target_kind": { + "name": "target_kind", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "target_project_name": { + "name": "target_project_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "target_script_name": { + "name": "target_script_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "app_deployment_run_app_id_idx": { + "name": "app_deployment_run_app_id_idx", + "columns": ["app_id", "id"], + "isUnique": false + }, + "app_deployment_run_deployment_id_idx": { + "name": "app_deployment_run_deployment_id_idx", + "columns": ["deployment_id", "id"], + "isUnique": false + }, + "app_deployment_run_active_app_idx": { + "name": "app_deployment_run_active_app_idx", + "columns": ["app_id"], + "isUnique": true, + "where": "\"app_deployment_run\".\"status\" IN ('queued', 'preparing', 'building', 'submitting', 'submitted', 'activating')" + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": { + "app_deployment_run_status_check": { + "name": "app_deployment_run_status_check", + "value": "\"app_deployment_run\".\"status\" IN ('queued', 'preparing', 'building', 'submitting', 'submitted', 'activating', 'success', 'failed')" + }, + "app_deployment_run_target_kind_check": { + "name": "app_deployment_run_target_kind_check", + "value": "\"app_deployment_run\".\"target_kind\" IS NULL OR \"app_deployment_run\".\"target_kind\" IN ('cloudflare_static_assets', 'cloudflare_worker')" + } + } + }, + "app_deployment_secret": { + "name": "app_deployment_secret", + "columns": { + "app_id": { + "name": "app_id", + "type": "text CHECK (\"app_id\" = upper(\"app_id\") AND length(\"app_id\") = 26 AND substr(\"app_id\", 1, 1) GLOB '[0-7]' AND \"app_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "vault_secret_id": { + "name": "vault_secret_id", + "type": "text CHECK (\"vault_secret_id\" = upper(\"vault_secret_id\") AND length(\"vault_secret_id\") = 26 AND substr(\"vault_secret_id\", 1, 1) GLOB '[0-7]' AND \"vault_secret_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "app_deployment_secret_app_name_idx": { + "name": "app_deployment_secret_app_name_idx", + "columns": ["app_id", "name"], + "isUnique": true + }, + "app_deployment_secret_vault_secret_idx": { + "name": "app_deployment_secret_vault_secret_idx", + "columns": ["vault_secret_id"], + "isUnique": true + } + }, + "foreignKeys": { + "app_deployment_secret_vault_secret_id_vault_secret_id_fk": { + "name": "app_deployment_secret_vault_secret_id_vault_secret_id_fk", + "tableFrom": "app_deployment_secret", + "tableTo": "vault_secret", + "columnsFrom": ["vault_secret_id"], + "columnsTo": ["id"], + "onDelete": "restrict", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "app_deployment": { + "name": "app_deployment", + "columns": { + "active_script_name": { + "name": "active_script_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "app_id": { + "name": "app_id", + "type": "text CHECK (\"app_id\" = upper(\"app_id\") AND length(\"app_id\") = 26 AND substr(\"app_id\", 1, 1) GLOB '[0-7]' AND \"app_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "default_branch": { + "name": "default_branch", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "deleted_at": { + "name": "deleted_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "id": { + "name": "id", + "type": "text CHECK (\"id\" = upper(\"id\") AND length(\"id\") = 26 AND substr(\"id\", 1, 1) GLOB '[0-7]' AND \"id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "last_successful_url": { + "name": "last_successful_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "latest_run_id": { + "name": "latest_run_id", + "type": "text CHECK (\"latest_run_id\" = upper(\"latest_run_id\") AND length(\"latest_run_id\") = 26 AND substr(\"latest_run_id\", 1, 1) GLOB '[0-7]' AND \"latest_run_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "mosoo_subdomain": { + "name": "mosoo_subdomain", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "owner_account_id": { + "name": "owner_account_id", + "type": "text CHECK (\"owner_account_id\" = upper(\"owner_account_id\") AND length(\"owner_account_id\") = 26 AND substr(\"owner_account_id\", 1, 1) GLOB '[0-7]' AND \"owner_account_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "repo_name": { + "name": "repo_name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "repo_owner": { + "name": "repo_owner", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "repo_url": { + "name": "repo_url", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "source_kind": { + "name": "source_kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "app_deployment_active_app_idx": { + "name": "app_deployment_active_app_idx", + "columns": ["app_id"], + "isUnique": true, + "where": "\"app_deployment\".\"deleted_at\" IS NULL" + }, + "app_deployment_active_subdomain_idx": { + "name": "app_deployment_active_subdomain_idx", + "columns": ["mosoo_subdomain"], + "isUnique": true, + "where": "\"app_deployment\".\"deleted_at\" IS NULL" + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": { + "app_deployment_traffic_authority_check": { + "name": "app_deployment_traffic_authority_check", + "value": "(\"app_deployment\".\"active_script_name\" IS NULL AND \"app_deployment\".\"last_successful_url\" IS NULL) OR (\"app_deployment\".\"deleted_at\" IS NULL AND typeof(\"app_deployment\".\"active_script_name\") = 'text' AND length(\"app_deployment\".\"active_script_name\") > 0 AND typeof(\"app_deployment\".\"last_successful_url\") = 'text' AND length(\"app_deployment\".\"last_successful_url\") > 0)" + }, + "app_deployment_source_kind_check": { + "name": "app_deployment_source_kind_check", + "value": "\"app_deployment\".\"source_kind\" IN ('github_public')" + } + } + }, + "app": { + "name": "app", + "columns": { + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "default_environment_id": { + "name": "default_environment_id", + "type": "text CHECK (\"default_environment_id\" = upper(\"default_environment_id\") AND length(\"default_environment_id\") = 26 AND substr(\"default_environment_id\", 1, 1) GLOB '[0-7]' AND \"default_environment_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "id": { + "name": "id", + "type": "text CHECK (\"id\" = upper(\"id\") AND length(\"id\") = 26 AND substr(\"id\", 1, 1) GLOB '[0-7]' AND \"id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "organization_id": { + "name": "organization_id", + "type": "text CHECK (\"organization_id\" = upper(\"organization_id\") AND length(\"organization_id\") = 26 AND substr(\"organization_id\", 1, 1) GLOB '[0-7]' AND \"organization_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "owner_account_id": { + "name": "owner_account_id", + "type": "text CHECK (\"owner_account_id\" = upper(\"owner_account_id\") AND length(\"owner_account_id\") = 26 AND substr(\"owner_account_id\", 1, 1) GLOB '[0-7]' AND \"owner_account_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "bound_agent_call_idempotency_key": { + "name": "bound_agent_call_idempotency_key", + "columns": { + "body_hash": { + "name": "body_hash", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "id": { + "name": "id", + "type": "text CHECK (\"id\" = upper(\"id\") AND length(\"id\") = 26 AND substr(\"id\", 1, 1) GLOB '[0-7]' AND \"id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "idempotency_key": { + "name": "idempotency_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "run_id": { + "name": "run_id", + "type": "text CHECK (\"run_id\" = upper(\"run_id\") AND length(\"run_id\") = 26 AND substr(\"run_id\", 1, 1) GLOB '[0-7]' AND \"run_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "session_id": { + "name": "session_id", + "type": "text CHECK (\"session_id\" = upper(\"session_id\") AND length(\"session_id\") = 26 AND substr(\"session_id\", 1, 1) GLOB '[0-7]' AND \"session_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "subject_hash": { + "name": "subject_hash", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "bound_agent_call_idempotency_subject_key_idx": { + "name": "bound_agent_call_idempotency_subject_key_idx", + "columns": ["subject_hash", "idempotency_key"], + "isUnique": true + }, + "bound_agent_call_idempotency_updated_idx": { + "name": "bound_agent_call_idempotency_updated_idx", + "columns": ["updated_at"], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "public_api_idempotency_key": { + "name": "public_api_idempotency_key", + "columns": { + "body_hash": { + "name": "body_hash", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "id": { + "name": "id", + "type": "text CHECK (\"id\" = upper(\"id\") AND length(\"id\") = 26 AND substr(\"id\", 1, 1) GLOB '[0-7]' AND \"id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "idempotency_key": { + "name": "idempotency_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "method": { + "name": "method", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "response_json": { + "name": "response_json", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "response_status": { + "name": "response_status", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "route": { + "name": "route", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "token_id": { + "name": "token_id", + "type": "text CHECK (\"token_id\" = upper(\"token_id\") AND length(\"token_id\") = 26 AND substr(\"token_id\", 1, 1) GLOB '[0-7]' AND \"token_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "public_api_idempotency_token_key_idx": { + "name": "public_api_idempotency_token_key_idx", + "columns": ["token_id", "idempotency_key"], + "isUnique": true + }, + "public_api_idempotency_updated_idx": { + "name": "public_api_idempotency_updated_idx", + "columns": ["updated_at"], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "public_api_rate_limit_window": { + "name": "public_api_rate_limit_window", + "columns": { + "bucket_key": { + "name": "bucket_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "request_count": { + "name": "request_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "shard": { + "name": "shard", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "window_start": { + "name": "window_start", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "public_api_rate_limit_window_updated_idx": { + "name": "public_api_rate_limit_window_updated_idx", + "columns": ["updated_at"], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "public_api_rate_limit_window_bucket_key_window_start_shard_pk": { + "columns": ["bucket_key", "window_start", "shard"], + "name": "public_api_rate_limit_window_bucket_key_window_start_shard_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "driver_command": { + "name": "driver_command", + "columns": { + "acked_at": { + "name": "acked_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "completed_at": { + "name": "completed_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "delivery_connection_id": { + "name": "delivery_connection_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "driver_generation": { + "name": "driver_generation", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "driver_instance_id": { + "name": "driver_instance_id", + "type": "text CHECK (\"driver_instance_id\" = upper(\"driver_instance_id\") AND length(\"driver_instance_id\") = 26 AND substr(\"driver_instance_id\", 1, 1) GLOB '[0-7]' AND \"driver_instance_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "error_json": { + "name": "error_json", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "id": { + "name": "id", + "type": "text CHECK (\"id\" = upper(\"id\") AND length(\"id\") = 26 AND substr(\"id\", 1, 1) GLOB '[0-7]' AND \"id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "issued_at": { + "name": "issued_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "payload_json": { + "name": "payload_json", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "result_json": { + "name": "result_json", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "seq": { + "name": "seq", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "driver_command_instance_seq_idx": { + "name": "driver_command_instance_seq_idx", + "columns": ["driver_instance_id", "seq"], + "isUnique": true + }, + "driver_command_instance_status_idx": { + "name": "driver_command_instance_status_idx", + "columns": ["driver_instance_id", "status", "expires_at"], + "isUnique": false + } + }, + "foreignKeys": { + "driver_command_driver_instance_id_driver_instance_id_fk": { + "name": "driver_command_driver_instance_id_driver_instance_id_fk", + "tableFrom": "driver_command", + "tableTo": "driver_instance", + "columnsFrom": ["driver_instance_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": { + "driver_command_generation_check": { + "name": "driver_command_generation_check", + "value": "\"driver_command\".\"driver_generation\" IS NULL OR (typeof(\"driver_command\".\"driver_generation\") = 'integer' AND \"driver_command\".\"driver_generation\" BETWEEN 0 AND 9007199254740991)" + }, + "driver_command_nonterminal_generation_check": { + "name": "driver_command_nonterminal_generation_check", + "value": "\"driver_command\".\"status\" IN ('completed', 'failed', 'expired', 'cancelled') OR \"driver_command\".\"driver_generation\" IS NOT NULL" + } + } + }, + "driver_instance_mcp_grant": { + "name": "driver_instance_mcp_grant", + "columns": { + "auth_type": { + "name": "auth_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "authorization_state": { + "name": "authorization_state", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "can_invalidate": { + "name": "can_invalidate", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "can_refresh": { + "name": "can_refresh", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "credential_id": { + "name": "credential_id", + "type": "text CHECK (\"credential_id\" = upper(\"credential_id\") AND length(\"credential_id\") = 26 AND substr(\"credential_id\", 1, 1) GLOB '[0-7]' AND \"credential_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "driver_instance_id": { + "name": "driver_instance_id", + "type": "text CHECK (\"driver_instance_id\" = upper(\"driver_instance_id\") AND length(\"driver_instance_id\") = 26 AND substr(\"driver_instance_id\", 1, 1) GLOB '[0-7]' AND \"driver_instance_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "app_id": { + "name": "app_id", + "type": "text CHECK (\"app_id\" = upper(\"app_id\") AND length(\"app_id\") = 26 AND substr(\"app_id\", 1, 1) GLOB '[0-7]' AND \"app_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "server_id": { + "name": "server_id", + "type": "text CHECK (\"server_id\" = upper(\"server_id\") AND length(\"server_id\") = 26 AND substr(\"server_id\", 1, 1) GLOB '[0-7]' AND \"server_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "driver_instance_mcp_grant_instance_server_idx": { + "name": "driver_instance_mcp_grant_instance_server_idx", + "columns": ["driver_instance_id", "server_id"], + "isUnique": true + }, + "driver_instance_mcp_grant_instance_credential_idx": { + "name": "driver_instance_mcp_grant_instance_credential_idx", + "columns": ["driver_instance_id", "credential_id"], + "isUnique": false + } + }, + "foreignKeys": { + "driver_instance_mcp_grant_driver_instance_id_driver_instance_id_fk": { + "name": "driver_instance_mcp_grant_driver_instance_id_driver_instance_id_fk", + "tableFrom": "driver_instance_mcp_grant", + "tableTo": "driver_instance", + "columnsFrom": ["driver_instance_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "driver_instance": { + "name": "driver_instance", + "columns": { + "boot_token_expires_at": { + "name": "boot_token_expires_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "boot_token_hash": { + "name": "boot_token_hash", + "type": "blob", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "boot_token_used_at": { + "name": "boot_token_used_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "close_code": { + "name": "close_code", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "close_reason": { + "name": "close_reason", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "connection_id": { + "name": "connection_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "command_seq_cursor": { + "name": "command_seq_cursor", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "driver_pid": { + "name": "driver_pid", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "driver_started_at": { + "name": "driver_started_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "driver_version": { + "name": "driver_version", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "heartbeat_count": { + "name": "heartbeat_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "generation": { + "name": "generation", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "id": { + "name": "id", + "type": "text CHECK (\"id\" = upper(\"id\") AND length(\"id\") = 26 AND substr(\"id\", 1, 1) GLOB '[0-7]' AND \"id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "last_heartbeat_at": { + "name": "last_heartbeat_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "process_id": { + "name": "process_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "protocol": { + "name": "protocol", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "protocol_version": { + "name": "protocol_version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "restart_count": { + "name": "restart_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "runtime": { + "name": "runtime", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "sandbox_id": { + "name": "sandbox_id", + "type": "text CHECK (\"sandbox_id\" = upper(\"sandbox_id\") AND length(\"sandbox_id\") = 26 AND substr(\"sandbox_id\", 1, 1) GLOB '[0-7]' AND \"sandbox_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "sandbox_incarnation": { + "name": "sandbox_incarnation", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "sandbox_session_id": { + "name": "sandbox_session_id", + "type": "text CHECK (\"sandbox_session_id\" = upper(\"sandbox_session_id\") AND length(\"sandbox_session_id\") = 26 AND substr(\"sandbox_session_id\", 1, 1) GLOB '[0-7]' AND \"sandbox_session_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status_changed_at": { + "name": "status_changed_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "status_event": { + "name": "status_event", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'driver.provision'" + }, + "status_operation_id": { + "name": "status_operation_id", + "type": "text CHECK (\"status_operation_id\" = upper(\"status_operation_id\") AND length(\"status_operation_id\") = 26 AND substr(\"status_operation_id\", 1, 1) GLOB '[0-7]' AND \"status_operation_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status_seq": { + "name": "status_seq", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "status_source": { + "name": "status_source", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'system'" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "driver_instance_completed_idx": { + "name": "driver_instance_completed_idx", + "columns": ["expires_at", "status"], + "isUnique": false + }, + "driver_instance_connection_idx": { + "name": "driver_instance_connection_idx", + "columns": ["connection_id"], + "isUnique": true, + "where": "\"driver_instance\".\"connection_id\" IS NOT NULL" + }, + "driver_instance_boot_token_expiry_idx": { + "name": "driver_instance_boot_token_expiry_idx", + "columns": ["status", "boot_token_expires_at"], + "isUnique": false, + "where": "\"driver_instance\".\"boot_token_used_at\" IS NULL" + }, + "driver_instance_boot_token_hash_idx": { + "name": "driver_instance_boot_token_hash_idx", + "columns": ["boot_token_hash"], + "isUnique": true + }, + "driver_instance_sandbox_session_idx": { + "name": "driver_instance_sandbox_session_idx", + "columns": [ + "sandbox_id", + "sandbox_incarnation", + "sandbox_session_id", + "status", + "updated_at" + ], + "isUnique": false + }, + "driver_instance_live_sandbox_session_idx": { + "name": "driver_instance_live_sandbox_session_idx", + "columns": ["sandbox_id", "sandbox_incarnation", "sandbox_session_id"], + "isUnique": true, + "where": "\"driver_instance\".\"status\" IN ('provisioning', 'connecting', 'ready', 'stopping')" + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": { + "driver_instance_status_check": { + "name": "driver_instance_status_check", + "value": "\"driver_instance\".\"status\" IN ('provisioning', 'connecting', 'ready', 'stopping', 'stopped', 'failed')" + }, + "driver_instance_status_seq_check": { + "name": "driver_instance_status_seq_check", + "value": "\"driver_instance\".\"status_seq\" >= 0" + }, + "driver_instance_generation_incarnation_check": { + "name": "driver_instance_generation_incarnation_check", + "value": "typeof(\"driver_instance\".\"generation\") = 'integer' AND \"driver_instance\".\"generation\" BETWEEN 0 AND 9007199254740991 AND typeof(\"driver_instance\".\"sandbox_incarnation\") = 'integer' AND \"driver_instance\".\"sandbox_incarnation\" BETWEEN 0 AND 9007199254740991 AND (\"driver_instance\".\"status\" IN ('stopped', 'failed') OR \"driver_instance\".\"sandbox_incarnation\" > 0)" + } + } + }, + "external_tool_effect_attempt": { + "name": "external_tool_effect_attempt", + "columns": { + "attempt": { + "name": "attempt", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "claim_token": { + "name": "claim_token", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "completed_at": { + "name": "completed_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "effect_id": { + "name": "effect_id", + "type": "text CHECK (\"effect_id\" = upper(\"effect_id\") AND length(\"effect_id\") = 26 AND substr(\"effect_id\", 1, 1) GLOB '[0-7]' AND \"effect_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "provider_receipt_json": { + "name": "provider_receipt_json", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "result_json": { + "name": "result_json", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "external_tool_effect_attempt_status_idx": { + "name": "external_tool_effect_attempt_status_idx", + "columns": ["status", "created_at"], + "isUnique": false + } + }, + "foreignKeys": { + "external_tool_effect_attempt_effect_id_external_tool_effect_id_fk": { + "name": "external_tool_effect_attempt_effect_id_external_tool_effect_id_fk", + "tableFrom": "external_tool_effect_attempt", + "tableTo": "external_tool_effect", + "columnsFrom": ["effect_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "external_tool_effect_attempt_effect_id_attempt_pk": { + "columns": ["effect_id", "attempt"], + "name": "external_tool_effect_attempt_effect_id_attempt_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": { + "external_tool_effect_attempt_status_check": { + "name": "external_tool_effect_attempt_status_check", + "value": "\"external_tool_effect_attempt\".\"status\" IN ('claimed', 'succeeded', 'unknown')" + }, + "external_tool_effect_attempt_claim_token_uuid_check": { + "name": "external_tool_effect_attempt_claim_token_uuid_check", + "value": "length(\"external_tool_effect_attempt\".\"claim_token\") = 36 AND length(replace(\"external_tool_effect_attempt\".\"claim_token\", '-', '')) = 32 AND \"external_tool_effect_attempt\".\"claim_token\" = lower(\"external_tool_effect_attempt\".\"claim_token\") AND substr(\"external_tool_effect_attempt\".\"claim_token\", 9, 1) = '-' AND substr(\"external_tool_effect_attempt\".\"claim_token\", 14, 1) = '-' AND substr(\"external_tool_effect_attempt\".\"claim_token\", 15, 1) = '4' AND substr(\"external_tool_effect_attempt\".\"claim_token\", 19, 1) = '-' AND substr(\"external_tool_effect_attempt\".\"claim_token\", 20, 1) GLOB '[89ab]' AND substr(\"external_tool_effect_attempt\".\"claim_token\", 24, 1) = '-' AND replace(\"external_tool_effect_attempt\".\"claim_token\", '-', '') NOT GLOB '*[^0-9a-f]*'" + } + } + }, + "external_tool_effect": { + "name": "external_tool_effect", + "columns": { + "attempt_count": { + "name": "attempt_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "claim_token": { + "name": "claim_token", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "command_id": { + "name": "command_id", + "type": "text CHECK (\"command_id\" = upper(\"command_id\") AND length(\"command_id\") = 26 AND substr(\"command_id\", 1, 1) GLOB '[0-7]' AND \"command_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "driver_instance_id": { + "name": "driver_instance_id", + "type": "text CHECK (\"driver_instance_id\" = upper(\"driver_instance_id\") AND length(\"driver_instance_id\") = 26 AND substr(\"driver_instance_id\", 1, 1) GLOB '[0-7]' AND \"driver_instance_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "id": { + "name": "id", + "type": "text CHECK (\"id\" = upper(\"id\") AND length(\"id\") = 26 AND substr(\"id\", 1, 1) GLOB '[0-7]' AND \"id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "idempotency_key": { + "name": "idempotency_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "provider_receipt_json": { + "name": "provider_receipt_json", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "result_json": { + "name": "result_json", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "server_id": { + "name": "server_id", + "type": "text CHECK (\"server_id\" = upper(\"server_id\") AND length(\"server_id\") = 26 AND substr(\"server_id\", 1, 1) GLOB '[0-7]' AND \"server_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "session_run_id": { + "name": "session_run_id", + "type": "text CHECK (\"session_run_id\" = upper(\"session_run_id\") AND length(\"session_run_id\") = 26 AND substr(\"session_run_id\", 1, 1) GLOB '[0-7]' AND \"session_run_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "tool_name": { + "name": "tool_name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "external_tool_effect_command_idx": { + "name": "external_tool_effect_command_idx", + "columns": ["command_id"], + "isUnique": true + }, + "external_tool_effect_idempotency_key_idx": { + "name": "external_tool_effect_idempotency_key_idx", + "columns": ["idempotency_key"], + "isUnique": true + }, + "external_tool_effect_run_status_idx": { + "name": "external_tool_effect_run_status_idx", + "columns": ["session_run_id", "status", "id"], + "isUnique": false + }, + "external_tool_effect_driver_status_idx": { + "name": "external_tool_effect_driver_status_idx", + "columns": ["driver_instance_id", "status"], + "isUnique": false + } + }, + "foreignKeys": { + "external_tool_effect_command_id_driver_command_id_fk": { + "name": "external_tool_effect_command_id_driver_command_id_fk", + "tableFrom": "external_tool_effect", + "tableTo": "driver_command", + "columnsFrom": ["command_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "external_tool_effect_driver_instance_id_driver_instance_id_fk": { + "name": "external_tool_effect_driver_instance_id_driver_instance_id_fk", + "tableFrom": "external_tool_effect", + "tableTo": "driver_instance", + "columnsFrom": ["driver_instance_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "external_tool_effect_session_run_id_session_run_id_fk": { + "name": "external_tool_effect_session_run_id_session_run_id_fk", + "tableFrom": "external_tool_effect", + "tableTo": "session_run", + "columnsFrom": ["session_run_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": { + "external_tool_effect_status_check": { + "name": "external_tool_effect_status_check", + "value": "\"external_tool_effect\".\"status\" IN ('intent', 'claimed', 'succeeded', 'unknown')" + }, + "external_tool_effect_claim_token_uuid_check": { + "name": "external_tool_effect_claim_token_uuid_check", + "value": "\"external_tool_effect\".\"claim_token\" IS NULL OR (length(\"external_tool_effect\".\"claim_token\") = 36 AND length(replace(\"external_tool_effect\".\"claim_token\", '-', '')) = 32 AND \"external_tool_effect\".\"claim_token\" = lower(\"external_tool_effect\".\"claim_token\") AND substr(\"external_tool_effect\".\"claim_token\", 9, 1) = '-' AND substr(\"external_tool_effect\".\"claim_token\", 14, 1) = '-' AND substr(\"external_tool_effect\".\"claim_token\", 15, 1) = '4' AND substr(\"external_tool_effect\".\"claim_token\", 19, 1) = '-' AND substr(\"external_tool_effect\".\"claim_token\", 20, 1) GLOB '[89ab]' AND substr(\"external_tool_effect\".\"claim_token\", 24, 1) = '-' AND replace(\"external_tool_effect\".\"claim_token\", '-', '') NOT GLOB '*[^0-9a-f]*')" + } + } + }, + "native_resume_ref": { + "name": "native_resume_ref", + "columns": { + "committed_session_run_id": { + "name": "committed_session_run_id", + "type": "text CHECK (\"committed_session_run_id\" = upper(\"committed_session_run_id\") AND length(\"committed_session_run_id\") = 26 AND substr(\"committed_session_run_id\", 1, 1) GLOB '[0-7]' AND \"committed_session_run_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "committed_value": { + "name": "committed_value", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "observed_driver_instance_id": { + "name": "observed_driver_instance_id", + "type": "text CHECK (\"observed_driver_instance_id\" = upper(\"observed_driver_instance_id\") AND length(\"observed_driver_instance_id\") = 26 AND substr(\"observed_driver_instance_id\", 1, 1) GLOB '[0-7]' AND \"observed_driver_instance_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "observed_event_seq": { + "name": "observed_event_seq", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "observed_session_run_id": { + "name": "observed_session_run_id", + "type": "text CHECK (\"observed_session_run_id\" = upper(\"observed_session_run_id\") AND length(\"observed_session_run_id\") = 26 AND substr(\"observed_session_run_id\", 1, 1) GLOB '[0-7]' AND \"observed_session_run_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "runtime_id": { + "name": "runtime_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "session_id": { + "name": "session_id", + "type": "text CHECK (\"session_id\" = upper(\"session_id\") AND length(\"session_id\") = 26 AND substr(\"session_id\", 1, 1) GLOB '[0-7]' AND \"session_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "native_resume_ref_runtime_updated_idx": { + "name": "native_resume_ref_runtime_updated_idx", + "columns": ["runtime_id", "updated_at"], + "isUnique": false + } + }, + "foreignKeys": { + "native_resume_ref_session_id_session_id_fk": { + "name": "native_resume_ref_session_id_session_id_fk", + "tableFrom": "native_resume_ref", + "tableTo": "session", + "columnsFrom": ["session_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": { + "native_resume_ref_observed_event_seq_check": { + "name": "native_resume_ref_observed_event_seq_check", + "value": "\"native_resume_ref\".\"observed_event_seq\" >= 0" + } + } + }, + "sandbox_backup_staging": { + "name": "sandbox_backup_staging", + "columns": { + "actual_backup_id": { + "name": "actual_backup_id", + "type": "text CHECK (\"actual_backup_id\" = upper(\"actual_backup_id\") AND length(\"actual_backup_id\") = 26 AND substr(\"actual_backup_id\", 1, 1) GLOB '[0-7]' AND \"actual_backup_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "dir": { + "name": "dir", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "driver_generation": { + "name": "driver_generation", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "driver_instance_id": { + "name": "driver_instance_id", + "type": "text CHECK (\"driver_instance_id\" = upper(\"driver_instance_id\") AND length(\"driver_instance_id\") = 26 AND substr(\"driver_instance_id\", 1, 1) GLOB '[0-7]' AND \"driver_instance_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "id": { + "name": "id", + "type": "text CHECK (\"id\" = upper(\"id\") AND length(\"id\") = 26 AND substr(\"id\", 1, 1) GLOB '[0-7]' AND \"id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "operation_id": { + "name": "operation_id", + "type": "text CHECK (\"operation_id\" = upper(\"operation_id\") AND length(\"operation_id\") = 26 AND substr(\"operation_id\", 1, 1) GLOB '[0-7]' AND \"operation_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sandbox_id": { + "name": "sandbox_id", + "type": "text CHECK (\"sandbox_id\" = upper(\"sandbox_id\") AND length(\"sandbox_id\") = 26 AND substr(\"sandbox_id\", 1, 1) GLOB '[0-7]' AND \"sandbox_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "sandbox_incarnation": { + "name": "sandbox_incarnation", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "session_run_id": { + "name": "session_run_id", + "type": "text CHECK (\"session_run_id\" = upper(\"session_run_id\") AND length(\"session_run_id\") = 26 AND substr(\"session_run_id\", 1, 1) GLOB '[0-7]' AND \"session_run_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "ttl_seconds": { + "name": "ttl_seconds", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updates_subject_backup": { + "name": "updates_subject_backup", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "workspace_session_id": { + "name": "workspace_session_id", + "type": "text CHECK (\"workspace_session_id\" = upper(\"workspace_session_id\") AND length(\"workspace_session_id\") = 26 AND substr(\"workspace_session_id\", 1, 1) GLOB '[0-7]' AND \"workspace_session_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "sandbox_backup_staging_updated_idx": { + "name": "sandbox_backup_staging_updated_idx", + "columns": ["updated_at", "id"], + "isUnique": false + }, + "sandbox_backup_staging_actual_idx": { + "name": "sandbox_backup_staging_actual_idx", + "columns": ["actual_backup_id"], + "isUnique": true, + "where": "\"sandbox_backup_staging\".\"actual_backup_id\" IS NOT NULL" + }, + "sandbox_backup_staging_terminal_checkpoint_idx": { + "name": "sandbox_backup_staging_terminal_checkpoint_idx", + "columns": ["sandbox_id", "sandbox_incarnation", "dir", "session_run_id"], + "isUnique": true, + "where": "\"sandbox_backup_staging\".\"session_run_id\" IS NOT NULL" + }, + "sandbox_backup_staging_operation_checkpoint_idx": { + "name": "sandbox_backup_staging_operation_checkpoint_idx", + "columns": ["sandbox_id", "sandbox_incarnation", "operation_id", "dir"], + "isUnique": true, + "where": "\"sandbox_backup_staging\".\"operation_id\" IS NOT NULL" + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": { + "sandbox_backup_staging_dir_check": { + "name": "sandbox_backup_staging_dir_check", + "value": "typeof(\"sandbox_backup_staging\".\"dir\") = 'text' AND length(\"sandbox_backup_staging\".\"dir\") > 0" + }, + "sandbox_backup_staging_incarnation_check": { + "name": "sandbox_backup_staging_incarnation_check", + "value": "typeof(\"sandbox_backup_staging\".\"sandbox_incarnation\") = 'integer' AND \"sandbox_backup_staging\".\"sandbox_incarnation\" BETWEEN 1 AND 9007199254740991" + }, + "sandbox_backup_staging_ttl_check": { + "name": "sandbox_backup_staging_ttl_check", + "value": "typeof(\"sandbox_backup_staging\".\"ttl_seconds\") = 'integer' AND \"sandbox_backup_staging\".\"ttl_seconds\" BETWEEN 1 AND 9007199254740991" + }, + "sandbox_backup_staging_timestamps_check": { + "name": "sandbox_backup_staging_timestamps_check", + "value": "typeof(\"sandbox_backup_staging\".\"created_at\") = 'integer' AND \"sandbox_backup_staging\".\"created_at\" BETWEEN 0 AND 9007199254740991 AND typeof(\"sandbox_backup_staging\".\"updated_at\") = 'integer' AND \"sandbox_backup_staging\".\"updated_at\" BETWEEN \"sandbox_backup_staging\".\"created_at\" AND 9007199254740991" + }, + "sandbox_backup_staging_scope_check": { + "name": "sandbox_backup_staging_scope_check", + "value": "((\"sandbox_backup_staging\".\"operation_id\" IS NOT NULL AND \"sandbox_backup_staging\".\"session_run_id\" IS NULL AND \"sandbox_backup_staging\".\"driver_instance_id\" IS NULL AND \"sandbox_backup_staging\".\"driver_generation\" IS NULL) OR (\"sandbox_backup_staging\".\"operation_id\" IS NULL AND \"sandbox_backup_staging\".\"session_run_id\" IS NOT NULL AND \"sandbox_backup_staging\".\"workspace_session_id\" IS NOT NULL AND \"sandbox_backup_staging\".\"driver_instance_id\" IS NOT NULL AND typeof(\"sandbox_backup_staging\".\"driver_generation\") = 'integer' AND \"sandbox_backup_staging\".\"driver_generation\" BETWEEN 0 AND 9007199254740991)) AND (\"sandbox_backup_staging\".\"updates_subject_backup\" = false OR (\"sandbox_backup_staging\".\"operation_id\" IS NOT NULL AND \"sandbox_backup_staging\".\"workspace_session_id\" IS NULL))" + }, + "sandbox_backup_staging_updates_subject_check": { + "name": "sandbox_backup_staging_updates_subject_check", + "value": "typeof(\"sandbox_backup_staging\".\"updates_subject_backup\") = 'integer' AND \"sandbox_backup_staging\".\"updates_subject_backup\" IN (false, true)" + } + } + }, + "sandbox_backup": { + "name": "sandbox_backup", + "columns": { + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "dir": { + "name": "dir", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "id": { + "name": "id", + "type": "text CHECK (\"id\" = upper(\"id\") AND length(\"id\") = 26 AND substr(\"id\", 1, 1) GLOB '[0-7]' AND \"id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "keep": { + "name": "keep", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "operation_id": { + "name": "operation_id", + "type": "text CHECK (\"operation_id\" = upper(\"operation_id\") AND length(\"operation_id\") = 26 AND substr(\"operation_id\", 1, 1) GLOB '[0-7]' AND \"operation_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sandbox_id": { + "name": "sandbox_id", + "type": "text CHECK (\"sandbox_id\" = upper(\"sandbox_id\") AND length(\"sandbox_id\") = 26 AND substr(\"sandbox_id\", 1, 1) GLOB '[0-7]' AND \"sandbox_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "sandbox_incarnation": { + "name": "sandbox_incarnation", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "session_run_id": { + "name": "session_run_id", + "type": "text CHECK (\"session_run_id\" = upper(\"session_run_id\") AND length(\"session_run_id\") = 26 AND substr(\"session_run_id\", 1, 1) GLOB '[0-7]' AND \"session_run_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "staging_id": { + "name": "staging_id", + "type": "text CHECK (\"staging_id\" = upper(\"staging_id\") AND length(\"staging_id\") = 26 AND substr(\"staging_id\", 1, 1) GLOB '[0-7]' AND \"staging_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "ttl_seconds": { + "name": "ttl_seconds", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "workspace_session_id": { + "name": "workspace_session_id", + "type": "text CHECK (\"workspace_session_id\" = upper(\"workspace_session_id\") AND length(\"workspace_session_id\") = 26 AND substr(\"workspace_session_id\", 1, 1) GLOB '[0-7]' AND \"workspace_session_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "sandbox_backup_sandbox_status_dir_created_idx": { + "name": "sandbox_backup_sandbox_status_dir_created_idx", + "columns": ["sandbox_id", "status", "dir", "created_at", "id"], + "isUnique": false + }, + "sandbox_backup_workspace_status_updated_idx": { + "name": "sandbox_backup_workspace_status_updated_idx", + "columns": ["workspace_session_id", "status", "updated_at", "id"], + "isUnique": false + }, + "sandbox_backup_staging_idx": { + "name": "sandbox_backup_staging_idx", + "columns": ["staging_id"], + "isUnique": true + }, + "sandbox_backup_terminal_checkpoint_idx": { + "name": "sandbox_backup_terminal_checkpoint_idx", + "columns": ["sandbox_id", "sandbox_incarnation", "dir", "session_run_id"], + "isUnique": true, + "where": "\"sandbox_backup\".\"session_run_id\" IS NOT NULL" + }, + "sandbox_backup_operation_checkpoint_idx": { + "name": "sandbox_backup_operation_checkpoint_idx", + "columns": ["sandbox_id", "sandbox_incarnation", "operation_id", "dir"], + "isUnique": true, + "where": "\"sandbox_backup\".\"operation_id\" IS NOT NULL" + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": { + "sandbox_backup_status_check": { + "name": "sandbox_backup_status_check", + "value": "\"sandbox_backup\".\"status\" IN ('ready', 'pruned')" + }, + "sandbox_backup_dir_check": { + "name": "sandbox_backup_dir_check", + "value": "typeof(\"sandbox_backup\".\"dir\") = 'text' AND length(\"sandbox_backup\".\"dir\") > 0" + }, + "sandbox_backup_keep_check": { + "name": "sandbox_backup_keep_check", + "value": "typeof(\"sandbox_backup\".\"keep\") = 'integer' AND \"sandbox_backup\".\"keep\" IN (false, true)" + }, + "sandbox_backup_incarnation_check": { + "name": "sandbox_backup_incarnation_check", + "value": "typeof(\"sandbox_backup\".\"sandbox_incarnation\") = 'integer' AND \"sandbox_backup\".\"sandbox_incarnation\" BETWEEN 0 AND 9007199254740991 AND (\"sandbox_backup\".\"sandbox_incarnation\" > 0 OR \"sandbox_backup\".\"staging_id\" = \"sandbox_backup\".\"id\")" + }, + "sandbox_backup_ttl_check": { + "name": "sandbox_backup_ttl_check", + "value": "typeof(\"sandbox_backup\".\"ttl_seconds\") = 'integer' AND \"sandbox_backup\".\"ttl_seconds\" BETWEEN 1 AND 9007199254740991" + }, + "sandbox_backup_timestamps_check": { + "name": "sandbox_backup_timestamps_check", + "value": "typeof(\"sandbox_backup\".\"created_at\") = 'integer' AND \"sandbox_backup\".\"created_at\" BETWEEN 0 AND 9007199254740991 AND typeof(\"sandbox_backup\".\"updated_at\") = 'integer' AND \"sandbox_backup\".\"updated_at\" BETWEEN \"sandbox_backup\".\"created_at\" AND 9007199254740991" + }, + "sandbox_backup_scope_check": { + "name": "sandbox_backup_scope_check", + "value": "(\"sandbox_backup\".\"session_run_id\" IS NULL OR \"sandbox_backup\".\"workspace_session_id\" IS NOT NULL) AND ((\"sandbox_backup\".\"operation_id\" IS NOT NULL) <> (\"sandbox_backup\".\"session_run_id\" IS NOT NULL) OR (\"sandbox_backup\".\"operation_id\" IS NULL AND \"sandbox_backup\".\"session_run_id\" IS NULL AND \"sandbox_backup\".\"workspace_session_id\" IS NULL AND \"sandbox_backup\".\"staging_id\" = \"sandbox_backup\".\"id\" AND \"sandbox_backup\".\"sandbox_incarnation\" = 0))" + } + } + }, + "sandbox_session": { + "name": "sandbox_session", + "columns": { + "cloudflare_session_id": { + "name": "cloudflare_session_id", + "type": "text CHECK (\"cloudflare_session_id\" = upper(\"cloudflare_session_id\") AND length(\"cloudflare_session_id\") = 26 AND substr(\"cloudflare_session_id\", 1, 1) GLOB '[0-7]' AND \"cloudflare_session_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "cleanup_operation_id": { + "name": "cleanup_operation_id", + "type": "text CHECK (\"cleanup_operation_id\" = upper(\"cleanup_operation_id\") AND length(\"cleanup_operation_id\") = 26 AND substr(\"cleanup_operation_id\", 1, 1) GLOB '[0-7]' AND \"cleanup_operation_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "cwd": { + "name": "cwd", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "origin_json": { + "name": "origin_json", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "sandbox_id": { + "name": "sandbox_id", + "type": "text CHECK (\"sandbox_id\" = upper(\"sandbox_id\") AND length(\"sandbox_id\") = 26 AND substr(\"sandbox_id\", 1, 1) GLOB '[0-7]' AND \"sandbox_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "sandbox_incarnation": { + "name": "sandbox_incarnation", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "session_id": { + "name": "session_id", + "type": "text CHECK (\"session_id\" = upper(\"session_id\") AND length(\"session_id\") = 26 AND substr(\"session_id\", 1, 1) GLOB '[0-7]' AND \"session_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "sandbox_session_status_updated_idx": { + "name": "sandbox_session_status_updated_idx", + "columns": ["status", "updated_at", "session_id"], + "isUnique": false + }, + "sandbox_session_sandbox_status_idx": { + "name": "sandbox_session_sandbox_status_idx", + "columns": ["sandbox_id", "status", "updated_at"], + "isUnique": false + }, + "sandbox_session_cloudflare_session_idx": { + "name": "sandbox_session_cloudflare_session_idx", + "columns": ["cloudflare_session_id"], + "isUnique": true + } + }, + "foreignKeys": { + "sandbox_session_session_id_session_id_fk": { + "name": "sandbox_session_session_id_session_id_fk", + "tableFrom": "sandbox_session", + "tableTo": "session", + "columnsFrom": ["session_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": { + "sandbox_session_cleanup_check": { + "name": "sandbox_session_cleanup_check", + "value": "(\"sandbox_session\".\"status\" = 'cleanup_pending' AND \"sandbox_session\".\"cleanup_operation_id\" IS NOT NULL) OR (\"sandbox_session\".\"status\" <> 'cleanup_pending' AND \"sandbox_session\".\"cleanup_operation_id\" IS NULL)" + }, + "sandbox_session_status_incarnation_check": { + "name": "sandbox_session_status_incarnation_check", + "value": "\"sandbox_session\".\"status\" IN ('active', 'cleanup_pending', 'closed', 'error') AND typeof(\"sandbox_session\".\"sandbox_incarnation\") = 'integer' AND \"sandbox_session\".\"sandbox_incarnation\" BETWEEN 0 AND 9007199254740991 AND (\"sandbox_session\".\"status\" IN ('closed', 'error') OR \"sandbox_session\".\"sandbox_incarnation\" > 0)" + } + } + }, + "sandbox": { + "name": "sandbox", + "columns": { + "agent_id": { + "name": "agent_id", + "type": "text CHECK (\"agent_id\" = upper(\"agent_id\") AND length(\"agent_id\") = 26 AND substr(\"agent_id\", 1, 1) GLOB '[0-7]' AND \"agent_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "app_id": { + "name": "app_id", + "type": "text CHECK (\"app_id\" = upper(\"app_id\") AND length(\"app_id\") = 26 AND substr(\"app_id\", 1, 1) GLOB '[0-7]' AND \"app_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "bind_mount_ready": { + "name": "bind_mount_ready", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "claim_expires_at": { + "name": "claim_expires_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "claim_owner": { + "name": "claim_owner", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "global_mounts_json": { + "name": "global_mounts_json", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'[]'" + }, + "id": { + "name": "id", + "type": "text CHECK (\"id\" = upper(\"id\") AND length(\"id\") = 26 AND substr(\"id\", 1, 1) GLOB '[0-7]' AND \"id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "inactive_deadline_at": { + "name": "inactive_deadline_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "incarnation": { + "name": "incarnation", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "last_backup_id": { + "name": "last_backup_id", + "type": "text CHECK (\"last_backup_id\" = upper(\"last_backup_id\") AND length(\"last_backup_id\") = 26 AND substr(\"last_backup_id\", 1, 1) GLOB '[0-7]' AND \"last_backup_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_error_code": { + "name": "last_error_code", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_restore_backup_id": { + "name": "last_restore_backup_id", + "type": "text CHECK (\"last_restore_backup_id\" = upper(\"last_restore_backup_id\") AND length(\"last_restore_backup_id\") = 26 AND substr(\"last_restore_backup_id\", 1, 1) GLOB '[0-7]' AND \"last_restore_backup_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "network_constraints_hash": { + "name": "network_constraints_hash", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "owner_account_id": { + "name": "owner_account_id", + "type": "text CHECK (\"owner_account_id\" = upper(\"owner_account_id\") AND length(\"owner_account_id\") = 26 AND substr(\"owner_account_id\", 1, 1) GLOB '[0-7]' AND \"owner_account_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "operation_kind": { + "name": "operation_kind", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status_changed_at": { + "name": "status_changed_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "status_event": { + "name": "status_event", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'runtime_subject.cold'" + }, + "status_operation_id": { + "name": "status_operation_id", + "type": "text CHECK (\"status_operation_id\" = upper(\"status_operation_id\") AND length(\"status_operation_id\") = 26 AND substr(\"status_operation_id\", 1, 1) GLOB '[0-7]' AND \"status_operation_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status_seq": { + "name": "status_seq", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "status_source": { + "name": "status_source", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'system'" + }, + "subject_id": { + "name": "subject_id", + "type": "text CHECK (\"subject_id\" = upper(\"subject_id\") AND length(\"subject_id\") = 26 AND substr(\"subject_id\", 1, 1) GLOB '[0-7]' AND \"subject_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "subject_kind": { + "name": "subject_kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "sandbox_subject_idx": { + "name": "sandbox_subject_idx", + "columns": ["kind", "subject_kind", "subject_id"], + "isUnique": true + }, + "sandbox_status_deadline_idx": { + "name": "sandbox_status_deadline_idx", + "columns": ["status", "inactive_deadline_at", "updated_at"], + "isUnique": false + }, + "sandbox_claim_idx": { + "name": "sandbox_claim_idx", + "columns": ["claim_expires_at", "claim_owner"], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": { + "sandbox_status_check": { + "name": "sandbox_status_check", + "value": "\"sandbox\".\"status\" IN ('cold', 'restoring', 'active', 'backing_up', 'destroying')" + }, + "sandbox_status_seq_check": { + "name": "sandbox_status_seq_check", + "value": "\"sandbox\".\"status_seq\" >= 0" + }, + "sandbox_incarnation_check": { + "name": "sandbox_incarnation_check", + "value": "typeof(\"sandbox\".\"incarnation\") = 'integer' AND \"sandbox\".\"incarnation\" BETWEEN 0 AND 9007199254740991 AND (\"sandbox\".\"status\" = 'cold' OR \"sandbox\".\"incarnation\" > 0)" + }, + "sandbox_identity_check": { + "name": "sandbox_identity_check", + "value": "(\"sandbox\".\"kind\" = 'pet' AND \"sandbox\".\"subject_kind\" = 'agent' AND \"sandbox\".\"subject_id\" = \"sandbox\".\"agent_id\") OR (\"sandbox\".\"kind\" = 'cattle' AND \"sandbox\".\"subject_kind\" = 'session')" + }, + "sandbox_network_constraints_hash_check": { + "name": "sandbox_network_constraints_hash_check", + "value": "(\"sandbox\".\"network_constraints_hash\" IS NULL AND \"sandbox\".\"status\" = 'cold') OR (\"sandbox\".\"network_constraints_hash\" IS NOT NULL AND length(\"sandbox\".\"network_constraints_hash\") = 64 AND \"sandbox\".\"network_constraints_hash\" = lower(\"sandbox\".\"network_constraints_hash\") AND \"sandbox\".\"network_constraints_hash\" NOT GLOB '*[^0-9a-f]*')" + }, + "sandbox_operation_state_check": { + "name": "sandbox_operation_state_check", + "value": "(\"sandbox\".\"status\" IN ('cold', 'active') AND \"sandbox\".\"operation_kind\" IS NULL AND \"sandbox\".\"status_operation_id\" IS NULL) OR (\"sandbox\".\"status\" = 'restoring' AND \"sandbox\".\"operation_kind\" = 'activate' AND \"sandbox\".\"status_operation_id\" IS NOT NULL) OR (\"sandbox\".\"status\" = 'backing_up' AND \"sandbox\".\"operation_kind\" IN ('hibernate', 'recreate', 'reset') AND \"sandbox\".\"status_operation_id\" IS NOT NULL) OR (\"sandbox\".\"status\" = 'destroying' AND \"sandbox\".\"operation_kind\" IN ('activate', 'hibernate', 'recreate', 'reset') AND \"sandbox\".\"status_operation_id\" IS NOT NULL)" + }, + "sandbox_claim_check": { + "name": "sandbox_claim_check", + "value": "(\"sandbox\".\"claim_owner\" IS NULL AND \"sandbox\".\"claim_expires_at\" IS NULL) OR (\"sandbox\".\"claim_owner\" IS NOT NULL AND typeof(\"sandbox\".\"claim_expires_at\") = 'integer' AND \"sandbox\".\"claim_expires_at\" BETWEEN 0 AND 9007199254740991)" + }, + "sandbox_operation_claim_check": { + "name": "sandbox_operation_claim_check", + "value": "\"sandbox\".\"status\" IN ('cold', 'active') OR \"sandbox\".\"claim_owner\" IS NOT NULL" + } + } + }, + "session_message": { + "name": "session_message", + "columns": { + "content_text": { + "name": "content_text", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_by_account_id": { + "name": "created_by_account_id", + "type": "text CHECK (\"created_by_account_id\" = upper(\"created_by_account_id\") AND length(\"created_by_account_id\") = 26 AND substr(\"created_by_account_id\", 1, 1) GLOB '[0-7]' AND \"created_by_account_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "id": { + "name": "id", + "type": "text CHECK (\"id\" = upper(\"id\") AND length(\"id\") = 26 AND substr(\"id\", 1, 1) GLOB '[0-7]' AND \"id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "plan_json": { + "name": "plan_json", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "projection_format": { + "name": "projection_format", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'materialized'" + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "segments_json": { + "name": "segments_json", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "seq": { + "name": "seq", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "session_id": { + "name": "session_id", + "type": "text CHECK (\"session_id\" = upper(\"session_id\") AND length(\"session_id\") = 26 AND substr(\"session_id\", 1, 1) GLOB '[0-7]' AND \"session_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "session_run_id": { + "name": "session_run_id", + "type": "text CHECK (\"session_run_id\" = upper(\"session_run_id\") AND length(\"session_run_id\") = 26 AND substr(\"session_run_id\", 1, 1) GLOB '[0-7]' AND \"session_run_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "session_message_session_seq_idx": { + "name": "session_message_session_seq_idx", + "columns": ["session_id", "seq"], + "isUnique": true + }, + "session_message_run_idx": { + "name": "session_message_run_idx", + "columns": ["session_run_id"], + "isUnique": false + } + }, + "foreignKeys": { + "session_message_session_id_session_id_fk": { + "name": "session_message_session_id_session_id_fk", + "tableFrom": "session_message", + "tableTo": "session", + "columnsFrom": ["session_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": { + "session_message_projection_format_check": { + "name": "session_message_projection_format_check", + "value": "\"session_message\".\"projection_format\" IN ('materialized', 'event_stream_v3')" + }, + "session_message_event_stream_v3_check": { + "name": "session_message_event_stream_v3_check", + "value": "\"session_message\".\"projection_format\" <> 'event_stream_v3' OR (\"session_message\".\"role\" = 'assistant' AND \"session_message\".\"session_run_id\" IS NOT NULL AND \"session_message\".\"content_text\" = '' AND \"session_message\".\"plan_json\" IS NULL AND \"session_message\".\"segments_json\" IS NULL)" + } + } + }, + "session": { + "name": "session", + "columns": { + "agent_id": { + "name": "agent_id", + "type": "text CHECK (\"agent_id\" = upper(\"agent_id\") AND length(\"agent_id\") = 26 AND substr(\"agent_id\", 1, 1) GLOB '[0-7]' AND \"agent_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "archived_at": { + "name": "archived_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "auto_title_event_seq": { + "name": "auto_title_event_seq", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "cleanup_operation_kind": { + "name": "cleanup_operation_kind", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "end_user_id": { + "name": "end_user_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "attributed_user_id": { + "name": "attributed_user_id", + "type": "text CHECK (\"attributed_user_id\" = upper(\"attributed_user_id\") AND length(\"attributed_user_id\") = 26 AND substr(\"attributed_user_id\", 1, 1) GLOB '[0-7]' AND \"attributed_user_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "creator_account_id": { + "name": "creator_account_id", + "type": "text CHECK (\"creator_account_id\" = upper(\"creator_account_id\") AND length(\"creator_account_id\") = 26 AND substr(\"creator_account_id\", 1, 1) GLOB '[0-7]' AND \"creator_account_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "deployment_version_id": { + "name": "deployment_version_id", + "type": "text CHECK (\"deployment_version_id\" = upper(\"deployment_version_id\") AND length(\"deployment_version_id\") = 26 AND substr(\"deployment_version_id\", 1, 1) GLOB '[0-7]' AND \"deployment_version_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "deployment_version_number": { + "name": "deployment_version_number", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "id": { + "name": "id", + "type": "text CHECK (\"id\" = upper(\"id\") AND length(\"id\") = 26 AND substr(\"id\", 1, 1) GLOB '[0-7]' AND \"id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "last_message_at": { + "name": "last_message_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_run_id": { + "name": "last_run_id", + "type": "text CHECK (\"last_run_id\" = upper(\"last_run_id\") AND length(\"last_run_id\") = 26 AND substr(\"last_run_id\", 1, 1) GLOB '[0-7]' AND \"last_run_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "message_seq_cursor": { + "name": "message_seq_cursor", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "metadata_json": { + "name": "metadata_json", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'{}'" + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "app_id": { + "name": "app_id", + "type": "text CHECK (\"app_id\" = upper(\"app_id\") AND length(\"app_id\") = 26 AND substr(\"app_id\", 1, 1) GLOB '[0-7]' AND \"app_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "renamed": { + "name": "renamed", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "runtime_id": { + "name": "runtime_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status_operation_id": { + "name": "status_operation_id", + "type": "text CHECK (\"status_operation_id\" = upper(\"status_operation_id\") AND length(\"status_operation_id\") = 26 AND substr(\"status_operation_id\", 1, 1) GLOB '[0-7]' AND \"status_operation_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status_seq": { + "name": "status_seq", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "runtime_event_seq_cursor": { + "name": "runtime_event_seq_cursor", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "runtime_provisioning_heartbeat_at": { + "name": "runtime_provisioning_heartbeat_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "runtime_provisioning_operation_id": { + "name": "runtime_provisioning_operation_id", + "type": "text CHECK (\"runtime_provisioning_operation_id\" = upper(\"runtime_provisioning_operation_id\") AND length(\"runtime_provisioning_operation_id\") = 26 AND substr(\"runtime_provisioning_operation_id\", 1, 1) GLOB '[0-7]' AND \"runtime_provisioning_operation_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "runtime_provisioning_run_id": { + "name": "runtime_provisioning_run_id", + "type": "text CHECK (\"runtime_provisioning_run_id\" = upper(\"runtime_provisioning_run_id\") AND length(\"runtime_provisioning_run_id\") = 26 AND substr(\"runtime_provisioning_run_id\", 1, 1) GLOB '[0-7]' AND \"runtime_provisioning_run_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "runtime_provisioning_sandbox_id": { + "name": "runtime_provisioning_sandbox_id", + "type": "text CHECK (\"runtime_provisioning_sandbox_id\" = upper(\"runtime_provisioning_sandbox_id\") AND length(\"runtime_provisioning_sandbox_id\") = 26 AND substr(\"runtime_provisioning_sandbox_id\", 1, 1) GLOB '[0-7]' AND \"runtime_provisioning_sandbox_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "runtime_provisioning_sandbox_session_id": { + "name": "runtime_provisioning_sandbox_session_id", + "type": "text CHECK (\"runtime_provisioning_sandbox_session_id\" = upper(\"runtime_provisioning_sandbox_session_id\") AND length(\"runtime_provisioning_sandbox_session_id\") = 26 AND substr(\"runtime_provisioning_sandbox_session_id\", 1, 1) GLOB '[0-7]' AND \"runtime_provisioning_sandbox_session_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "runtime_provisioning_sandbox_incarnation": { + "name": "runtime_provisioning_sandbox_incarnation", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'preview'" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "workspace_checkpoint_required": { + "name": "workspace_checkpoint_required", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + } + }, + "indexes": { + "session_agent_updated_idx": { + "name": "session_agent_updated_idx", + "columns": ["agent_id", "updated_at", "id"], + "isUnique": false + }, + "session_app_creator_archived_updated_idx": { + "name": "session_app_creator_archived_updated_idx", + "columns": ["app_id", "creator_account_id", "archived_at", "updated_at", "id"], + "isUnique": false + }, + "session_app_attributed_archived_updated_idx": { + "name": "session_app_attributed_archived_updated_idx", + "columns": ["app_id", "attributed_user_id", "archived_at", "updated_at", "id"], + "isUnique": false + }, + "session_app_creator_type_archived_updated_idx": { + "name": "session_app_creator_type_archived_updated_idx", + "columns": ["app_id", "creator_account_id", "type", "archived_at", "updated_at", "id"], + "isUnique": false + }, + "session_app_attributed_type_archived_updated_idx": { + "name": "session_app_attributed_type_archived_updated_idx", + "columns": ["app_id", "attributed_user_id", "type", "archived_at", "updated_at", "id"], + "isUnique": false + }, + "session_status_operation_updated_idx": { + "name": "session_status_operation_updated_idx", + "columns": ["status", "status_operation_id", "updated_at"], + "isUnique": false + }, + "session_cleanup_operation_updated_idx": { + "name": "session_cleanup_operation_updated_idx", + "columns": ["cleanup_operation_kind", "status", "updated_at", "id"], + "isUnique": false + }, + "session_runtime_provisioning_heartbeat_idx": { + "name": "session_runtime_provisioning_heartbeat_idx", + "columns": ["runtime_provisioning_heartbeat_at", "id"], + "isUnique": false + }, + "session_runtime_provisioning_sandbox_idx": { + "name": "session_runtime_provisioning_sandbox_idx", + "columns": ["runtime_provisioning_sandbox_id"], + "isUnique": true, + "where": "\"session\".\"runtime_provisioning_operation_id\" IS NOT NULL" + }, + "session_status_updated_idx": { + "name": "session_status_updated_idx", + "columns": ["status", "updated_at", "id"], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": { + "session_cleanup_operation_kind_check": { + "name": "session_cleanup_operation_kind_check", + "value": "\"session\".\"cleanup_operation_kind\" IS NULL OR (\"session\".\"cleanup_operation_kind\" IN ('archive', 'delete') AND \"session\".\"archived_at\" IS NOT NULL AND \"session\".\"status\" IN ('IDLE', 'RESCHEDULING') AND (\"session\".\"status_operation_id\" IS NOT NULL OR (\"session\".\"cleanup_operation_kind\" = 'archive' AND \"session\".\"status\" = 'IDLE')))" + }, + "session_runtime_provisioning_lease_check": { + "name": "session_runtime_provisioning_lease_check", + "value": "(\"session\".\"runtime_provisioning_operation_id\" IS NULL AND \"session\".\"runtime_provisioning_run_id\" IS NULL AND \"session\".\"runtime_provisioning_sandbox_id\" IS NULL AND \"session\".\"runtime_provisioning_heartbeat_at\" IS NULL) OR (\"session\".\"runtime_provisioning_operation_id\" IS NOT NULL AND \"session\".\"runtime_provisioning_sandbox_id\" IS NOT NULL AND \"session\".\"runtime_provisioning_heartbeat_at\" IS NOT NULL AND typeof(\"session\".\"runtime_provisioning_heartbeat_at\") = 'integer' AND \"session\".\"runtime_provisioning_heartbeat_at\" >= 0 AND \"session\".\"archived_at\" IS NULL AND \"session\".\"cleanup_operation_kind\" IS NULL AND \"session\".\"status_operation_id\" IS NULL)" + }, + "session_runtime_provisioning_sandbox_pair_check": { + "name": "session_runtime_provisioning_sandbox_pair_check", + "value": "(\"session\".\"runtime_provisioning_sandbox_session_id\" IS NULL AND \"session\".\"runtime_provisioning_sandbox_incarnation\" IS NULL) OR (\"session\".\"runtime_provisioning_operation_id\" IS NOT NULL AND typeof(\"session\".\"runtime_provisioning_sandbox_incarnation\") = 'integer' AND \"session\".\"runtime_provisioning_sandbox_incarnation\" BETWEEN 0 AND 9007199254740991)" + }, + "session_status_check": { + "name": "session_status_check", + "value": "\"session\".\"status\" IN ('IDLE', 'RUNNING', 'RESCHEDULING', 'TERMINATED')" + }, + "session_auto_title_event_seq_check": { + "name": "session_auto_title_event_seq_check", + "value": "\"session\".\"auto_title_event_seq\" IS NULL OR \"session\".\"auto_title_event_seq\" >= 0" + }, + "session_status_seq_check": { + "name": "session_status_seq_check", + "value": "\"session\".\"status_seq\" >= 0" + } + } + }, + "session_execution_snapshot": { + "name": "session_execution_snapshot", + "columns": { + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "plan_json": { + "name": "plan_json", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "session_id": { + "name": "session_id", + "type": "text CHECK (\"session_id\" = upper(\"session_id\") AND length(\"session_id\") = 26 AND substr(\"session_id\", 1, 1) GLOB '[0-7]' AND \"session_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": true, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "session_execution_snapshot_session_id_session_id_fk": { + "name": "session_execution_snapshot_session_id_session_id_fk", + "tableFrom": "session_execution_snapshot", + "tableTo": "session", + "columnsFrom": ["session_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "session_run_skill": { + "name": "session_run_skill", + "columns": { + "blob_sha256": { + "name": "blob_sha256", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "materialization_status": { + "name": "materialization_status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "mount_path": { + "name": "mount_path", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "resolution_mode": { + "name": "resolution_mode", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "session_run_id": { + "name": "session_run_id", + "type": "text CHECK (\"session_run_id\" = upper(\"session_run_id\") AND length(\"session_run_id\") = 26 AND substr(\"session_run_id\", 1, 1) GLOB '[0-7]' AND \"session_run_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "skill_id": { + "name": "skill_id", + "type": "text CHECK (\"skill_id\" = upper(\"skill_id\") AND length(\"skill_id\") = 26 AND substr(\"skill_id\", 1, 1) GLOB '[0-7]' AND \"skill_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "skill_name": { + "name": "skill_name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "snapshot_id": { + "name": "snapshot_id", + "type": "text CHECK (\"snapshot_id\" = upper(\"snapshot_id\") AND length(\"snapshot_id\") = 26 AND substr(\"snapshot_id\", 1, 1) GLOB '[0-7]' AND \"snapshot_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "warning_code": { + "name": "warning_code", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "session_run_skill_run_resolution_idx": { + "name": "session_run_skill_run_resolution_idx", + "columns": ["session_run_id", "resolution_mode"], + "isUnique": false + } + }, + "foreignKeys": { + "session_run_skill_session_run_id_session_run_id_fk": { + "name": "session_run_skill_session_run_id_session_run_id_fk", + "tableFrom": "session_run_skill", + "tableTo": "session_run", + "columnsFrom": ["session_run_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "session_run_skill_session_run_id_skill_id_pk": { + "columns": ["session_run_id", "skill_id"], + "name": "session_run_skill_session_run_id_skill_id_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "session_run": { + "name": "session_run", + "columns": { + "agent_id": { + "name": "agent_id", + "type": "text CHECK (\"agent_id\" = upper(\"agent_id\") AND length(\"agent_id\") = 26 AND substr(\"agent_id\", 1, 1) GLOB '[0-7]' AND \"agent_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "bound_capability_agent_id": { + "name": "bound_capability_agent_id", + "type": "text CHECK (\"bound_capability_agent_id\" = upper(\"bound_capability_agent_id\") AND length(\"bound_capability_agent_id\") = 26 AND substr(\"bound_capability_agent_id\", 1, 1) GLOB '[0-7]' AND \"bound_capability_agent_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "bound_capability_app_id": { + "name": "bound_capability_app_id", + "type": "text CHECK (\"bound_capability_app_id\" = upper(\"bound_capability_app_id\") AND length(\"bound_capability_app_id\") = 26 AND substr(\"bound_capability_app_id\", 1, 1) GLOB '[0-7]' AND \"bound_capability_app_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "bound_capability_binding_env": { + "name": "bound_capability_binding_env", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "bound_capability_binding_name": { + "name": "bound_capability_binding_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "bound_capability_deployment_id": { + "name": "bound_capability_deployment_id", + "type": "text CHECK (\"bound_capability_deployment_id\" = upper(\"bound_capability_deployment_id\") AND length(\"bound_capability_deployment_id\") = 26 AND substr(\"bound_capability_deployment_id\", 1, 1) GLOB '[0-7]' AND \"bound_capability_deployment_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "bound_capability_deployment_run_id": { + "name": "bound_capability_deployment_run_id", + "type": "text CHECK (\"bound_capability_deployment_run_id\" = upper(\"bound_capability_deployment_run_id\") AND length(\"bound_capability_deployment_run_id\") = 26 AND substr(\"bound_capability_deployment_run_id\", 1, 1) GLOB '[0-7]' AND \"bound_capability_deployment_run_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "completed_at": { + "name": "completed_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_by_account_id": { + "name": "created_by_account_id", + "type": "text CHECK (\"created_by_account_id\" = upper(\"created_by_account_id\") AND length(\"created_by_account_id\") = 26 AND substr(\"created_by_account_id\", 1, 1) GLOB '[0-7]' AND \"created_by_account_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "deployment_version_id": { + "name": "deployment_version_id", + "type": "text CHECK (\"deployment_version_id\" = upper(\"deployment_version_id\") AND length(\"deployment_version_id\") = 26 AND substr(\"deployment_version_id\", 1, 1) GLOB '[0-7]' AND \"deployment_version_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "deployment_version_number": { + "name": "deployment_version_number", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "driver_instance_id": { + "name": "driver_instance_id", + "type": "text CHECK (\"driver_instance_id\" = upper(\"driver_instance_id\") AND length(\"driver_instance_id\") = 26 AND substr(\"driver_instance_id\", 1, 1) GLOB '[0-7]' AND \"driver_instance_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "error_code": { + "name": "error_code", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "error_details_json": { + "name": "error_details_json", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "error_retryable": { + "name": "error_retryable", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "id": { + "name": "id", + "type": "text CHECK (\"id\" = upper(\"id\") AND length(\"id\") = 26 AND substr(\"id\", 1, 1) GLOB '[0-7]' AND \"id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "runtime_id": { + "name": "runtime_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "session_id": { + "name": "session_id", + "type": "text CHECK (\"session_id\" = upper(\"session_id\") AND length(\"session_id\") = 26 AND substr(\"session_id\", 1, 1) GLOB '[0-7]' AND \"session_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "started_at": { + "name": "started_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status_changed_at": { + "name": "status_changed_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "status_event": { + "name": "status_event", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'run.queue'" + }, + "status_operation_id": { + "name": "status_operation_id", + "type": "text CHECK (\"status_operation_id\" = upper(\"status_operation_id\") AND length(\"status_operation_id\") = 26 AND substr(\"status_operation_id\", 1, 1) GLOB '[0-7]' AND \"status_operation_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status_seq": { + "name": "status_seq", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "status_source": { + "name": "status_source", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'system'" + }, + "terminal_reconciliation_attempted_at": { + "name": "terminal_reconciliation_attempted_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "trace_id": { + "name": "trace_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "trigger": { + "name": "trigger", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "session_run_driver_instance_idx": { + "name": "session_run_driver_instance_idx", + "columns": ["driver_instance_id", "created_at"], + "isUnique": false + }, + "session_run_active_driver_lease_idx": { + "name": "session_run_active_driver_lease_idx", + "columns": ["driver_instance_id"], + "isUnique": true, + "where": "\"session_run\".\"driver_instance_id\" IS NOT NULL AND \"session_run\".\"status\" IN ('queued', 'booting', 'running', 'waiting_input')" + }, + "session_run_session_created_at_idx": { + "name": "session_run_session_created_at_idx", + "columns": ["session_id", "created_at"], + "isUnique": false + }, + "session_run_session_status_idx": { + "name": "session_run_session_status_idx", + "columns": ["session_id", "status"], + "isUnique": false + }, + "session_run_terminal_reconciliation_attempt_idx": { + "name": "session_run_terminal_reconciliation_attempt_idx", + "columns": ["coalesce(\"terminal_reconciliation_attempted_at\", \"updated_at\")", "id"], + "isUnique": false + } + }, + "foreignKeys": { + "session_run_session_id_session_id_fk": { + "name": "session_run_session_id_session_id_fk", + "tableFrom": "session_run", + "tableTo": "session", + "columnsFrom": ["session_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": { + "session_run_error_retryable_check": { + "name": "session_run_error_retryable_check", + "value": "\"session_run\".\"error_retryable\" IS NULL OR (\"session_run\".\"error_retryable\" IN (false, true) AND \"session_run\".\"error_code\" IS NOT NULL AND \"session_run\".\"error_details_json\" IS NOT NULL AND \"session_run\".\"error_message\" IS NOT NULL)" + }, + "session_run_status_check": { + "name": "session_run_status_check", + "value": "\"session_run\".\"status\" IN ('queued', 'booting', 'running', 'waiting_input', 'completed', 'failed', 'cancelled', 'expired')" + }, + "session_run_status_seq_check": { + "name": "session_run_status_seq_check", + "value": "\"session_run\".\"status_seq\" >= 0" + }, + "session_run_terminal_reconciliation_attempted_at_check": { + "name": "session_run_terminal_reconciliation_attempted_at_check", + "value": "\"session_run\".\"terminal_reconciliation_attempted_at\" IS NULL OR \"session_run\".\"terminal_reconciliation_attempted_at\" >= 0" + } + } + }, + "session_agent_task_snapshot": { + "name": "session_agent_task_snapshot", + "columns": { + "driver_instance_id": { + "name": "driver_instance_id", + "type": "text CHECK (\"driver_instance_id\" = upper(\"driver_instance_id\") AND length(\"driver_instance_id\") = 26 AND substr(\"driver_instance_id\", 1, 1) GLOB '[0-7]' AND \"driver_instance_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "run_id": { + "name": "run_id", + "type": "text CHECK (\"run_id\" = upper(\"run_id\") AND length(\"run_id\") = 26 AND substr(\"run_id\", 1, 1) GLOB '[0-7]' AND \"run_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "seq": { + "name": "seq", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "session_id": { + "name": "session_id", + "type": "text CHECK (\"session_id\" = upper(\"session_id\") AND length(\"session_id\") = 26 AND substr(\"session_id\", 1, 1) GLOB '[0-7]' AND \"session_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tasks_json": { + "name": "tasks_json", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "session_agent_task_snapshot_run_id_session_run_id_fk": { + "name": "session_agent_task_snapshot_run_id_session_run_id_fk", + "tableFrom": "session_agent_task_snapshot", + "tableTo": "session_run", + "columnsFrom": ["run_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "session_agent_task_snapshot_session_id_session_id_fk": { + "name": "session_agent_task_snapshot_session_id_session_id_fk", + "tableFrom": "session_agent_task_snapshot", + "tableTo": "session", + "columnsFrom": ["session_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "session_event": { + "name": "session_event", + "columns": { + "agent_id": { + "name": "agent_id", + "type": "text CHECK (\"agent_id\" = upper(\"agent_id\") AND length(\"agent_id\") = 26 AND substr(\"agent_id\", 1, 1) GLOB '[0-7]' AND \"agent_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "artifact_attempt_id": { + "name": "artifact_attempt_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "artifact_manifest_json": { + "name": "artifact_manifest_json", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "artifact_manifest_sha256": { + "name": "artifact_manifest_sha256", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "content_text": { + "name": "content_text", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "ended_at": { + "name": "ended_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "event_type": { + "name": "event_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "family": { + "name": "family", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "id": { + "name": "id", + "type": "text CHECK (\"id\" = upper(\"id\") AND length(\"id\") = 26 AND substr(\"id\", 1, 1) GLOB '[0-7]' AND \"id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "mcp_command_id": { + "name": "mcp_command_id", + "type": "text CHECK (\"mcp_command_id\" = upper(\"mcp_command_id\") AND length(\"mcp_command_id\") = 26 AND substr(\"mcp_command_id\", 1, 1) GLOB '[0-7]' AND \"mcp_command_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "occurred_at": { + "name": "occurred_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "process_status": { + "name": "process_status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "process_type": { + "name": "process_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "run_id": { + "name": "run_id", + "type": "text CHECK (\"run_id\" = upper(\"run_id\") AND length(\"run_id\") = 26 AND substr(\"run_id\", 1, 1) GLOB '[0-7]' AND \"run_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "runtime_operation_event_json": { + "name": "runtime_operation_event_json", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "semantic_hash": { + "name": "semantic_hash", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "seq": { + "name": "seq", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "session_id": { + "name": "session_id", + "type": "text CHECK (\"session_id\" = upper(\"session_id\") AND length(\"session_id\") = 26 AND substr(\"session_id\", 1, 1) GLOB '[0-7]' AND \"session_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "source_event_id": { + "name": "source_event_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "stream_id": { + "name": "stream_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "terminal_event_json": { + "name": "terminal_event_json", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "tool_call_id": { + "name": "tool_call_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "tool_input_delta_json": { + "name": "tool_input_delta_json", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "tool_input_json": { + "name": "tool_input_json", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "tool_name": { + "name": "tool_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "tool_output_delta_text": { + "name": "tool_output_delta_text", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "tool_output_text": { + "name": "tool_output_text", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "tool_parent_message_id": { + "name": "tool_parent_message_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "tool_result_message_id": { + "name": "tool_result_message_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "tool_status": { + "name": "tool_status", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "tokens": { + "name": "tokens", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "trace_id": { + "name": "trace_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "visibility": { + "name": "visibility", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "session_event_agent_family_created_idx": { + "name": "session_event_agent_family_created_idx", + "columns": ["agent_id", "family", "created_at", "id"], + "isUnique": false + }, + "session_event_artifact_attempt_idx": { + "name": "session_event_artifact_attempt_idx", + "columns": ["artifact_attempt_id"], + "isUnique": true, + "where": "\"session_event\".\"artifact_attempt_id\" IS NOT NULL" + }, + "session_event_agent_visibility_created_idx": { + "name": "session_event_agent_visibility_created_idx", + "columns": ["agent_id", "visibility", "created_at", "id"], + "isUnique": false + }, + "session_event_agent_created_idx": { + "name": "session_event_agent_created_idx", + "columns": ["agent_id", "created_at", "id"], + "isUnique": false + }, + "session_event_session_visibility_seq_idx": { + "name": "session_event_session_visibility_seq_idx", + "columns": ["session_id", "visibility", "seq"], + "isUnique": false + }, + "session_event_run_event_type_idx": { + "name": "session_event_run_event_type_idx", + "columns": ["run_id", "event_type"], + "isUnique": false + }, + "session_event_run_stream_process_seq_idx": { + "name": "session_event_run_stream_process_seq_idx", + "columns": ["run_id", "stream_id", "process_type", "seq"], + "isUnique": false + }, + "session_event_run_tool_call_seq_idx": { + "name": "session_event_run_tool_call_seq_idx", + "columns": ["run_id", "tool_call_id", "seq"], + "isUnique": false + }, + "session_event_session_seq_idx": { + "name": "session_event_session_seq_idx", + "columns": ["session_id", "seq"], + "isUnique": true + }, + "session_event_session_source_idx": { + "name": "session_event_session_source_idx", + "columns": ["session_id", "source_event_id"], + "isUnique": true + }, + "session_event_run_terminal_winner_idx": { + "name": "session_event_run_terminal_winner_idx", + "columns": ["session_id", "run_id"], + "isUnique": true, + "where": "\"session_event\".\"semantic_hash\" IS NOT NULL AND \"session_event\".\"run_id\" IS NOT NULL AND \"session_event\".\"event_type\" IN ('run.cancelled', 'run.completed', 'run.failed')" + }, + "session_event_mcp_terminal_winner_idx": { + "name": "session_event_mcp_terminal_winner_idx", + "columns": ["session_id", "mcp_command_id"], + "isUnique": true, + "where": "\"session_event\".\"mcp_command_id\" IS NOT NULL" + } + }, + "foreignKeys": { + "session_event_session_id_session_id_fk": { + "name": "session_event_session_id_session_id_fk", + "tableFrom": "session_event", + "tableTo": "session", + "columnsFrom": ["session_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": { + "session_event_artifact_manifest_check": { + "name": "session_event_artifact_manifest_check", + "value": "(\"session_event\".\"artifact_attempt_id\" IS NULL AND \"session_event\".\"artifact_manifest_json\" IS NULL AND \"session_event\".\"artifact_manifest_sha256\" IS NULL) OR (\"session_event\".\"artifact_attempt_id\" IS NOT NULL AND \"session_event\".\"artifact_manifest_json\" IS NOT NULL AND json_valid(\"session_event\".\"artifact_manifest_json\") = 1 AND json_extract(\"session_event\".\"artifact_manifest_json\", '$.version') IS 1 AND json_type(\"session_event\".\"artifact_manifest_json\", '$.captureStatus') IS 'text' AND json_extract(\"session_event\".\"artifact_manifest_json\", '$.captureStatus') IN ('complete', 'omitted_file_limit', 'omitted_runtime_unavailable', 'omitted_size_limit', 'omitted_source_changed', 'omitted_source_missing') AND json_type(\"session_event\".\"artifact_manifest_json\", '$.mode') IS 'text' AND json_extract(\"session_event\".\"artifact_manifest_json\", '$.mode') IN ('delta', 'snapshot') AND (json_extract(\"session_event\".\"artifact_manifest_json\", '$.captureStatus') = 'complete' OR json_array_length(\"session_event\".\"artifact_manifest_json\", '$.files') = 0) AND json_extract(\"session_event\".\"artifact_manifest_json\", '$.sourceEventId') IS \"session_event\".\"source_event_id\" AND json_extract(\"session_event\".\"artifact_manifest_json\", '$.semanticHash') IS \"session_event\".\"semantic_hash\" AND json_type(\"session_event\".\"artifact_manifest_json\", '$.files') IS 'array' AND \"session_event\".\"artifact_manifest_sha256\" IS NOT NULL AND length(\"session_event\".\"artifact_manifest_sha256\") = 64 AND \"session_event\".\"artifact_manifest_sha256\" = lower(\"session_event\".\"artifact_manifest_sha256\") AND \"session_event\".\"artifact_manifest_sha256\" NOT GLOB '*[^0-9a-f]*' AND \"session_event\".\"semantic_hash\" IS NOT NULL AND \"session_event\".\"event_type\" IN ('file.change.updated', 'file.changed', 'run.completed'))" + }, + "session_event_mcp_command_check": { + "name": "session_event_mcp_command_check", + "value": "\"session_event\".\"mcp_command_id\" IS NULL OR (\"session_event\".\"event_type\" = 'tool.call.updated' AND \"session_event\".\"tool_status\" IS NOT NULL AND \"session_event\".\"tool_status\" IN ('completed', 'failed', 'cancelled'))" + }, + "session_event_runtime_operation_event_json_check": { + "name": "session_event_runtime_operation_event_json_check", + "value": "\"session_event\".\"runtime_operation_event_json\" IS NULL OR (json_valid(\"session_event\".\"runtime_operation_event_json\") = 1 AND json_extract(\"session_event\".\"runtime_operation_event_json\", '$.kind') IS 'agent.task.updated' AND json_type(\"session_event\".\"runtime_operation_event_json\", '$.payload') IS 'object' AND json_extract(\"session_event\".\"runtime_operation_event_json\", '$.payload.status') IN ('updating', 'ready') AND \"session_event\".\"semantic_hash\" IS NOT NULL AND \"session_event\".\"event_type\" = 'agent.task.updated')" + }, + "session_event_semantic_hash_check": { + "name": "session_event_semantic_hash_check", + "value": "\"session_event\".\"semantic_hash\" IS NULL OR (length(\"session_event\".\"semantic_hash\") = 64 AND \"session_event\".\"semantic_hash\" = lower(\"session_event\".\"semantic_hash\") AND \"session_event\".\"semantic_hash\" NOT GLOB '*[^0-9a-f]*')" + }, + "session_event_terminal_event_json_check": { + "name": "session_event_terminal_event_json_check", + "value": "(\"session_event\".\"terminal_event_json\" IS NULL AND NOT (\"session_event\".\"semantic_hash\" IS NOT NULL AND \"session_event\".\"event_type\" IN ('run.cancelled', 'run.completed', 'run.failed'))) OR (\"session_event\".\"terminal_event_json\" IS NOT NULL AND json_valid(\"session_event\".\"terminal_event_json\") = 1 AND \"session_event\".\"semantic_hash\" IS NOT NULL AND \"session_event\".\"event_type\" IN ('run.cancelled', 'run.completed', 'run.failed'))" + }, + "session_event_tool_input_kind_check": { + "name": "session_event_tool_input_kind_check", + "value": "\"session_event\".\"tool_input_delta_json\" IS NULL OR \"session_event\".\"tool_input_json\" IS NULL" + }, + "session_event_tool_output_kind_check": { + "name": "session_event_tool_output_kind_check", + "value": "\"session_event\".\"tool_output_delta_text\" IS NULL OR \"session_event\".\"tool_output_text\" IS NULL" + }, + "session_event_tool_status_check": { + "name": "session_event_tool_status_check", + "value": "\"session_event\".\"tool_status\" IS NULL OR \"session_event\".\"tool_status\" IN ('running', 'completed', 'failed', 'cancelled')" + } + } + }, + "session_model_call": { + "name": "session_model_call", + "columns": { + "cache_creation_tokens": { + "name": "cache_creation_tokens", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "cache_read_tokens": { + "name": "cache_read_tokens", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "call_key": { + "name": "call_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "completed_at": { + "name": "completed_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "cost_currency": { + "name": "cost_currency", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "driver_instance_id": { + "name": "driver_instance_id", + "type": "text CHECK (\"driver_instance_id\" = upper(\"driver_instance_id\") AND length(\"driver_instance_id\") = 26 AND substr(\"driver_instance_id\", 1, 1) GLOB '[0-7]' AND \"driver_instance_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "error_code": { + "name": "error_code", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "id": { + "name": "id", + "type": "text CHECK (\"id\" = upper(\"id\") AND length(\"id\") = 26 AND substr(\"id\", 1, 1) GLOB '[0-7]' AND \"id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "input_tokens": { + "name": "input_tokens", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "metadata_json": { + "name": "metadata_json", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "native_call_id": { + "name": "native_call_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "output_tokens": { + "name": "output_tokens", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "source_event_seq": { + "name": "source_event_seq", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "session_id": { + "name": "session_id", + "type": "text CHECK (\"session_id\" = upper(\"session_id\") AND length(\"session_id\") = 26 AND substr(\"session_id\", 1, 1) GLOB '[0-7]' AND \"session_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "session_run_id": { + "name": "session_run_id", + "type": "text CHECK (\"session_run_id\" = upper(\"session_run_id\") AND length(\"session_run_id\") = 26 AND substr(\"session_run_id\", 1, 1) GLOB '[0-7]' AND \"session_run_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "started_at": { + "name": "started_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "total_cost_usd_micros": { + "name": "total_cost_usd_micros", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "trace_id": { + "name": "trace_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "session_model_call_run_created_idx": { + "name": "session_model_call_run_created_idx", + "columns": ["session_run_id", "created_at"], + "isUnique": false + }, + "session_model_call_session_created_idx": { + "name": "session_model_call_session_created_idx", + "columns": ["session_id", "created_at"], + "isUnique": false + }, + "session_model_call_run_key_idx": { + "name": "session_model_call_run_key_idx", + "columns": ["session_run_id", "call_key"], + "isUnique": true + }, + "session_model_call_native_idx": { + "name": "session_model_call_native_idx", + "columns": ["driver_instance_id", "native_call_id"], + "isUnique": true + } + }, + "foreignKeys": { + "session_model_call_session_id_session_id_fk": { + "name": "session_model_call_session_id_session_id_fk", + "tableFrom": "session_model_call", + "tableTo": "session", + "columnsFrom": ["session_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "session_model_call_session_run_id_session_run_id_fk": { + "name": "session_model_call_session_run_id_session_run_id_fk", + "tableFrom": "session_model_call", + "tableTo": "session_run", + "columnsFrom": ["session_run_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": { + "session_model_call_source_event_seq_check": { + "name": "session_model_call_source_event_seq_check", + "value": "\"session_model_call\".\"source_event_seq\" >= 0" + } + } + }, + "session_permission_request": { + "name": "session_permission_request", + "columns": { + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "driver_instance_id": { + "name": "driver_instance_id", + "type": "text CHECK (\"driver_instance_id\" = upper(\"driver_instance_id\") AND length(\"driver_instance_id\") = 26 AND substr(\"driver_instance_id\", 1, 1) GLOB '[0-7]' AND \"driver_instance_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "raw_input": { + "name": "raw_input", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "request_id": { + "name": "request_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "run_id": { + "name": "run_id", + "type": "text CHECK (\"run_id\" = upper(\"run_id\") AND length(\"run_id\") = 26 AND substr(\"run_id\", 1, 1) GLOB '[0-7]' AND \"run_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "session_id": { + "name": "session_id", + "type": "text CHECK (\"session_id\" = upper(\"session_id\") AND length(\"session_id\") = 26 AND substr(\"session_id\", 1, 1) GLOB '[0-7]' AND \"session_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "tool_call_id": { + "name": "tool_call_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "tool_kind": { + "name": "tool_kind", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "session_permission_request_run_idx": { + "name": "session_permission_request_run_idx", + "columns": ["session_id", "run_id"], + "isUnique": false + } + }, + "foreignKeys": { + "session_permission_request_session_id_session_id_fk": { + "name": "session_permission_request_session_id_session_id_fk", + "tableFrom": "session_permission_request", + "tableTo": "session", + "columnsFrom": ["session_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "session_permission_request_session_id_request_id_pk": { + "columns": ["session_id", "request_id"], + "name": "session_permission_request_session_id_request_id_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "session_readiness_snapshot": { + "name": "session_readiness_snapshot", + "columns": { + "readiness_json": { + "name": "readiness_json", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "session_id": { + "name": "session_id", + "type": "text CHECK (\"session_id\" = upper(\"session_id\") AND length(\"session_id\") = 26 AND substr(\"session_id\", 1, 1) GLOB '[0-7]' AND \"session_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "session_readiness_snapshot_session_id_session_id_fk": { + "name": "session_readiness_snapshot_session_id_session_id_fk", + "tableFrom": "session_readiness_snapshot", + "tableTo": "session", + "columnsFrom": ["session_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "skill_snapshot_entry": { + "name": "skill_snapshot_entry", + "columns": { + "entry_kind": { + "name": "entry_kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "is_executable": { + "name": "is_executable", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "mime_type": { + "name": "mime_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "path": { + "name": "path", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "sha256": { + "name": "sha256", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "size": { + "name": "size", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "snapshot_id": { + "name": "snapshot_id", + "type": "text CHECK (\"snapshot_id\" = upper(\"snapshot_id\") AND length(\"snapshot_id\") = 26 AND substr(\"snapshot_id\", 1, 1) GLOB '[0-7]' AND \"snapshot_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "skill_snapshot_entry_snapshot_id_path_pk": { + "columns": ["snapshot_id", "path"], + "name": "skill_snapshot_entry_snapshot_id_path_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "skill_snapshot": { + "name": "skill_snapshot", + "columns": { + "author": { + "name": "author", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "blob_key": { + "name": "blob_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "blob_sha256": { + "name": "blob_sha256", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "blob_size": { + "name": "blob_size", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "id": { + "name": "id", + "type": "text CHECK (\"id\" = upper(\"id\") AND length(\"id\") = 26 AND substr(\"id\", 1, 1) GLOB '[0-7]' AND \"id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "app_id": { + "name": "app_id", + "type": "text CHECK (\"app_id\" = upper(\"app_id\") AND length(\"app_id\") = 26 AND substr(\"app_id\", 1, 1) GLOB '[0-7]' AND \"app_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "skill_markdown_path": { + "name": "skill_markdown_path", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "uncompressed_size": { + "name": "uncompressed_size", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "version": { + "name": "version", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "skill_snapshot_app_created_at_idx": { + "name": "skill_snapshot_app_created_at_idx", + "columns": ["app_id", "created_at"], + "isUnique": false + }, + "skill_snapshot_blob_sha256_idx": { + "name": "skill_snapshot_blob_sha256_idx", + "columns": ["app_id", "blob_sha256"], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "skill": { + "name": "skill", + "columns": { + "author": { + "name": "author", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "current_snapshot_id": { + "name": "current_snapshot_id", + "type": "text CHECK (\"current_snapshot_id\" = upper(\"current_snapshot_id\") AND length(\"current_snapshot_id\") = 26 AND substr(\"current_snapshot_id\", 1, 1) GLOB '[0-7]' AND \"current_snapshot_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "forked_from_owner_name": { + "name": "forked_from_owner_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "forked_from_skill_id": { + "name": "forked_from_skill_id", + "type": "text CHECK (\"forked_from_skill_id\" = upper(\"forked_from_skill_id\") AND length(\"forked_from_skill_id\") = 26 AND substr(\"forked_from_skill_id\", 1, 1) GLOB '[0-7]' AND \"forked_from_skill_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "forked_from_skill_name": { + "name": "forked_from_skill_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "id": { + "name": "id", + "type": "text CHECK (\"id\" = upper(\"id\") AND length(\"id\") = 26 AND substr(\"id\", 1, 1) GLOB '[0-7]' AND \"id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "owner_account_id": { + "name": "owner_account_id", + "type": "text CHECK (\"owner_account_id\" = upper(\"owner_account_id\") AND length(\"owner_account_id\") = 26 AND substr(\"owner_account_id\", 1, 1) GLOB '[0-7]' AND \"owner_account_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "app_id": { + "name": "app_id", + "type": "text CHECK (\"app_id\" = upper(\"app_id\") AND length(\"app_id\") = 26 AND substr(\"app_id\", 1, 1) GLOB '[0-7]' AND \"app_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "source_kind": { + "name": "source_kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "version": { + "name": "version", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "skill_app_updated_at_idx": { + "name": "skill_app_updated_at_idx", + "columns": ["app_id", "updated_at"], + "isUnique": false + }, + "skill_owner_account_updated_at_idx": { + "name": "skill_owner_account_updated_at_idx", + "columns": ["owner_account_id", "updated_at"], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "account": { + "name": "account", + "columns": { + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "email_verified": { + "name": "email_verified", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "id": { + "name": "id", + "type": "text CHECK (\"id\" = upper(\"id\") AND length(\"id\") = 26 AND substr(\"id\", 1, 1) GLOB '[0-7]' AND \"id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "image_url": { + "name": "image_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_active_organization_id": { + "name": "last_active_organization_id", + "type": "text CHECK (\"last_active_organization_id\" = upper(\"last_active_organization_id\") AND length(\"last_active_organization_id\") = 26 AND substr(\"last_active_organization_id\", 1, 1) GLOB '[0-7]' AND \"last_active_organization_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "system_agent_model": { + "name": "system_agent_model", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "account_email_idx": { + "name": "account_email_idx", + "columns": ["email"], + "isUnique": true + }, + "account_last_active_organization_idx": { + "name": "account_last_active_organization_idx", + "columns": ["last_active_organization_id"], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "usage_daily_rollup": { + "name": "usage_daily_rollup", + "columns": { + "actor_user_id": { + "name": "actor_user_id", + "type": "text CHECK (\"actor_user_id\" = upper(\"actor_user_id\") AND length(\"actor_user_id\") = 26 AND substr(\"actor_user_id\", 1, 1) GLOB '[0-7]' AND \"actor_user_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "agent_id": { + "name": "agent_id", + "type": "text CHECK (\"agent_id\" = upper(\"agent_id\") AND length(\"agent_id\") = 26 AND substr(\"agent_id\", 1, 1) GLOB '[0-7]' AND \"agent_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "agent_owner_user_id": { + "name": "agent_owner_user_id", + "type": "text CHECK (\"agent_owner_user_id\" = upper(\"agent_owner_user_id\") AND length(\"agent_owner_user_id\") = 26 AND substr(\"agent_owner_user_id\", 1, 1) GLOB '[0-7]' AND \"agent_owner_user_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "agent_publication_state_at_run": { + "name": "agent_publication_state_at_run", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "cache_creation_tokens": { + "name": "cache_creation_tokens", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "cache_read_tokens": { + "name": "cache_read_tokens", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "date": { + "name": "date", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "input_tokens": { + "name": "input_tokens", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "organization_id": { + "name": "organization_id", + "type": "text CHECK (\"organization_id\" = upper(\"organization_id\") AND length(\"organization_id\") = 26 AND substr(\"organization_id\", 1, 1) GLOB '[0-7]' AND \"organization_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "app_id": { + "name": "app_id", + "type": "text CHECK (\"app_id\" = upper(\"app_id\") AND length(\"app_id\") = 26 AND substr(\"app_id\", 1, 1) GLOB '[0-7]' AND \"app_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "output_tokens": { + "name": "output_tokens", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "request_count": { + "name": "request_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "run_purpose": { + "name": "run_purpose", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "total_cost_usd_micros": { + "name": "total_cost_usd_micros", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "unpriced_request_count": { + "name": "unpriced_request_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "usage_daily_rollup_app_date_idx": { + "name": "usage_daily_rollup_app_date_idx", + "columns": ["app_id", "date"], + "isUnique": false + }, + "usage_daily_rollup_organization_date_idx": { + "name": "usage_daily_rollup_organization_date_idx", + "columns": ["organization_id", "date"], + "isUnique": false + }, + "usage_daily_rollup_agent_date_idx": { + "name": "usage_daily_rollup_agent_date_idx", + "columns": ["agent_id", "date"], + "isUnique": false + }, + "usage_daily_rollup_actor_date_idx": { + "name": "usage_daily_rollup_actor_date_idx", + "columns": ["actor_user_id", "date"], + "isUnique": false + }, + "usage_daily_rollup_owner_date_idx": { + "name": "usage_daily_rollup_owner_date_idx", + "columns": ["agent_owner_user_id", "date"], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "usage_daily_rollup_organization_id_app_id_agent_id_actor_user_id_agent_owner_user_id_date_agent_publication_state_at_run_run_purpose_provider_model_pk": { + "columns": [ + "organization_id", + "app_id", + "agent_id", + "actor_user_id", + "agent_owner_user_id", + "date", + "agent_publication_state_at_run", + "run_purpose", + "provider", + "model" + ], + "name": "usage_daily_rollup_organization_id_app_id_agent_id_actor_user_id_agent_owner_user_id_date_agent_publication_state_at_run_run_purpose_provider_model_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "usage_event_rollup_receipt": { + "name": "usage_event_rollup_receipt", + "columns": { + "rolled_up_at": { + "name": "rolled_up_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "source_event_id": { + "name": "source_event_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "usage_event_rollup_receipt_rolled_up_at_idx": { + "name": "usage_event_rollup_receipt_rolled_up_at_idx", + "columns": ["rolled_up_at"], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "usage_event_rollup_receipt_source_source_event_id_pk": { + "columns": ["source", "source_event_id"], + "name": "usage_event_rollup_receipt_source_source_event_id_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "usage_event": { + "name": "usage_event", + "columns": { + "actor_user_id": { + "name": "actor_user_id", + "type": "text CHECK (\"actor_user_id\" = upper(\"actor_user_id\") AND length(\"actor_user_id\") = 26 AND substr(\"actor_user_id\", 1, 1) GLOB '[0-7]' AND \"actor_user_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "agent_id": { + "name": "agent_id", + "type": "text CHECK (\"agent_id\" = upper(\"agent_id\") AND length(\"agent_id\") = 26 AND substr(\"agent_id\", 1, 1) GLOB '[0-7]' AND \"agent_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "agent_owner_user_id": { + "name": "agent_owner_user_id", + "type": "text CHECK (\"agent_owner_user_id\" = upper(\"agent_owner_user_id\") AND length(\"agent_owner_user_id\") = 26 AND substr(\"agent_owner_user_id\", 1, 1) GLOB '[0-7]' AND \"agent_owner_user_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "agent_publication_state_at_run": { + "name": "agent_publication_state_at_run", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "agent_revision_id": { + "name": "agent_revision_id", + "type": "text CHECK (\"agent_revision_id\" = upper(\"agent_revision_id\") AND length(\"agent_revision_id\") = 26 AND substr(\"agent_revision_id\", 1, 1) GLOB '[0-7]' AND \"agent_revision_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "cache_creation_tokens": { + "name": "cache_creation_tokens", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "cache_read_tokens": { + "name": "cache_read_tokens", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "id": { + "name": "id", + "type": "text CHECK (\"id\" = upper(\"id\") AND length(\"id\") = 26 AND substr(\"id\", 1, 1) GLOB '[0-7]' AND \"id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "input_tokens": { + "name": "input_tokens", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "organization_id": { + "name": "organization_id", + "type": "text CHECK (\"organization_id\" = upper(\"organization_id\") AND length(\"organization_id\") = 26 AND substr(\"organization_id\", 1, 1) GLOB '[0-7]' AND \"organization_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "app_id": { + "name": "app_id", + "type": "text CHECK (\"app_id\" = upper(\"app_id\") AND length(\"app_id\") = 26 AND substr(\"app_id\", 1, 1) GLOB '[0-7]' AND \"app_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "output_tokens": { + "name": "output_tokens", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "price_snapshot_json": { + "name": "price_snapshot_json", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "pricing_status": { + "name": "pricing_status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "run_purpose": { + "name": "run_purpose", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "runtime_id": { + "name": "runtime_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "session_id": { + "name": "session_id", + "type": "text CHECK (\"session_id\" = upper(\"session_id\") AND length(\"session_id\") = 26 AND substr(\"session_id\", 1, 1) GLOB '[0-7]' AND \"session_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "session_run_id": { + "name": "session_run_id", + "type": "text CHECK (\"session_run_id\" = upper(\"session_run_id\") AND length(\"session_run_id\") = 26 AND substr(\"session_run_id\", 1, 1) GLOB '[0-7]' AND \"session_run_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "source_event_id": { + "name": "source_event_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "source_event_seq": { + "name": "source_event_seq", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "total_cost_usd_micros": { + "name": "total_cost_usd_micros", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "usage_contract": { + "name": "usage_contract", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "usage_event_app_created_idx": { + "name": "usage_event_app_created_idx", + "columns": ["app_id", "created_at"], + "isUnique": false + }, + "usage_event_organization_created_idx": { + "name": "usage_event_organization_created_idx", + "columns": ["organization_id", "created_at"], + "isUnique": false + }, + "usage_event_agent_created_idx": { + "name": "usage_event_agent_created_idx", + "columns": ["agent_id", "created_at"], + "isUnique": false + }, + "usage_event_actor_created_idx": { + "name": "usage_event_actor_created_idx", + "columns": ["actor_user_id", "created_at"], + "isUnique": false + }, + "usage_event_owner_created_idx": { + "name": "usage_event_owner_created_idx", + "columns": ["agent_owner_user_id", "created_at"], + "isUnique": false + }, + "usage_event_session_run_idx": { + "name": "usage_event_session_run_idx", + "columns": ["session_run_id"], + "isUnique": false + }, + "usage_event_source_event_idx": { + "name": "usage_event_source_event_idx", + "columns": ["source", "source_event_id"], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": { + "usage_event_source_event_seq_check": { + "name": "usage_event_source_event_seq_check", + "value": "\"usage_event\".\"source_event_seq\" >= 0" + } + } + }, + "vendor_credential": { + "name": "vendor_credential", + "columns": { + "api_base": { + "name": "api_base", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "api_key_secret_id": { + "name": "api_key_secret_id", + "type": "text CHECK (\"api_key_secret_id\" = upper(\"api_key_secret_id\") AND length(\"api_key_secret_id\") = 26 AND substr(\"api_key_secret_id\", 1, 1) GLOB '[0-7]' AND \"api_key_secret_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "id": { + "name": "id", + "type": "text CHECK (\"id\" = upper(\"id\") AND length(\"id\") = 26 AND substr(\"id\", 1, 1) GLOB '[0-7]' AND \"id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "is_default": { + "name": "is_default", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "models": { + "name": "models", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "app_id": { + "name": "app_id", + "type": "text CHECK (\"app_id\" = upper(\"app_id\") AND length(\"app_id\") = 26 AND substr(\"app_id\", 1, 1) GLOB '[0-7]' AND \"app_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "vendor_id": { + "name": "vendor_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "vendor_credential_app_vendor_idx": { + "name": "vendor_credential_app_vendor_idx", + "columns": ["app_id", "vendor_id"], + "isUnique": false + }, + "vendor_credential_app_vendor_name_idx": { + "name": "vendor_credential_app_vendor_name_idx", + "columns": ["app_id", "vendor_id", "name"], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + } + }, + "views": {}, + "enums": {}, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + }, + "internal": { + "indexes": { + "file_record_listing_idx": { + "columns": { + "lower(\"name\")": { + "isExpression": true + } + } + }, + "session_run_terminal_reconciliation_attempt_idx": { + "columns": { + "coalesce(\"terminal_reconciliation_attempted_at\", \"updated_at\")": { + "isExpression": true + } + } + } + } + } +} diff --git a/pkgs/db/drizzle/meta/0020_snapshot.json b/pkgs/db/drizzle/meta/0020_snapshot.json new file mode 100644 index 00000000..53127608 --- /dev/null +++ b/pkgs/db/drizzle/meta/0020_snapshot.json @@ -0,0 +1,8116 @@ +{ + "version": "6", + "dialect": "sqlite", + "id": "d67308d5-90a3-4ee5-9937-906c63fbc1e0", + "prevId": "9cf52036-2892-44fe-b0bc-4d45eef1d17f", + "tables": { + "agent_deployment_version": { + "name": "agent_deployment_version", + "columns": { + "agent_id": { + "name": "agent_id", + "type": "text CHECK (\"agent_id\" = upper(\"agent_id\") AND length(\"agent_id\") = 26 AND substr(\"agent_id\", 1, 1) GLOB '[0-7]' AND \"agent_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "config_json": { + "name": "config_json", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_by_account_id": { + "name": "created_by_account_id", + "type": "text CHECK (\"created_by_account_id\" = upper(\"created_by_account_id\") AND length(\"created_by_account_id\") = 26 AND substr(\"created_by_account_id\", 1, 1) GLOB '[0-7]' AND \"created_by_account_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "environment_id": { + "name": "environment_id", + "type": "text CHECK (\"environment_id\" = upper(\"environment_id\") AND length(\"environment_id\") = 26 AND substr(\"environment_id\", 1, 1) GLOB '[0-7]' AND \"environment_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "id": { + "name": "id", + "type": "text CHECK (\"id\" = upper(\"id\") AND length(\"id\") = 26 AND substr(\"id\", 1, 1) GLOB '[0-7]' AND \"id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "mcp_bindings_json": { + "name": "mcp_bindings_json", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "prompt": { + "name": "prompt", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "runtime_id": { + "name": "runtime_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "skills_json": { + "name": "skills_json", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "summary": { + "name": "summary", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "version_number": { + "name": "version_number", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "agent_deployment_version_agent_number_idx": { + "name": "agent_deployment_version_agent_number_idx", + "columns": ["agent_id", "version_number"], + "isUnique": true + }, + "agent_deployment_version_agent_created_idx": { + "name": "agent_deployment_version_agent_created_idx", + "columns": ["agent_id", "created_at"], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "agent_mcp_binding": { + "name": "agent_mcp_binding", + "columns": { + "agent_credential_id": { + "name": "agent_credential_id", + "type": "text CHECK (\"agent_credential_id\" = upper(\"agent_credential_id\") AND length(\"agent_credential_id\") = 26 AND substr(\"agent_credential_id\", 1, 1) GLOB '[0-7]' AND \"agent_credential_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "agent_id": { + "name": "agent_id", + "type": "text CHECK (\"agent_id\" = upper(\"agent_id\") AND length(\"agent_id\") = 26 AND substr(\"agent_id\", 1, 1) GLOB '[0-7]' AND \"agent_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "credential_mode": { + "name": "credential_mode", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'runtime_resolved'" + }, + "enabled": { + "name": "enabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "id": { + "name": "id", + "type": "text CHECK (\"id\" = upper(\"id\") AND length(\"id\") = 26 AND substr(\"id\", 1, 1) GLOB '[0-7]' AND \"id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "server_id": { + "name": "server_id", + "type": "text CHECK (\"server_id\" = upper(\"server_id\") AND length(\"server_id\") = 26 AND substr(\"server_id\", 1, 1) GLOB '[0-7]' AND \"server_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "agent_mcp_binding_agent_sort_idx": { + "name": "agent_mcp_binding_agent_sort_idx", + "columns": ["agent_id", "sort_order"], + "isUnique": true + }, + "agent_mcp_binding_server_idx": { + "name": "agent_mcp_binding_server_idx", + "columns": ["server_id"], + "isUnique": false + }, + "agent_mcp_binding_profile_server_idx": { + "name": "agent_mcp_binding_profile_server_idx", + "columns": ["agent_id", "server_id"], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": { + "agent_mcp_binding_agent_credential_shape_check": { + "name": "agent_mcp_binding_agent_credential_shape_check", + "value": "\n (\"agent_mcp_binding\".\"credential_mode\" = 'agent_bound' AND \"agent_mcp_binding\".\"agent_credential_id\" IS NOT NULL)\n OR (\"agent_mcp_binding\".\"credential_mode\" = 'runtime_resolved' AND \"agent_mcp_binding\".\"agent_credential_id\" IS NULL)\n " + } + } + }, + "agent_skill": { + "name": "agent_skill", + "columns": { + "agent_id": { + "name": "agent_id", + "type": "text CHECK (\"agent_id\" = upper(\"agent_id\") AND length(\"agent_id\") = 26 AND substr(\"agent_id\", 1, 1) GLOB '[0-7]' AND \"agent_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "skill_id": { + "name": "skill_id", + "type": "text CHECK (\"skill_id\" = upper(\"skill_id\") AND length(\"skill_id\") = 26 AND substr(\"skill_id\", 1, 1) GLOB '[0-7]' AND \"skill_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "agent_skill_agent_sort_idx": { + "name": "agent_skill_agent_sort_idx", + "columns": ["agent_id", "sort_order"], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "agent_skill_agent_id_skill_id_pk": { + "columns": ["agent_id", "skill_id"], + "name": "agent_skill_agent_id_skill_id_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "agent": { + "name": "agent", + "columns": { + "config_json": { + "name": "config_json", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "environment_id": { + "name": "environment_id", + "type": "text CHECK (\"environment_id\" = upper(\"environment_id\") AND length(\"environment_id\") = 26 AND substr(\"environment_id\", 1, 1) GLOB '[0-7]' AND \"environment_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "id": { + "name": "id", + "type": "text CHECK (\"id\" = upper(\"id\") AND length(\"id\") = 26 AND substr(\"id\", 1, 1) GLOB '[0-7]' AND \"id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'pet'" + }, + "live_deployment_version_id": { + "name": "live_deployment_version_id", + "type": "text CHECK (\"live_deployment_version_id\" = upper(\"live_deployment_version_id\") AND length(\"live_deployment_version_id\") = 26 AND substr(\"live_deployment_version_id\", 1, 1) GLOB '[0-7]' AND \"live_deployment_version_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "owner_account_id": { + "name": "owner_account_id", + "type": "text CHECK (\"owner_account_id\" = upper(\"owner_account_id\") AND length(\"owner_account_id\") = 26 AND substr(\"owner_account_id\", 1, 1) GLOB '[0-7]' AND \"owner_account_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "app_id": { + "name": "app_id", + "type": "text CHECK (\"app_id\" = upper(\"app_id\") AND length(\"app_id\") = 26 AND substr(\"app_id\", 1, 1) GLOB '[0-7]' AND \"app_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "prompt": { + "name": "prompt", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "runtime_id": { + "name": "runtime_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'draft'" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "visibility": { + "name": "visibility", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'private'" + } + }, + "indexes": { + "agent_app_owner_account_idx": { + "name": "agent_app_owner_account_idx", + "columns": ["app_id", "owner_account_id"], + "isUnique": false + }, + "agent_app_status_idx": { + "name": "agent_app_status_idx", + "columns": ["app_id", "status"], + "isUnique": false + }, + "agent_environment_idx": { + "name": "agent_environment_idx", + "columns": ["environment_id"], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": { + "agent_published_live_deployment_version_check": { + "name": "agent_published_live_deployment_version_check", + "value": "\"agent\".\"status\" <> 'published' OR \"agent\".\"live_deployment_version_id\" IS NOT NULL" + } + } + }, + "api_command": { + "name": "api_command", + "columns": { + "attempt_count": { + "name": "attempt_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "claim_expires_at": { + "name": "claim_expires_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "claim_owner": { + "name": "claim_owner", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "completed_at": { + "name": "completed_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "dedupe_key": { + "name": "dedupe_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "delivery_generation": { + "name": "delivery_generation", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 1 + }, + "id": { + "name": "id", + "type": "text CHECK (\"id\" = upper(\"id\") AND length(\"id\") = 26 AND substr(\"id\", 1, 1) GLOB '[0-7]' AND \"id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "last_error_code": { + "name": "last_error_code", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_error_message": { + "name": "last_error_message", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "payload_json": { + "name": "payload_json", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "api_command_dedupe_idx": { + "name": "api_command_dedupe_idx", + "columns": ["dedupe_key"], + "isUnique": true + }, + "api_command_status_updated_idx": { + "name": "api_command_status_updated_idx", + "columns": ["status", "updated_at"], + "isUnique": false + }, + "api_command_claim_idx": { + "name": "api_command_claim_idx", + "columns": ["status", "claim_expires_at"], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": { + "api_command_delivery_generation_check": { + "name": "api_command_delivery_generation_check", + "value": "typeof(\"api_command\".\"delivery_generation\") = 'integer' AND \"api_command\".\"delivery_generation\" BETWEEN 1 AND 9007199254740991" + } + } + }, + "auth_account": { + "name": "auth_account", + "columns": { + "access_token": { + "name": "access_token", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "access_token_expires_at": { + "name": "access_token_expires_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "provider_account_id": { + "name": "provider_account_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "id_token": { + "name": "id_token", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "refresh_token": { + "name": "refresh_token", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "refresh_token_expires_at": { + "name": "refresh_token_expires_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "account_id": { + "name": "account_id", + "type": "text CHECK (\"account_id\" = upper(\"account_id\") AND length(\"account_id\") = 26 AND substr(\"account_id\", 1, 1) GLOB '[0-7]' AND \"account_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "auth_account_provider_account_idx": { + "name": "auth_account_provider_account_idx", + "columns": ["provider_id", "provider_account_id"], + "isUnique": true + }, + "auth_account_account_id_idx": { + "name": "auth_account_account_id_idx", + "columns": ["account_id"], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "auth_session": { + "name": "auth_session", + "columns": { + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "ip_address": { + "name": "ip_address", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "account_id": { + "name": "account_id", + "type": "text CHECK (\"account_id\" = upper(\"account_id\") AND length(\"account_id\") = 26 AND substr(\"account_id\", 1, 1) GLOB '[0-7]' AND \"account_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "auth_session_expires_at_idx": { + "name": "auth_session_expires_at_idx", + "columns": ["expires_at"], + "isUnique": false + }, + "auth_session_token_idx": { + "name": "auth_session_token_idx", + "columns": ["token"], + "isUnique": true + }, + "auth_session_account_id_idx": { + "name": "auth_session_account_id_idx", + "columns": ["account_id"], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "auth_verification": { + "name": "auth_verification", + "columns": { + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "identifier": { + "name": "identifier", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "auth_verification_expires_at_idx": { + "name": "auth_verification_expires_at_idx", + "columns": ["expires_at"], + "isUnique": false + }, + "auth_verification_identifier_idx": { + "name": "auth_verification_identifier_idx", + "columns": ["identifier"], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "cli_oauth_flow": { + "name": "cli_oauth_flow", + "columns": { + "account_id": { + "name": "account_id", + "type": "text CHECK (\"account_id\" = upper(\"account_id\") AND length(\"account_id\") = 26 AND substr(\"account_id\", 1, 1) GLOB '[0-7]' AND \"account_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "authorized_at": { + "name": "authorized_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "completed_at": { + "name": "completed_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "device_code_hash": { + "name": "device_code_hash", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "hostname": { + "name": "hostname", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "id": { + "name": "id", + "type": "text CHECK (\"id\" = upper(\"id\") AND length(\"id\") = 26 AND substr(\"id\", 1, 1) GLOB '[0-7]' AND \"id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_code": { + "name": "user_code", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "cli_oauth_flow_status_expires_idx": { + "name": "cli_oauth_flow_status_expires_idx", + "columns": ["status", "expires_at"], + "isUnique": false + }, + "cli_oauth_flow_device_code_hash_idx": { + "name": "cli_oauth_flow_device_code_hash_idx", + "columns": ["device_code_hash"], + "isUnique": true + }, + "cli_oauth_flow_user_code_idx": { + "name": "cli_oauth_flow_user_code_idx", + "columns": ["user_code"], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "personal_access_token": { + "name": "personal_access_token", + "columns": { + "account_id": { + "name": "account_id", + "type": "text CHECK (\"account_id\" = upper(\"account_id\") AND length(\"account_id\") = 26 AND substr(\"account_id\", 1, 1) GLOB '[0-7]' AND \"account_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "id": { + "name": "id", + "type": "text CHECK (\"id\" = upper(\"id\") AND length(\"id\") = 26 AND substr(\"id\", 1, 1) GLOB '[0-7]' AND \"id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "last_used_at": { + "name": "last_used_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "revoked_at": { + "name": "revoked_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "token_hash": { + "name": "token_hash", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "personal_access_token_account_created_idx": { + "name": "personal_access_token_account_created_idx", + "columns": ["account_id", "created_at"], + "isUnique": false + }, + "personal_access_token_hash_idx": { + "name": "personal_access_token_hash_idx", + "columns": ["token_hash"], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "email_log": { + "name": "email_log", + "columns": { + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "recipient_domain": { + "name": "recipient_domain", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "recipient_masked": { + "name": "recipient_masked", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "subject": { + "name": "subject", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "email_log_created_at_idx": { + "name": "email_log_created_at_idx", + "columns": ["created_at"], + "isUnique": false + }, + "email_log_type_status_idx": { + "name": "email_log_type_status_idx", + "columns": ["type", "status"], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "environment_package_artifact_backup_staging": { + "name": "environment_package_artifact_backup_staging", + "columns": { + "actual_backup_id": { + "name": "actual_backup_id", + "type": "text CHECK (\"actual_backup_id\" = upper(\"actual_backup_id\") AND length(\"actual_backup_id\") = 26 AND substr(\"actual_backup_id\", 1, 1) GLOB '[0-7]' AND \"actual_backup_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "app_id": { + "name": "app_id", + "type": "text CHECK (\"app_id\" = upper(\"app_id\") AND length(\"app_id\") = 26 AND substr(\"app_id\", 1, 1) GLOB '[0-7]' AND \"app_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "attempt_count": { + "name": "attempt_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "claim_owner": { + "name": "claim_owner", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "command_id": { + "name": "command_id", + "type": "text CHECK (\"command_id\" = upper(\"command_id\") AND length(\"command_id\") = 26 AND substr(\"command_id\", 1, 1) GLOB '[0-7]' AND \"command_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "delivery_generation": { + "name": "delivery_generation", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "dir": { + "name": "dir", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "input_digest": { + "name": "input_digest", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "paths_json": { + "name": "paths_json", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "environment_package_artifact_backup_staging_actual_idx": { + "name": "environment_package_artifact_backup_staging_actual_idx", + "columns": ["actual_backup_id"], + "isUnique": true, + "where": "\"environment_package_artifact_backup_staging\".\"actual_backup_id\" IS NOT NULL" + }, + "environment_package_artifact_backup_staging_intent_idx": { + "name": "environment_package_artifact_backup_staging_intent_idx", + "columns": ["app_id", "input_digest"], + "isUnique": true + }, + "environment_package_artifact_backup_staging_updated_idx": { + "name": "environment_package_artifact_backup_staging_updated_idx", + "columns": ["updated_at", "command_id"], + "isUnique": false + } + }, + "foreignKeys": { + "environment_package_artifact_backup_staging_command_id_api_command_id_fk": { + "name": "environment_package_artifact_backup_staging_command_id_api_command_id_fk", + "tableFrom": "environment_package_artifact_backup_staging", + "tableTo": "api_command", + "columnsFrom": ["command_id"], + "columnsTo": ["id"], + "onDelete": "restrict", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": { + "environment_package_artifact_backup_staging_attempt_check": { + "name": "environment_package_artifact_backup_staging_attempt_check", + "value": "typeof(\"environment_package_artifact_backup_staging\".\"attempt_count\") = 'integer' AND \"environment_package_artifact_backup_staging\".\"attempt_count\" BETWEEN 1 AND 9007199254740991" + }, + "environment_package_artifact_backup_staging_claim_owner_check": { + "name": "environment_package_artifact_backup_staging_claim_owner_check", + "value": "typeof(\"environment_package_artifact_backup_staging\".\"claim_owner\") = 'text' AND length(\"environment_package_artifact_backup_staging\".\"claim_owner\") > 0" + }, + "environment_package_artifact_backup_staging_delivery_check": { + "name": "environment_package_artifact_backup_staging_delivery_check", + "value": "typeof(\"environment_package_artifact_backup_staging\".\"delivery_generation\") = 'integer' AND \"environment_package_artifact_backup_staging\".\"delivery_generation\" BETWEEN 1 AND 9007199254740991" + }, + "environment_package_artifact_backup_staging_digest_check": { + "name": "environment_package_artifact_backup_staging_digest_check", + "value": "length(\"environment_package_artifact_backup_staging\".\"input_digest\") = 64 AND \"environment_package_artifact_backup_staging\".\"input_digest\" = lower(\"environment_package_artifact_backup_staging\".\"input_digest\") AND \"environment_package_artifact_backup_staging\".\"input_digest\" NOT GLOB '*[^0-9a-f]*'" + }, + "environment_package_artifact_backup_staging_dir_check": { + "name": "environment_package_artifact_backup_staging_dir_check", + "value": "typeof(\"environment_package_artifact_backup_staging\".\"dir\") = 'text' AND length(\"environment_package_artifact_backup_staging\".\"dir\") > 0" + }, + "environment_package_artifact_backup_staging_paths_check": { + "name": "environment_package_artifact_backup_staging_paths_check", + "value": "json_valid(\"environment_package_artifact_backup_staging\".\"paths_json\") = 1 AND json_type(\"environment_package_artifact_backup_staging\".\"paths_json\") = 'object' AND json_type(\"environment_package_artifact_backup_staging\".\"paths_json\", '$.executable') = 'array' AND json_type(\"environment_package_artifact_backup_staging\".\"paths_json\", '$.node') = 'array' AND json_type(\"environment_package_artifact_backup_staging\".\"paths_json\", '$.python') = 'array'" + }, + "environment_package_artifact_backup_staging_time_check": { + "name": "environment_package_artifact_backup_staging_time_check", + "value": "typeof(\"environment_package_artifact_backup_staging\".\"created_at\") = 'integer' AND \"environment_package_artifact_backup_staging\".\"created_at\" BETWEEN 0 AND 9007199254740991 AND typeof(\"environment_package_artifact_backup_staging\".\"updated_at\") = 'integer' AND \"environment_package_artifact_backup_staging\".\"updated_at\" BETWEEN \"environment_package_artifact_backup_staging\".\"created_at\" AND 9007199254740991" + } + } + }, + "environment_package_artifact_backup": { + "name": "environment_package_artifact_backup", + "columns": { + "app_id": { + "name": "app_id", + "type": "text CHECK (\"app_id\" = upper(\"app_id\") AND length(\"app_id\") = 26 AND substr(\"app_id\", 1, 1) GLOB '[0-7]' AND \"app_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "attempt_count": { + "name": "attempt_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "backup_id": { + "name": "backup_id", + "type": "text CHECK (\"backup_id\" = upper(\"backup_id\") AND length(\"backup_id\") = 26 AND substr(\"backup_id\", 1, 1) GLOB '[0-7]' AND \"backup_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "command_id": { + "name": "command_id", + "type": "text CHECK (\"command_id\" = upper(\"command_id\") AND length(\"command_id\") = 26 AND substr(\"command_id\", 1, 1) GLOB '[0-7]' AND \"command_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "committed_at": { + "name": "committed_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "delivery_generation": { + "name": "delivery_generation", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "input_digest": { + "name": "input_digest", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "manifest_generation": { + "name": "manifest_generation", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "paths_json": { + "name": "paths_json", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "environment_package_artifact_backup_expiry_idx": { + "name": "environment_package_artifact_backup_expiry_idx", + "columns": ["expires_at", "backup_id"], + "isUnique": false + }, + "environment_package_artifact_backup_key_idx": { + "name": "environment_package_artifact_backup_key_idx", + "columns": ["app_id", "input_digest"], + "isUnique": true + } + }, + "foreignKeys": { + "environment_package_artifact_backup_command_id_api_command_id_fk": { + "name": "environment_package_artifact_backup_command_id_api_command_id_fk", + "tableFrom": "environment_package_artifact_backup", + "tableTo": "api_command", + "columnsFrom": ["command_id"], + "columnsTo": ["id"], + "onDelete": "restrict", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": { + "environment_package_artifact_backup_attempt_check": { + "name": "environment_package_artifact_backup_attempt_check", + "value": "typeof(\"environment_package_artifact_backup\".\"attempt_count\") = 'integer' AND \"environment_package_artifact_backup\".\"attempt_count\" BETWEEN 1 AND 9007199254740991" + }, + "environment_package_artifact_backup_delivery_check": { + "name": "environment_package_artifact_backup_delivery_check", + "value": "typeof(\"environment_package_artifact_backup\".\"delivery_generation\") = 'integer' AND \"environment_package_artifact_backup\".\"delivery_generation\" BETWEEN 1 AND 9007199254740991" + }, + "environment_package_artifact_backup_generation_check": { + "name": "environment_package_artifact_backup_generation_check", + "value": "typeof(\"environment_package_artifact_backup\".\"manifest_generation\") = 'integer' AND \"environment_package_artifact_backup\".\"manifest_generation\" BETWEEN 1 AND 9007199254740991" + }, + "environment_package_artifact_backup_digest_check": { + "name": "environment_package_artifact_backup_digest_check", + "value": "length(\"environment_package_artifact_backup\".\"input_digest\") = 64 AND \"environment_package_artifact_backup\".\"input_digest\" = lower(\"environment_package_artifact_backup\".\"input_digest\") AND \"environment_package_artifact_backup\".\"input_digest\" NOT GLOB '*[^0-9a-f]*'" + }, + "environment_package_artifact_backup_paths_check": { + "name": "environment_package_artifact_backup_paths_check", + "value": "json_valid(\"environment_package_artifact_backup\".\"paths_json\") = 1 AND json_type(\"environment_package_artifact_backup\".\"paths_json\") IS 'object' AND json_type(\"environment_package_artifact_backup\".\"paths_json\", '$.executable') IS 'array' AND json_type(\"environment_package_artifact_backup\".\"paths_json\", '$.node') IS 'array' AND json_type(\"environment_package_artifact_backup\".\"paths_json\", '$.python') IS 'array'" + }, + "environment_package_artifact_backup_time_check": { + "name": "environment_package_artifact_backup_time_check", + "value": "typeof(\"environment_package_artifact_backup\".\"committed_at\") = 'integer' AND \"environment_package_artifact_backup\".\"committed_at\" BETWEEN 0 AND 9007199254740991 AND typeof(\"environment_package_artifact_backup\".\"expires_at\") = 'integer' AND \"environment_package_artifact_backup\".\"expires_at\" BETWEEN \"environment_package_artifact_backup\".\"committed_at\" + 86400001 AND 9007199254740991" + } + } + }, + "environment_revision": { + "name": "environment_revision", + "columns": { + "allow_mcp_servers": { + "name": "allow_mcp_servers", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "allow_package_managers": { + "name": "allow_package_managers", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "allowed_hosts_json": { + "name": "allowed_hosts_json", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_by_account_id": { + "name": "created_by_account_id", + "type": "text CHECK (\"created_by_account_id\" = upper(\"created_by_account_id\") AND length(\"created_by_account_id\") = 26 AND substr(\"created_by_account_id\", 1, 1) GLOB '[0-7]' AND \"created_by_account_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "env_vars_json": { + "name": "env_vars_json", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "environment_id": { + "name": "environment_id", + "type": "text CHECK (\"environment_id\" = upper(\"environment_id\") AND length(\"environment_id\") = 26 AND substr(\"environment_id\", 1, 1) GLOB '[0-7]' AND \"environment_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "id": { + "name": "id", + "type": "text CHECK (\"id\" = upper(\"id\") AND length(\"id\") = 26 AND substr(\"id\", 1, 1) GLOB '[0-7]' AND \"id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "network_policy": { + "name": "network_policy", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "packages_json": { + "name": "packages_json", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "app_id": { + "name": "app_id", + "type": "text CHECK (\"app_id\" = upper(\"app_id\") AND length(\"app_id\") = 26 AND substr(\"app_id\", 1, 1) GLOB '[0-7]' AND \"app_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "setup_script": { + "name": "setup_script", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "environment_revision_environment_created_at_idx": { + "name": "environment_revision_environment_created_at_idx", + "columns": ["environment_id", "created_at"], + "isUnique": false + }, + "environment_revision_app_created_at_idx": { + "name": "environment_revision_app_created_at_idx", + "columns": ["app_id", "created_at"], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": { + "environment_revision_network_policy_check": { + "name": "environment_revision_network_policy_check", + "value": "\"environment_revision\".\"network_policy\" IN ('full', 'limited')" + } + } + }, + "environment": { + "name": "environment", + "columns": { + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "current_revision_id": { + "name": "current_revision_id", + "type": "text CHECK (\"current_revision_id\" = upper(\"current_revision_id\") AND length(\"current_revision_id\") = 26 AND substr(\"current_revision_id\", 1, 1) GLOB '[0-7]' AND \"current_revision_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "forked_from_environment_id": { + "name": "forked_from_environment_id", + "type": "text CHECK (\"forked_from_environment_id\" = upper(\"forked_from_environment_id\") AND length(\"forked_from_environment_id\") = 26 AND substr(\"forked_from_environment_id\", 1, 1) GLOB '[0-7]' AND \"forked_from_environment_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "forked_from_environment_name": { + "name": "forked_from_environment_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "forked_from_owner_name": { + "name": "forked_from_owner_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "id": { + "name": "id", + "type": "text CHECK (\"id\" = upper(\"id\") AND length(\"id\") = 26 AND substr(\"id\", 1, 1) GLOB '[0-7]' AND \"id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "owner_account_id": { + "name": "owner_account_id", + "type": "text CHECK (\"owner_account_id\" = upper(\"owner_account_id\") AND length(\"owner_account_id\") = 26 AND substr(\"owner_account_id\", 1, 1) GLOB '[0-7]' AND \"owner_account_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "app_id": { + "name": "app_id", + "type": "text CHECK (\"app_id\" = upper(\"app_id\") AND length(\"app_id\") = 26 AND substr(\"app_id\", 1, 1) GLOB '[0-7]' AND \"app_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "environment_app_updated_at_idx": { + "name": "environment_app_updated_at_idx", + "columns": ["app_id", "updated_at"], + "isUnique": false + }, + "environment_owner_updated_at_idx": { + "name": "environment_owner_updated_at_idx", + "columns": ["owner_account_id", "updated_at"], + "isUnique": false + }, + "environment_owner_name_idx": { + "name": "environment_owner_name_idx", + "columns": ["app_id", "owner_account_id", "name"], + "isUnique": true, + "where": "\"environment\".\"owner_account_id\" IS NOT NULL" + }, + "environment_system_default_idx": { + "name": "environment_system_default_idx", + "columns": ["app_id"], + "isUnique": true, + "where": "\"environment\".\"owner_account_id\" IS NULL" + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "file_record": { + "name": "file_record", + "columns": { + "committed": { + "name": "committed", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_by_account_id": { + "name": "created_by_account_id", + "type": "text CHECK (\"created_by_account_id\" = upper(\"created_by_account_id\") AND length(\"created_by_account_id\") = 26 AND substr(\"created_by_account_id\", 1, 1) GLOB '[0-7]' AND \"created_by_account_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "etag": { + "name": "etag", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "id": { + "name": "id", + "type": "text CHECK (\"id\" = upper(\"id\") AND length(\"id\") = 26 AND substr(\"id\", 1, 1) GLOB '[0-7]' AND \"id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "mime_type": { + "name": "mime_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "object_key": { + "name": "object_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "owner_id": { + "name": "owner_id", + "type": "text CHECK (\"owner_id\" = upper(\"owner_id\") AND length(\"owner_id\") = 26 AND substr(\"owner_id\", 1, 1) GLOB '[0-7]' AND \"owner_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "owner_kind": { + "name": "owner_kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "parent_path": { + "name": "parent_path", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "path": { + "name": "path", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "purpose": { + "name": "purpose", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "runtime_event_seq": { + "name": "runtime_event_seq", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "scope_id": { + "name": "scope_id", + "type": "text CHECK (\"scope_id\" = upper(\"scope_id\") AND length(\"scope_id\") = 26 AND substr(\"scope_id\", 1, 1) GLOB '[0-7]' AND \"scope_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "scope_kind": { + "name": "scope_kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "session_kind": { + "name": "session_kind", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "size": { + "name": "size", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "version": { + "name": "version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "file_record_runtime_event_seq_idx": { + "name": "file_record_runtime_event_seq_idx", + "columns": ["scope_id", "runtime_event_seq"], + "isUnique": false + }, + "file_record_object_key_idx": { + "name": "file_record_object_key_idx", + "columns": ["object_key"], + "isUnique": true + }, + "file_record_unscoped_parent_path_name_status_idx": { + "name": "file_record_unscoped_parent_path_name_status_idx", + "columns": ["scope_kind", "parent_path", "name", "status"], + "isUnique": true, + "where": "\"file_record\".\"scope_id\" IS NULL" + }, + "file_record_scoped_parent_path_name_status_idx": { + "name": "file_record_scoped_parent_path_name_status_idx", + "columns": ["scope_kind", "scope_id", "parent_path", "name", "status"], + "isUnique": true + }, + "file_record_unscoped_pending_path_idx": { + "name": "file_record_unscoped_pending_path_idx", + "columns": ["scope_kind", "path"], + "isUnique": true, + "where": "\"file_record\".\"status\" = 'pending' AND \"file_record\".\"scope_id\" IS NULL" + }, + "file_record_scoped_pending_path_idx": { + "name": "file_record_scoped_pending_path_idx", + "columns": ["scope_kind", "scope_id", "path"], + "isUnique": true, + "where": "\"file_record\".\"status\" = 'pending' AND \"file_record\".\"scope_id\" IS NOT NULL" + }, + "file_record_unscoped_ready_path_idx": { + "name": "file_record_unscoped_ready_path_idx", + "columns": ["scope_kind", "path"], + "isUnique": true, + "where": "\"file_record\".\"status\" = 'ready' AND \"file_record\".\"scope_id\" IS NULL" + }, + "file_record_scoped_ready_path_idx": { + "name": "file_record_scoped_ready_path_idx", + "columns": ["scope_kind", "scope_id", "path"], + "isUnique": true, + "where": "\"file_record\".\"status\" = 'ready' AND \"file_record\".\"scope_id\" IS NOT NULL" + }, + "file_record_governance_idx": { + "name": "file_record_governance_idx", + "columns": ["purpose", "owner_kind", "owner_id", "status", "expires_at"], + "isUnique": false + }, + "file_record_listing_idx": { + "name": "file_record_listing_idx", + "columns": ["scope_kind", "scope_id", "parent_path", "status", "lower(\"name\")"], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": { + "file_record_runtime_event_seq_check": { + "name": "file_record_runtime_event_seq_check", + "value": "\"file_record\".\"runtime_event_seq\" IS NULL OR \"file_record\".\"runtime_event_seq\" >= 0" + } + } + }, + "file_upload": { + "name": "file_upload", + "columns": { + "content_type": { + "name": "content_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_by_account_id": { + "name": "created_by_account_id", + "type": "text CHECK (\"created_by_account_id\" = upper(\"created_by_account_id\") AND length(\"created_by_account_id\") = 26 AND substr(\"created_by_account_id\", 1, 1) GLOB '[0-7]' AND \"created_by_account_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expected_size": { + "name": "expected_size", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "file_id": { + "name": "file_id", + "type": "text CHECK (\"file_id\" = upper(\"file_id\") AND length(\"file_id\") = 26 AND substr(\"file_id\", 1, 1) GLOB '[0-7]' AND \"file_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "id": { + "name": "id", + "type": "text CHECK (\"id\" = upper(\"id\") AND length(\"id\") = 26 AND substr(\"id\", 1, 1) GLOB '[0-7]' AND \"id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "if_match_etag": { + "name": "if_match_etag", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "multipart_upload_id": { + "name": "multipart_upload_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "overwrite": { + "name": "overwrite", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "part_size": { + "name": "part_size", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "scope_id": { + "name": "scope_id", + "type": "text CHECK (\"scope_id\" = upper(\"scope_id\") AND length(\"scope_id\") = 26 AND substr(\"scope_id\", 1, 1) GLOB '[0-7]' AND \"scope_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "scope_kind": { + "name": "scope_kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "strategy": { + "name": "strategy", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "file_upload_file_id_idx": { + "name": "file_upload_file_id_idx", + "columns": ["file_id"], + "isUnique": true + }, + "file_upload_status_expires_idx": { + "name": "file_upload_status_expires_idx", + "columns": ["status", "expires_at"], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "file_version": { + "name": "file_version", + "columns": { + "committed": { + "name": "committed", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "committed_at": { + "name": "committed_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_by_account_id": { + "name": "created_by_account_id", + "type": "text CHECK (\"created_by_account_id\" = upper(\"created_by_account_id\") AND length(\"created_by_account_id\") = 26 AND substr(\"created_by_account_id\", 1, 1) GLOB '[0-7]' AND \"created_by_account_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "file_id": { + "name": "file_id", + "type": "text CHECK (\"file_id\" = upper(\"file_id\") AND length(\"file_id\") = 26 AND substr(\"file_id\", 1, 1) GLOB '[0-7]' AND \"file_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "id": { + "name": "id", + "type": "text CHECK (\"id\" = upper(\"id\") AND length(\"id\") = 26 AND substr(\"id\", 1, 1) GLOB '[0-7]' AND \"id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "mime_type": { + "name": "mime_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "object_key": { + "name": "object_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "path": { + "name": "path", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "reason": { + "name": "reason", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "scope_id": { + "name": "scope_id", + "type": "text CHECK (\"scope_id\" = upper(\"scope_id\") AND length(\"scope_id\") = 26 AND substr(\"scope_id\", 1, 1) GLOB '[0-7]' AND \"scope_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "scope_kind": { + "name": "scope_kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "size": { + "name": "size", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "source_etag": { + "name": "source_etag", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "source_object_key": { + "name": "source_object_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "version": { + "name": "version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "file_version_object_key_idx": { + "name": "file_version_object_key_idx", + "columns": ["object_key"], + "isUnique": true + }, + "file_version_scope_path_created_idx": { + "name": "file_version_scope_path_created_idx", + "columns": ["scope_kind", "scope_id", "path", "created_at"], + "isUnique": false + }, + "file_version_file_created_idx": { + "name": "file_version_file_created_idx", + "columns": ["file_id", "created_at"], + "isUnique": false + }, + "file_version_pending_idx": { + "name": "file_version_pending_idx", + "columns": ["committed", "created_at"], + "isUnique": false, + "where": "\"file_version\".\"committed\" = 0" + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "runtime_artifact_attempt": { + "name": "runtime_artifact_attempt", + "columns": { + "accepted_event_id": { + "name": "accepted_event_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_by_account_id": { + "name": "created_by_account_id", + "type": "text CHECK (\"created_by_account_id\" = upper(\"created_by_account_id\") AND length(\"created_by_account_id\") = 26 AND substr(\"created_by_account_id\", 1, 1) GLOB '[0-7]' AND \"created_by_account_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "delete_after": { + "name": "delete_after", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "driver_connection_id": { + "name": "driver_connection_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "driver_generation": { + "name": "driver_generation", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "driver_instance_id": { + "name": "driver_instance_id", + "type": "text CHECK (\"driver_instance_id\" = upper(\"driver_instance_id\") AND length(\"driver_instance_id\") = 26 AND substr(\"driver_instance_id\", 1, 1) GLOB '[0-7]' AND \"driver_instance_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "event_type": { + "name": "event_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "manifest_json": { + "name": "manifest_json", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "manifest_sha256": { + "name": "manifest_sha256", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "owned_object_keys_json": { + "name": "owned_object_keys_json", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'[]'" + }, + "run_id": { + "name": "run_id", + "type": "text CHECK (\"run_id\" = upper(\"run_id\") AND length(\"run_id\") = 26 AND substr(\"run_id\", 1, 1) GLOB '[0-7]' AND \"run_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "semantic_hash": { + "name": "semantic_hash", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "session_id": { + "name": "session_id", + "type": "text CHECK (\"session_id\" = upper(\"session_id\") AND length(\"session_id\") = 26 AND substr(\"session_id\", 1, 1) GLOB '[0-7]' AND \"session_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "source_event_id": { + "name": "source_event_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "runtime_artifact_attempt_accepted_event_idx": { + "name": "runtime_artifact_attempt_accepted_event_idx", + "columns": ["accepted_event_id"], + "isUnique": true, + "where": "\"runtime_artifact_attempt\".\"accepted_event_id\" IS NOT NULL" + }, + "runtime_artifact_attempt_cleanup_idx": { + "name": "runtime_artifact_attempt_cleanup_idx", + "columns": ["status", "expires_at", "updated_at", "id"], + "isUnique": false + }, + "runtime_artifact_attempt_session_status_idx": { + "name": "runtime_artifact_attempt_session_status_idx", + "columns": ["session_id", "status", "id"], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": { + "runtime_artifact_attempt_manifest_check": { + "name": "runtime_artifact_attempt_manifest_check", + "value": "(\"runtime_artifact_attempt\".\"manifest_json\" IS NULL AND \"runtime_artifact_attempt\".\"manifest_sha256\" IS NULL) OR (\"runtime_artifact_attempt\".\"manifest_json\" IS NOT NULL AND json_valid(\"runtime_artifact_attempt\".\"manifest_json\") = 1 AND json_extract(\"runtime_artifact_attempt\".\"manifest_json\", '$.version') IS 1 AND json_type(\"runtime_artifact_attempt\".\"manifest_json\", '$.captureStatus') IS 'text' AND json_extract(\"runtime_artifact_attempt\".\"manifest_json\", '$.captureStatus') IN ('complete', 'omitted_file_limit', 'omitted_runtime_unavailable', 'omitted_size_limit', 'omitted_source_changed', 'omitted_source_missing') AND json_type(\"runtime_artifact_attempt\".\"manifest_json\", '$.mode') IS 'text' AND json_extract(\"runtime_artifact_attempt\".\"manifest_json\", '$.mode') IN ('delta', 'snapshot') AND (json_extract(\"runtime_artifact_attempt\".\"manifest_json\", '$.captureStatus') = 'complete' OR json_array_length(\"runtime_artifact_attempt\".\"manifest_json\", '$.files') = 0) AND json_extract(\"runtime_artifact_attempt\".\"manifest_json\", '$.sourceEventId') IS \"runtime_artifact_attempt\".\"source_event_id\" AND json_extract(\"runtime_artifact_attempt\".\"manifest_json\", '$.semanticHash') IS \"runtime_artifact_attempt\".\"semantic_hash\" AND json_type(\"runtime_artifact_attempt\".\"manifest_json\", '$.files') IS 'array' AND \"runtime_artifact_attempt\".\"manifest_sha256\" IS NOT NULL AND length(\"runtime_artifact_attempt\".\"manifest_sha256\") = 64 AND \"runtime_artifact_attempt\".\"manifest_sha256\" = lower(\"runtime_artifact_attempt\".\"manifest_sha256\") AND \"runtime_artifact_attempt\".\"manifest_sha256\" NOT GLOB '*[^0-9a-f]*')" + }, + "runtime_artifact_attempt_owned_keys_check": { + "name": "runtime_artifact_attempt_owned_keys_check", + "value": "json_valid(\"runtime_artifact_attempt\".\"owned_object_keys_json\") = 1 AND json_type(\"runtime_artifact_attempt\".\"owned_object_keys_json\") IS 'array'" + }, + "runtime_artifact_attempt_semantic_hash_check": { + "name": "runtime_artifact_attempt_semantic_hash_check", + "value": "length(\"runtime_artifact_attempt\".\"semantic_hash\") = 64 AND \"runtime_artifact_attempt\".\"semantic_hash\" = lower(\"runtime_artifact_attempt\".\"semantic_hash\") AND \"runtime_artifact_attempt\".\"semantic_hash\" NOT GLOB '*[^0-9a-f]*'" + }, + "runtime_artifact_attempt_status_check": { + "name": "runtime_artifact_attempt_status_check", + "value": "(\"runtime_artifact_attempt\".\"status\" = 'staging' AND \"runtime_artifact_attempt\".\"manifest_json\" IS NULL AND \"runtime_artifact_attempt\".\"accepted_event_id\" IS NULL AND \"runtime_artifact_attempt\".\"expires_at\" IS NOT NULL AND \"runtime_artifact_attempt\".\"delete_after\" IS NULL) OR (\"runtime_artifact_attempt\".\"status\" = 'staged' AND \"runtime_artifact_attempt\".\"manifest_json\" IS NOT NULL AND \"runtime_artifact_attempt\".\"accepted_event_id\" IS NULL AND \"runtime_artifact_attempt\".\"expires_at\" IS NOT NULL AND \"runtime_artifact_attempt\".\"delete_after\" IS NULL) OR (\"runtime_artifact_attempt\".\"status\" = 'accepted' AND \"runtime_artifact_attempt\".\"manifest_json\" IS NOT NULL AND \"runtime_artifact_attempt\".\"accepted_event_id\" IS NOT NULL AND \"runtime_artifact_attempt\".\"expires_at\" IS NULL AND \"runtime_artifact_attempt\".\"delete_after\" IS NULL AND json_array_length(\"runtime_artifact_attempt\".\"owned_object_keys_json\") = 0) OR (\"runtime_artifact_attempt\".\"status\" = 'deleting' AND \"runtime_artifact_attempt\".\"accepted_event_id\" IS NULL AND \"runtime_artifact_attempt\".\"delete_after\" IS NOT NULL)" + }, + "runtime_artifact_attempt_time_check": { + "name": "runtime_artifact_attempt_time_check", + "value": "\"runtime_artifact_attempt\".\"driver_generation\" >= 0 AND (\"runtime_artifact_attempt\".\"expires_at\" IS NULL OR \"runtime_artifact_attempt\".\"expires_at\" >= \"runtime_artifact_attempt\".\"created_at\") AND (\"runtime_artifact_attempt\".\"delete_after\" IS NULL OR \"runtime_artifact_attempt\".\"delete_after\" >= \"runtime_artifact_attempt\".\"created_at\") AND \"runtime_artifact_attempt\".\"updated_at\" >= \"runtime_artifact_attempt\".\"created_at\"" + } + } + }, + "session_artifact_head": { + "name": "session_artifact_head", + "columns": { + "file_id": { + "name": "file_id", + "type": "text CHECK (\"file_id\" = upper(\"file_id\") AND length(\"file_id\") = 26 AND substr(\"file_id\", 1, 1) GLOB '[0-7]' AND \"file_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "runtime_event_seq": { + "name": "runtime_event_seq", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "session_id": { + "name": "session_id", + "type": "text CHECK (\"session_id\" = upper(\"session_id\") AND length(\"session_id\") = 26 AND substr(\"session_id\", 1, 1) GLOB '[0-7]' AND \"session_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "source_event_id": { + "name": "source_event_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "source_path": { + "name": "source_path", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "session_artifact_head_session_path_idx": { + "name": "session_artifact_head_session_path_idx", + "columns": ["session_id", "source_path"], + "isUnique": true + }, + "session_artifact_head_session_seq_idx": { + "name": "session_artifact_head_session_seq_idx", + "columns": ["session_id", "runtime_event_seq", "source_path"], + "isUnique": false + } + }, + "foreignKeys": { + "session_artifact_head_session_id_session_id_fk": { + "name": "session_artifact_head_session_id_session_id_fk", + "tableFrom": "session_artifact_head", + "tableTo": "session", + "columnsFrom": ["session_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": { + "session_artifact_head_path_check": { + "name": "session_artifact_head_path_check", + "value": "length(\"session_artifact_head\".\"source_path\") > 8 AND substr(\"session_artifact_head\".\"source_path\", 1, 8) = 'outputs/' AND instr(\"session_artifact_head\".\"source_path\", char(0)) = 0 AND instr(\"session_artifact_head\".\"source_path\", '\\') = 0 AND \"session_artifact_head\".\"source_path\" NOT LIKE '%//%' AND \"session_artifact_head\".\"source_path\" NOT LIKE '%/./%' AND \"session_artifact_head\".\"source_path\" NOT LIKE '%/.' AND \"session_artifact_head\".\"source_path\" NOT LIKE '%/../%' AND \"session_artifact_head\".\"source_path\" NOT LIKE '%/..'" + }, + "session_artifact_head_seq_check": { + "name": "session_artifact_head_seq_check", + "value": "\"session_artifact_head\".\"runtime_event_seq\" >= 0 AND \"session_artifact_head\".\"updated_at\" >= 0" + } + } + }, + "mcp_credential": { + "name": "mcp_credential", + "columns": { + "account_id": { + "name": "account_id", + "type": "text CHECK (\"account_id\" = upper(\"account_id\") AND length(\"account_id\") = 26 AND substr(\"account_id\", 1, 1) GLOB '[0-7]' AND \"account_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "agent_id": { + "name": "agent_id", + "type": "text CHECK (\"agent_id\" = upper(\"agent_id\") AND length(\"agent_id\") = 26 AND substr(\"agent_id\", 1, 1) GLOB '[0-7]' AND \"agent_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "auth_type": { + "name": "auth_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "id": { + "name": "id", + "type": "text CHECK (\"id\" = upper(\"id\") AND length(\"id\") = 26 AND substr(\"id\", 1, 1) GLOB '[0-7]' AND \"id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "last_refreshed_at": { + "name": "last_refreshed_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "oauth_client_id": { + "name": "oauth_client_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "oauth_client_secret_secret_id": { + "name": "oauth_client_secret_secret_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "app_id": { + "name": "app_id", + "type": "text CHECK (\"app_id\" = upper(\"app_id\") AND length(\"app_id\") = 26 AND substr(\"app_id\", 1, 1) GLOB '[0-7]' AND \"app_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "refresh_secret_id": { + "name": "refresh_secret_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "scope_values_json": { + "name": "scope_values_json", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "secret_id": { + "name": "secret_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "server_id": { + "name": "server_id", + "type": "text CHECK (\"server_id\" = upper(\"server_id\") AND length(\"server_id\") = 26 AND substr(\"server_id\", 1, 1) GLOB '[0-7]' AND \"server_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "subject_label": { + "name": "subject_label", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "mcp_credential_server_scope_status_idx": { + "name": "mcp_credential_server_scope_status_idx", + "columns": ["server_id", "scope", "status"], + "isUnique": false + }, + "mcp_credential_app_scope_status_idx": { + "name": "mcp_credential_app_scope_status_idx", + "columns": ["app_id", "scope", "status"], + "isUnique": false + }, + "mcp_credential_app_scope_idx": { + "name": "mcp_credential_app_scope_idx", + "columns": ["server_id", "scope"], + "isUnique": true, + "where": "\"mcp_credential\".\"scope\" = 'app'" + }, + "mcp_credential_agent_scope_idx": { + "name": "mcp_credential_agent_scope_idx", + "columns": ["server_id", "agent_id", "scope"], + "isUnique": true, + "where": "\"mcp_credential\".\"scope\" = 'agent' AND \"mcp_credential\".\"agent_id\" IS NOT NULL" + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": { + "mcp_credential_scope_shape_check": { + "name": "mcp_credential_scope_shape_check", + "value": "\n (\"mcp_credential\".\"scope\" = 'app' AND \"mcp_credential\".\"account_id\" IS NULL AND \"mcp_credential\".\"agent_id\" IS NULL)\n OR (\"mcp_credential\".\"scope\" = 'agent' AND \"mcp_credential\".\"account_id\" IS NULL AND \"mcp_credential\".\"agent_id\" IS NOT NULL)\n " + }, + "mcp_credential_scope_values_json_check": { + "name": "mcp_credential_scope_values_json_check", + "value": "\n \"mcp_credential\".\"scope_values_json\" IS NULL\n OR (json_valid(\"mcp_credential\".\"scope_values_json\") AND json_type(\"mcp_credential\".\"scope_values_json\") = 'array')\n " + }, + "mcp_credential_bearer_shape_check": { + "name": "mcp_credential_bearer_shape_check", + "value": "\n \"mcp_credential\".\"auth_type\" != 'bearer'\n OR (\n \"mcp_credential\".\"oauth_client_id\" IS NULL\n AND \"mcp_credential\".\"oauth_client_secret_secret_id\" IS NULL\n AND \"mcp_credential\".\"refresh_secret_id\" IS NULL\n )\n " + } + } + }, + "mcp_oauth_flow": { + "name": "mcp_oauth_flow", + "columns": { + "authorization_endpoint": { + "name": "authorization_endpoint", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "cleanup_after": { + "name": "cleanup_after", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "code_verifier": { + "name": "code_verifier", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "completed_at": { + "name": "completed_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "id": { + "name": "id", + "type": "text CHECK (\"id\" = upper(\"id\") AND length(\"id\") = 26 AND substr(\"id\", 1, 1) GLOB '[0-7]' AND \"id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "initiator_account_id": { + "name": "initiator_account_id", + "type": "text CHECK (\"initiator_account_id\" = upper(\"initiator_account_id\") AND length(\"initiator_account_id\") = 26 AND substr(\"initiator_account_id\", 1, 1) GLOB '[0-7]' AND \"initiator_account_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "oauth_client_id": { + "name": "oauth_client_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "oauth_client_secret_secret_id": { + "name": "oauth_client_secret_secret_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "app_id": { + "name": "app_id", + "type": "text CHECK (\"app_id\" = upper(\"app_id\") AND length(\"app_id\") = 26 AND substr(\"app_id\", 1, 1) GLOB '[0-7]' AND \"app_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "registration_endpoint": { + "name": "registration_endpoint", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "return_url": { + "name": "return_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "scope_values_json": { + "name": "scope_values_json", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "server_id": { + "name": "server_id", + "type": "text CHECK (\"server_id\" = upper(\"server_id\") AND length(\"server_id\") = 26 AND substr(\"server_id\", 1, 1) GLOB '[0-7]' AND \"server_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "subject_label": { + "name": "subject_label", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "token_endpoint": { + "name": "token_endpoint", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "mcp_oauth_flow_status_cleanup_after_idx": { + "name": "mcp_oauth_flow_status_cleanup_after_idx", + "columns": ["status", "cleanup_after"], + "isUnique": false + }, + "mcp_oauth_flow_expires_at_idx": { + "name": "mcp_oauth_flow_expires_at_idx", + "columns": ["expires_at"], + "isUnique": false + }, + "mcp_oauth_flow_server_account_idx": { + "name": "mcp_oauth_flow_server_account_idx", + "columns": ["server_id", "initiator_account_id"], + "isUnique": false + }, + "mcp_oauth_flow_app_server_account_idx": { + "name": "mcp_oauth_flow_app_server_account_idx", + "columns": ["app_id", "server_id", "initiator_account_id"], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": { + "mcp_oauth_flow_scope_values_json_check": { + "name": "mcp_oauth_flow_scope_values_json_check", + "value": "\n \"mcp_oauth_flow\".\"scope_values_json\" IS NULL\n OR (json_valid(\"mcp_oauth_flow\".\"scope_values_json\") AND json_type(\"mcp_oauth_flow\".\"scope_values_json\") = 'array')\n " + } + } + }, + "mcp_server": { + "name": "mcp_server", + "columns": { + "auth_type": { + "name": "auth_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "byo_client_id": { + "name": "byo_client_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "byo_client_secret_secret_id": { + "name": "byo_client_secret_secret_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "credential_scope": { + "name": "credential_scope", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "enabled": { + "name": "enabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "icon_url": { + "name": "icon_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "id": { + "name": "id", + "type": "text CHECK (\"id\" = upper(\"id\") AND length(\"id\") = 26 AND substr(\"id\", 1, 1) GLOB '[0-7]' AND \"id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "oauth_metadata_json": { + "name": "oauth_metadata_json", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "owner_account_id": { + "name": "owner_account_id", + "type": "text CHECK (\"owner_account_id\" = upper(\"owner_account_id\") AND length(\"owner_account_id\") = 26 AND substr(\"owner_account_id\", 1, 1) GLOB '[0-7]' AND \"owner_account_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "app_id": { + "name": "app_id", + "type": "text CHECK (\"app_id\" = upper(\"app_id\") AND length(\"app_id\") = 26 AND substr(\"app_id\", 1, 1) GLOB '[0-7]' AND \"app_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "mcp_server_app_enabled_idx": { + "name": "mcp_server_app_enabled_idx", + "columns": ["app_id", "enabled"], + "isUnique": false + }, + "mcp_server_owner_app_idx": { + "name": "mcp_server_owner_app_idx", + "columns": ["owner_account_id", "app_id"], + "isUnique": false + }, + "mcp_server_app_url_idx": { + "name": "mcp_server_app_url_idx", + "columns": ["app_id", "url"], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": { + "mcp_server_source_scope_check": { + "name": "mcp_server_source_scope_check", + "value": "\"mcp_server\".\"source\" = 'app' AND \"mcp_server\".\"credential_scope\" = 'app'" + } + } + }, + "vault_secret": { + "name": "vault_secret", + "columns": { + "algorithm": { + "name": "algorithm", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'AES-GCM'" + }, + "ciphertext": { + "name": "ciphertext", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "ciphertext_iv": { + "name": "ciphertext_iv", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "id": { + "name": "id", + "type": "text CHECK (\"id\" = upper(\"id\") AND length(\"id\") = 26 AND substr(\"id\", 1, 1) GLOB '[0-7]' AND \"id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "wrapped_dek": { + "name": "wrapped_dek", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "wrapped_dek_iv": { + "name": "wrapped_dek_iv", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "vault_secret_kind_created_at_idx": { + "name": "vault_secret_kind_created_at_idx", + "columns": ["kind", "created_at"], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "organization": { + "name": "organization", + "columns": { + "avatar_url": { + "name": "avatar_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "creator_account_id": { + "name": "creator_account_id", + "type": "text CHECK (\"creator_account_id\" = upper(\"creator_account_id\") AND length(\"creator_account_id\") = 26 AND substr(\"creator_account_id\", 1, 1) GLOB '[0-7]' AND \"creator_account_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "id": { + "name": "id", + "type": "text CHECK (\"id\" = upper(\"id\") AND length(\"id\") = 26 AND substr(\"id\", 1, 1) GLOB '[0-7]' AND \"id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "organization_creator_account_idx": { + "name": "organization_creator_account_idx", + "columns": ["creator_account_id"], + "isUnique": true, + "where": "\"organization\".\"creator_account_id\" IS NOT NULL" + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "app_deployment_run": { + "name": "app_deployment_run", + "columns": { + "app_id": { + "name": "app_id", + "type": "text CHECK (\"app_id\" = upper(\"app_id\") AND length(\"app_id\") = 26 AND substr(\"app_id\", 1, 1) GLOB '[0-7]' AND \"app_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "deployment_id": { + "name": "deployment_id", + "type": "text CHECK (\"deployment_id\" = upper(\"deployment_id\") AND length(\"deployment_id\") = 26 AND substr(\"deployment_id\", 1, 1) GLOB '[0-7]' AND \"deployment_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "error_code": { + "name": "error_code", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "external_deployment_id": { + "name": "external_deployment_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "external_project_id": { + "name": "external_project_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "external_version_id": { + "name": "external_version_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "generated_wrangler_config_json": { + "name": "generated_wrangler_config_json", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "id": { + "name": "id", + "type": "text CHECK (\"id\" = upper(\"id\") AND length(\"id\") = 26 AND substr(\"id\", 1, 1) GLOB '[0-7]' AND \"id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "mosoo_config_json": { + "name": "mosoo_config_json", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "plan_json": { + "name": "plan_json", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "source_branch": { + "name": "source_branch", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "source_commit_sha": { + "name": "source_commit_sha", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "target_kind": { + "name": "target_kind", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "target_project_name": { + "name": "target_project_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "target_script_name": { + "name": "target_script_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "app_deployment_run_app_id_idx": { + "name": "app_deployment_run_app_id_idx", + "columns": ["app_id", "id"], + "isUnique": false + }, + "app_deployment_run_deployment_id_idx": { + "name": "app_deployment_run_deployment_id_idx", + "columns": ["deployment_id", "id"], + "isUnique": false + }, + "app_deployment_run_active_app_idx": { + "name": "app_deployment_run_active_app_idx", + "columns": ["app_id"], + "isUnique": true, + "where": "\"app_deployment_run\".\"status\" IN ('queued', 'preparing', 'building', 'submitting', 'submitted', 'activating')" + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": { + "app_deployment_run_status_check": { + "name": "app_deployment_run_status_check", + "value": "\"app_deployment_run\".\"status\" IN ('queued', 'preparing', 'building', 'submitting', 'submitted', 'activating', 'success', 'failed')" + }, + "app_deployment_run_target_kind_check": { + "name": "app_deployment_run_target_kind_check", + "value": "\"app_deployment_run\".\"target_kind\" IS NULL OR \"app_deployment_run\".\"target_kind\" IN ('cloudflare_static_assets', 'cloudflare_worker')" + } + } + }, + "app_deployment_script": { + "name": "app_deployment_script", + "columns": { + "attempt_count": { + "name": "attempt_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "command_id": { + "name": "command_id", + "type": "text CHECK (\"command_id\" = upper(\"command_id\") AND length(\"command_id\") = 26 AND substr(\"command_id\", 1, 1) GLOB '[0-7]' AND \"command_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "delivery_generation": { + "name": "delivery_generation", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "deployment_id": { + "name": "deployment_id", + "type": "text CHECK (\"deployment_id\" = upper(\"deployment_id\") AND length(\"deployment_id\") = 26 AND substr(\"deployment_id\", 1, 1) GLOB '[0-7]' AND \"deployment_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "external_deleted_at": { + "name": "external_deleted_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_reconciled_at": { + "name": "last_reconciled_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "next_reconcile_at": { + "name": "next_reconcile_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "reconcile_count": { + "name": "reconcile_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "reconcile_expires_at": { + "name": "reconcile_expires_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "reconcile_owner": { + "name": "reconcile_owner", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "registered_at": { + "name": "registered_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "registered_claim_owner": { + "name": "registered_claim_owner", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "retire_after": { + "name": "retire_after", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "run_id": { + "name": "run_id", + "type": "text CHECK (\"run_id\" = upper(\"run_id\") AND length(\"run_id\") = 26 AND substr(\"run_id\", 1, 1) GLOB '[0-7]' AND \"run_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "script_name": { + "name": "script_name", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "upload_started_at": { + "name": "upload_started_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "app_deployment_script_reconcile_idx": { + "name": "app_deployment_script_reconcile_idx", + "columns": ["next_reconcile_at", "script_name"], + "isUnique": false + } + }, + "foreignKeys": { + "app_deployment_script_command_id_api_command_id_fk": { + "name": "app_deployment_script_command_id_api_command_id_fk", + "tableFrom": "app_deployment_script", + "tableTo": "api_command", + "columnsFrom": ["command_id"], + "columnsTo": ["id"], + "onDelete": "restrict", + "onUpdate": "no action" + }, + "app_deployment_script_deployment_id_app_deployment_id_fk": { + "name": "app_deployment_script_deployment_id_app_deployment_id_fk", + "tableFrom": "app_deployment_script", + "tableTo": "app_deployment", + "columnsFrom": ["deployment_id"], + "columnsTo": ["id"], + "onDelete": "restrict", + "onUpdate": "no action" + }, + "app_deployment_script_run_id_app_deployment_run_id_fk": { + "name": "app_deployment_script_run_id_app_deployment_run_id_fk", + "tableFrom": "app_deployment_script", + "tableTo": "app_deployment_run", + "columnsFrom": ["run_id"], + "columnsTo": ["id"], + "onDelete": "restrict", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": { + "app_deployment_script_attempt_check": { + "name": "app_deployment_script_attempt_check", + "value": "typeof(\"app_deployment_script\".\"attempt_count\") = 'integer' AND \"app_deployment_script\".\"attempt_count\" BETWEEN 1 AND 9007199254740991" + }, + "app_deployment_script_delivery_check": { + "name": "app_deployment_script_delivery_check", + "value": "typeof(\"app_deployment_script\".\"delivery_generation\") = 'integer' AND \"app_deployment_script\".\"delivery_generation\" BETWEEN 1 AND 9007199254740991" + }, + "app_deployment_script_name_check": { + "name": "app_deployment_script_name_check", + "value": "typeof(\"app_deployment_script\".\"script_name\") = 'text' AND length(\"app_deployment_script\".\"script_name\") BETWEEN 36 AND 63 AND substr(\"app_deployment_script\".\"script_name\", 1, 4) = 'app-' AND \"app_deployment_script\".\"script_name\" NOT GLOB '*[^0-9a-z-]*' AND substr(\"app_deployment_script\".\"script_name\", -1, 1) GLOB '[0-9a-z]'" + }, + "app_deployment_script_registered_owner_check": { + "name": "app_deployment_script_registered_owner_check", + "value": "typeof(\"app_deployment_script\".\"registered_claim_owner\") = 'text' AND length(\"app_deployment_script\".\"registered_claim_owner\") > 0" + }, + "app_deployment_script_reconcile_count_check": { + "name": "app_deployment_script_reconcile_count_check", + "value": "typeof(\"app_deployment_script\".\"reconcile_count\") = 'integer' AND \"app_deployment_script\".\"reconcile_count\" BETWEEN 0 AND 9007199254740991" + }, + "app_deployment_script_reconcile_lease_check": { + "name": "app_deployment_script_reconcile_lease_check", + "value": "(\"app_deployment_script\".\"reconcile_owner\" IS NULL AND \"app_deployment_script\".\"reconcile_expires_at\" IS NULL) OR (typeof(\"app_deployment_script\".\"reconcile_owner\") = 'text' AND length(\"app_deployment_script\".\"reconcile_owner\") > 0 AND typeof(\"app_deployment_script\".\"reconcile_expires_at\") = 'integer' AND \"app_deployment_script\".\"reconcile_expires_at\" BETWEEN 0 AND 9007199254740991)" + }, + "app_deployment_script_time_check": { + "name": "app_deployment_script_time_check", + "value": "typeof(\"app_deployment_script\".\"registered_at\") = 'integer' AND \"app_deployment_script\".\"registered_at\" BETWEEN 0 AND 9007199254740991 AND (\"app_deployment_script\".\"upload_started_at\" IS NULL OR (typeof(\"app_deployment_script\".\"upload_started_at\") = 'integer' AND \"app_deployment_script\".\"upload_started_at\" BETWEEN \"app_deployment_script\".\"registered_at\" AND 9007199254740991)) AND (\"app_deployment_script\".\"retire_after\" IS NULL OR (typeof(\"app_deployment_script\".\"retire_after\") = 'integer' AND \"app_deployment_script\".\"retire_after\" BETWEEN \"app_deployment_script\".\"registered_at\" AND 9007199254740991)) AND (\"app_deployment_script\".\"next_reconcile_at\" IS NULL OR (typeof(\"app_deployment_script\".\"next_reconcile_at\") = 'integer' AND \"app_deployment_script\".\"next_reconcile_at\" BETWEEN \"app_deployment_script\".\"registered_at\" AND 9007199254740991)) AND (\"app_deployment_script\".\"last_reconciled_at\" IS NULL OR (typeof(\"app_deployment_script\".\"last_reconciled_at\") = 'integer' AND \"app_deployment_script\".\"last_reconciled_at\" BETWEEN \"app_deployment_script\".\"registered_at\" AND 9007199254740991)) AND (\"app_deployment_script\".\"external_deleted_at\" IS NULL OR (\"app_deployment_script\".\"retire_after\" IS NOT NULL AND typeof(\"app_deployment_script\".\"external_deleted_at\") = 'integer' AND \"app_deployment_script\".\"external_deleted_at\" BETWEEN \"app_deployment_script\".\"registered_at\" AND 9007199254740991))" + } + } + }, + "app_deployment_secret": { + "name": "app_deployment_secret", + "columns": { + "app_id": { + "name": "app_id", + "type": "text CHECK (\"app_id\" = upper(\"app_id\") AND length(\"app_id\") = 26 AND substr(\"app_id\", 1, 1) GLOB '[0-7]' AND \"app_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "vault_secret_id": { + "name": "vault_secret_id", + "type": "text CHECK (\"vault_secret_id\" = upper(\"vault_secret_id\") AND length(\"vault_secret_id\") = 26 AND substr(\"vault_secret_id\", 1, 1) GLOB '[0-7]' AND \"vault_secret_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "app_deployment_secret_app_name_idx": { + "name": "app_deployment_secret_app_name_idx", + "columns": ["app_id", "name"], + "isUnique": true + }, + "app_deployment_secret_vault_secret_idx": { + "name": "app_deployment_secret_vault_secret_idx", + "columns": ["vault_secret_id"], + "isUnique": true + } + }, + "foreignKeys": { + "app_deployment_secret_vault_secret_id_vault_secret_id_fk": { + "name": "app_deployment_secret_vault_secret_id_vault_secret_id_fk", + "tableFrom": "app_deployment_secret", + "tableTo": "vault_secret", + "columnsFrom": ["vault_secret_id"], + "columnsTo": ["id"], + "onDelete": "restrict", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "app_deployment": { + "name": "app_deployment", + "columns": { + "active_script_name": { + "name": "active_script_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "app_id": { + "name": "app_id", + "type": "text CHECK (\"app_id\" = upper(\"app_id\") AND length(\"app_id\") = 26 AND substr(\"app_id\", 1, 1) GLOB '[0-7]' AND \"app_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "default_branch": { + "name": "default_branch", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "deleted_at": { + "name": "deleted_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "id": { + "name": "id", + "type": "text CHECK (\"id\" = upper(\"id\") AND length(\"id\") = 26 AND substr(\"id\", 1, 1) GLOB '[0-7]' AND \"id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "last_successful_url": { + "name": "last_successful_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "latest_run_id": { + "name": "latest_run_id", + "type": "text CHECK (\"latest_run_id\" = upper(\"latest_run_id\") AND length(\"latest_run_id\") = 26 AND substr(\"latest_run_id\", 1, 1) GLOB '[0-7]' AND \"latest_run_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "mosoo_subdomain": { + "name": "mosoo_subdomain", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "owner_account_id": { + "name": "owner_account_id", + "type": "text CHECK (\"owner_account_id\" = upper(\"owner_account_id\") AND length(\"owner_account_id\") = 26 AND substr(\"owner_account_id\", 1, 1) GLOB '[0-7]' AND \"owner_account_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "repo_name": { + "name": "repo_name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "repo_owner": { + "name": "repo_owner", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "repo_url": { + "name": "repo_url", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "source_kind": { + "name": "source_kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "app_deployment_active_app_idx": { + "name": "app_deployment_active_app_idx", + "columns": ["app_id"], + "isUnique": true, + "where": "\"app_deployment\".\"deleted_at\" IS NULL" + }, + "app_deployment_active_subdomain_idx": { + "name": "app_deployment_active_subdomain_idx", + "columns": ["mosoo_subdomain"], + "isUnique": true, + "where": "\"app_deployment\".\"deleted_at\" IS NULL" + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": { + "app_deployment_traffic_authority_check": { + "name": "app_deployment_traffic_authority_check", + "value": "(\"app_deployment\".\"active_script_name\" IS NULL AND \"app_deployment\".\"last_successful_url\" IS NULL) OR (\"app_deployment\".\"deleted_at\" IS NULL AND typeof(\"app_deployment\".\"active_script_name\") = 'text' AND length(\"app_deployment\".\"active_script_name\") > 0 AND typeof(\"app_deployment\".\"last_successful_url\") = 'text' AND length(\"app_deployment\".\"last_successful_url\") > 0)" + }, + "app_deployment_source_kind_check": { + "name": "app_deployment_source_kind_check", + "value": "\"app_deployment\".\"source_kind\" IN ('github_public')" + } + } + }, + "app": { + "name": "app", + "columns": { + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "default_environment_id": { + "name": "default_environment_id", + "type": "text CHECK (\"default_environment_id\" = upper(\"default_environment_id\") AND length(\"default_environment_id\") = 26 AND substr(\"default_environment_id\", 1, 1) GLOB '[0-7]' AND \"default_environment_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "id": { + "name": "id", + "type": "text CHECK (\"id\" = upper(\"id\") AND length(\"id\") = 26 AND substr(\"id\", 1, 1) GLOB '[0-7]' AND \"id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "organization_id": { + "name": "organization_id", + "type": "text CHECK (\"organization_id\" = upper(\"organization_id\") AND length(\"organization_id\") = 26 AND substr(\"organization_id\", 1, 1) GLOB '[0-7]' AND \"organization_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "owner_account_id": { + "name": "owner_account_id", + "type": "text CHECK (\"owner_account_id\" = upper(\"owner_account_id\") AND length(\"owner_account_id\") = 26 AND substr(\"owner_account_id\", 1, 1) GLOB '[0-7]' AND \"owner_account_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "bound_agent_call_idempotency_key": { + "name": "bound_agent_call_idempotency_key", + "columns": { + "body_hash": { + "name": "body_hash", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "id": { + "name": "id", + "type": "text CHECK (\"id\" = upper(\"id\") AND length(\"id\") = 26 AND substr(\"id\", 1, 1) GLOB '[0-7]' AND \"id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "idempotency_key": { + "name": "idempotency_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "run_id": { + "name": "run_id", + "type": "text CHECK (\"run_id\" = upper(\"run_id\") AND length(\"run_id\") = 26 AND substr(\"run_id\", 1, 1) GLOB '[0-7]' AND \"run_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "session_id": { + "name": "session_id", + "type": "text CHECK (\"session_id\" = upper(\"session_id\") AND length(\"session_id\") = 26 AND substr(\"session_id\", 1, 1) GLOB '[0-7]' AND \"session_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "subject_hash": { + "name": "subject_hash", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "bound_agent_call_idempotency_subject_key_idx": { + "name": "bound_agent_call_idempotency_subject_key_idx", + "columns": ["subject_hash", "idempotency_key"], + "isUnique": true + }, + "bound_agent_call_idempotency_updated_idx": { + "name": "bound_agent_call_idempotency_updated_idx", + "columns": ["updated_at"], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "public_api_idempotency_key": { + "name": "public_api_idempotency_key", + "columns": { + "body_hash": { + "name": "body_hash", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "id": { + "name": "id", + "type": "text CHECK (\"id\" = upper(\"id\") AND length(\"id\") = 26 AND substr(\"id\", 1, 1) GLOB '[0-7]' AND \"id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "idempotency_key": { + "name": "idempotency_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "method": { + "name": "method", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "response_json": { + "name": "response_json", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "response_status": { + "name": "response_status", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "route": { + "name": "route", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "token_id": { + "name": "token_id", + "type": "text CHECK (\"token_id\" = upper(\"token_id\") AND length(\"token_id\") = 26 AND substr(\"token_id\", 1, 1) GLOB '[0-7]' AND \"token_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "public_api_idempotency_token_key_idx": { + "name": "public_api_idempotency_token_key_idx", + "columns": ["token_id", "idempotency_key"], + "isUnique": true + }, + "public_api_idempotency_updated_idx": { + "name": "public_api_idempotency_updated_idx", + "columns": ["updated_at"], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "public_api_rate_limit_window": { + "name": "public_api_rate_limit_window", + "columns": { + "bucket_key": { + "name": "bucket_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "request_count": { + "name": "request_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "shard": { + "name": "shard", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "window_start": { + "name": "window_start", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "public_api_rate_limit_window_updated_idx": { + "name": "public_api_rate_limit_window_updated_idx", + "columns": ["updated_at"], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "public_api_rate_limit_window_bucket_key_window_start_shard_pk": { + "columns": ["bucket_key", "window_start", "shard"], + "name": "public_api_rate_limit_window_bucket_key_window_start_shard_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "driver_command": { + "name": "driver_command", + "columns": { + "acked_at": { + "name": "acked_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "completed_at": { + "name": "completed_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "delivery_connection_id": { + "name": "delivery_connection_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "driver_generation": { + "name": "driver_generation", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "driver_instance_id": { + "name": "driver_instance_id", + "type": "text CHECK (\"driver_instance_id\" = upper(\"driver_instance_id\") AND length(\"driver_instance_id\") = 26 AND substr(\"driver_instance_id\", 1, 1) GLOB '[0-7]' AND \"driver_instance_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "error_json": { + "name": "error_json", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "id": { + "name": "id", + "type": "text CHECK (\"id\" = upper(\"id\") AND length(\"id\") = 26 AND substr(\"id\", 1, 1) GLOB '[0-7]' AND \"id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "issued_at": { + "name": "issued_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "payload_json": { + "name": "payload_json", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "result_json": { + "name": "result_json", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "seq": { + "name": "seq", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "driver_command_instance_seq_idx": { + "name": "driver_command_instance_seq_idx", + "columns": ["driver_instance_id", "seq"], + "isUnique": true + }, + "driver_command_instance_status_idx": { + "name": "driver_command_instance_status_idx", + "columns": ["driver_instance_id", "status", "expires_at"], + "isUnique": false + } + }, + "foreignKeys": { + "driver_command_driver_instance_id_driver_instance_id_fk": { + "name": "driver_command_driver_instance_id_driver_instance_id_fk", + "tableFrom": "driver_command", + "tableTo": "driver_instance", + "columnsFrom": ["driver_instance_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": { + "driver_command_generation_check": { + "name": "driver_command_generation_check", + "value": "\"driver_command\".\"driver_generation\" IS NULL OR (typeof(\"driver_command\".\"driver_generation\") = 'integer' AND \"driver_command\".\"driver_generation\" BETWEEN 0 AND 9007199254740991)" + }, + "driver_command_nonterminal_generation_check": { + "name": "driver_command_nonterminal_generation_check", + "value": "\"driver_command\".\"status\" IN ('completed', 'failed', 'expired', 'cancelled') OR \"driver_command\".\"driver_generation\" IS NOT NULL" + } + } + }, + "driver_instance_mcp_grant": { + "name": "driver_instance_mcp_grant", + "columns": { + "auth_type": { + "name": "auth_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "authorization_state": { + "name": "authorization_state", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "can_invalidate": { + "name": "can_invalidate", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "can_refresh": { + "name": "can_refresh", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "credential_id": { + "name": "credential_id", + "type": "text CHECK (\"credential_id\" = upper(\"credential_id\") AND length(\"credential_id\") = 26 AND substr(\"credential_id\", 1, 1) GLOB '[0-7]' AND \"credential_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "driver_instance_id": { + "name": "driver_instance_id", + "type": "text CHECK (\"driver_instance_id\" = upper(\"driver_instance_id\") AND length(\"driver_instance_id\") = 26 AND substr(\"driver_instance_id\", 1, 1) GLOB '[0-7]' AND \"driver_instance_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "app_id": { + "name": "app_id", + "type": "text CHECK (\"app_id\" = upper(\"app_id\") AND length(\"app_id\") = 26 AND substr(\"app_id\", 1, 1) GLOB '[0-7]' AND \"app_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "server_id": { + "name": "server_id", + "type": "text CHECK (\"server_id\" = upper(\"server_id\") AND length(\"server_id\") = 26 AND substr(\"server_id\", 1, 1) GLOB '[0-7]' AND \"server_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "driver_instance_mcp_grant_instance_server_idx": { + "name": "driver_instance_mcp_grant_instance_server_idx", + "columns": ["driver_instance_id", "server_id"], + "isUnique": true + }, + "driver_instance_mcp_grant_instance_credential_idx": { + "name": "driver_instance_mcp_grant_instance_credential_idx", + "columns": ["driver_instance_id", "credential_id"], + "isUnique": false + } + }, + "foreignKeys": { + "driver_instance_mcp_grant_driver_instance_id_driver_instance_id_fk": { + "name": "driver_instance_mcp_grant_driver_instance_id_driver_instance_id_fk", + "tableFrom": "driver_instance_mcp_grant", + "tableTo": "driver_instance", + "columnsFrom": ["driver_instance_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "driver_instance": { + "name": "driver_instance", + "columns": { + "boot_token_expires_at": { + "name": "boot_token_expires_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "boot_token_hash": { + "name": "boot_token_hash", + "type": "blob", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "boot_token_used_at": { + "name": "boot_token_used_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "close_code": { + "name": "close_code", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "close_reason": { + "name": "close_reason", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "connection_id": { + "name": "connection_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "command_seq_cursor": { + "name": "command_seq_cursor", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "driver_pid": { + "name": "driver_pid", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "driver_started_at": { + "name": "driver_started_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "driver_version": { + "name": "driver_version", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "heartbeat_count": { + "name": "heartbeat_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "generation": { + "name": "generation", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "id": { + "name": "id", + "type": "text CHECK (\"id\" = upper(\"id\") AND length(\"id\") = 26 AND substr(\"id\", 1, 1) GLOB '[0-7]' AND \"id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "last_heartbeat_at": { + "name": "last_heartbeat_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "process_id": { + "name": "process_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "protocol": { + "name": "protocol", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "protocol_version": { + "name": "protocol_version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "restart_count": { + "name": "restart_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "runtime": { + "name": "runtime", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "sandbox_id": { + "name": "sandbox_id", + "type": "text CHECK (\"sandbox_id\" = upper(\"sandbox_id\") AND length(\"sandbox_id\") = 26 AND substr(\"sandbox_id\", 1, 1) GLOB '[0-7]' AND \"sandbox_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "sandbox_incarnation": { + "name": "sandbox_incarnation", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "sandbox_session_id": { + "name": "sandbox_session_id", + "type": "text CHECK (\"sandbox_session_id\" = upper(\"sandbox_session_id\") AND length(\"sandbox_session_id\") = 26 AND substr(\"sandbox_session_id\", 1, 1) GLOB '[0-7]' AND \"sandbox_session_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status_changed_at": { + "name": "status_changed_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "status_event": { + "name": "status_event", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'driver.provision'" + }, + "status_operation_id": { + "name": "status_operation_id", + "type": "text CHECK (\"status_operation_id\" = upper(\"status_operation_id\") AND length(\"status_operation_id\") = 26 AND substr(\"status_operation_id\", 1, 1) GLOB '[0-7]' AND \"status_operation_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status_seq": { + "name": "status_seq", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "status_source": { + "name": "status_source", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'system'" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "driver_instance_completed_idx": { + "name": "driver_instance_completed_idx", + "columns": ["expires_at", "status"], + "isUnique": false + }, + "driver_instance_connection_idx": { + "name": "driver_instance_connection_idx", + "columns": ["connection_id"], + "isUnique": true, + "where": "\"driver_instance\".\"connection_id\" IS NOT NULL" + }, + "driver_instance_boot_token_expiry_idx": { + "name": "driver_instance_boot_token_expiry_idx", + "columns": ["status", "boot_token_expires_at"], + "isUnique": false, + "where": "\"driver_instance\".\"boot_token_used_at\" IS NULL" + }, + "driver_instance_boot_token_hash_idx": { + "name": "driver_instance_boot_token_hash_idx", + "columns": ["boot_token_hash"], + "isUnique": true + }, + "driver_instance_sandbox_session_idx": { + "name": "driver_instance_sandbox_session_idx", + "columns": [ + "sandbox_id", + "sandbox_incarnation", + "sandbox_session_id", + "status", + "updated_at" + ], + "isUnique": false + }, + "driver_instance_live_sandbox_session_idx": { + "name": "driver_instance_live_sandbox_session_idx", + "columns": ["sandbox_id", "sandbox_incarnation", "sandbox_session_id"], + "isUnique": true, + "where": "\"driver_instance\".\"status\" IN ('provisioning', 'connecting', 'ready', 'stopping')" + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": { + "driver_instance_status_check": { + "name": "driver_instance_status_check", + "value": "\"driver_instance\".\"status\" IN ('provisioning', 'connecting', 'ready', 'stopping', 'stopped', 'failed')" + }, + "driver_instance_status_seq_check": { + "name": "driver_instance_status_seq_check", + "value": "\"driver_instance\".\"status_seq\" >= 0" + }, + "driver_instance_generation_incarnation_check": { + "name": "driver_instance_generation_incarnation_check", + "value": "typeof(\"driver_instance\".\"generation\") = 'integer' AND \"driver_instance\".\"generation\" BETWEEN 0 AND 9007199254740991 AND typeof(\"driver_instance\".\"sandbox_incarnation\") = 'integer' AND \"driver_instance\".\"sandbox_incarnation\" BETWEEN 0 AND 9007199254740991 AND (\"driver_instance\".\"status\" IN ('stopped', 'failed') OR \"driver_instance\".\"sandbox_incarnation\" > 0)" + } + } + }, + "external_tool_effect_attempt": { + "name": "external_tool_effect_attempt", + "columns": { + "attempt": { + "name": "attempt", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "claim_token": { + "name": "claim_token", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "completed_at": { + "name": "completed_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "effect_id": { + "name": "effect_id", + "type": "text CHECK (\"effect_id\" = upper(\"effect_id\") AND length(\"effect_id\") = 26 AND substr(\"effect_id\", 1, 1) GLOB '[0-7]' AND \"effect_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "provider_receipt_json": { + "name": "provider_receipt_json", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "result_json": { + "name": "result_json", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "external_tool_effect_attempt_status_idx": { + "name": "external_tool_effect_attempt_status_idx", + "columns": ["status", "created_at"], + "isUnique": false + } + }, + "foreignKeys": { + "external_tool_effect_attempt_effect_id_external_tool_effect_id_fk": { + "name": "external_tool_effect_attempt_effect_id_external_tool_effect_id_fk", + "tableFrom": "external_tool_effect_attempt", + "tableTo": "external_tool_effect", + "columnsFrom": ["effect_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "external_tool_effect_attempt_effect_id_attempt_pk": { + "columns": ["effect_id", "attempt"], + "name": "external_tool_effect_attempt_effect_id_attempt_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": { + "external_tool_effect_attempt_status_check": { + "name": "external_tool_effect_attempt_status_check", + "value": "\"external_tool_effect_attempt\".\"status\" IN ('claimed', 'succeeded', 'unknown')" + }, + "external_tool_effect_attempt_claim_token_uuid_check": { + "name": "external_tool_effect_attempt_claim_token_uuid_check", + "value": "length(\"external_tool_effect_attempt\".\"claim_token\") = 36 AND length(replace(\"external_tool_effect_attempt\".\"claim_token\", '-', '')) = 32 AND \"external_tool_effect_attempt\".\"claim_token\" = lower(\"external_tool_effect_attempt\".\"claim_token\") AND substr(\"external_tool_effect_attempt\".\"claim_token\", 9, 1) = '-' AND substr(\"external_tool_effect_attempt\".\"claim_token\", 14, 1) = '-' AND substr(\"external_tool_effect_attempt\".\"claim_token\", 15, 1) = '4' AND substr(\"external_tool_effect_attempt\".\"claim_token\", 19, 1) = '-' AND substr(\"external_tool_effect_attempt\".\"claim_token\", 20, 1) GLOB '[89ab]' AND substr(\"external_tool_effect_attempt\".\"claim_token\", 24, 1) = '-' AND replace(\"external_tool_effect_attempt\".\"claim_token\", '-', '') NOT GLOB '*[^0-9a-f]*'" + } + } + }, + "external_tool_effect": { + "name": "external_tool_effect", + "columns": { + "attempt_count": { + "name": "attempt_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "claim_token": { + "name": "claim_token", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "command_id": { + "name": "command_id", + "type": "text CHECK (\"command_id\" = upper(\"command_id\") AND length(\"command_id\") = 26 AND substr(\"command_id\", 1, 1) GLOB '[0-7]' AND \"command_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "driver_instance_id": { + "name": "driver_instance_id", + "type": "text CHECK (\"driver_instance_id\" = upper(\"driver_instance_id\") AND length(\"driver_instance_id\") = 26 AND substr(\"driver_instance_id\", 1, 1) GLOB '[0-7]' AND \"driver_instance_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "id": { + "name": "id", + "type": "text CHECK (\"id\" = upper(\"id\") AND length(\"id\") = 26 AND substr(\"id\", 1, 1) GLOB '[0-7]' AND \"id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "idempotency_key": { + "name": "idempotency_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "provider_receipt_json": { + "name": "provider_receipt_json", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "result_json": { + "name": "result_json", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "server_id": { + "name": "server_id", + "type": "text CHECK (\"server_id\" = upper(\"server_id\") AND length(\"server_id\") = 26 AND substr(\"server_id\", 1, 1) GLOB '[0-7]' AND \"server_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "session_run_id": { + "name": "session_run_id", + "type": "text CHECK (\"session_run_id\" = upper(\"session_run_id\") AND length(\"session_run_id\") = 26 AND substr(\"session_run_id\", 1, 1) GLOB '[0-7]' AND \"session_run_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "tool_name": { + "name": "tool_name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "external_tool_effect_command_idx": { + "name": "external_tool_effect_command_idx", + "columns": ["command_id"], + "isUnique": true + }, + "external_tool_effect_idempotency_key_idx": { + "name": "external_tool_effect_idempotency_key_idx", + "columns": ["idempotency_key"], + "isUnique": true + }, + "external_tool_effect_run_status_idx": { + "name": "external_tool_effect_run_status_idx", + "columns": ["session_run_id", "status", "id"], + "isUnique": false + }, + "external_tool_effect_driver_status_idx": { + "name": "external_tool_effect_driver_status_idx", + "columns": ["driver_instance_id", "status"], + "isUnique": false + } + }, + "foreignKeys": { + "external_tool_effect_command_id_driver_command_id_fk": { + "name": "external_tool_effect_command_id_driver_command_id_fk", + "tableFrom": "external_tool_effect", + "tableTo": "driver_command", + "columnsFrom": ["command_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "external_tool_effect_driver_instance_id_driver_instance_id_fk": { + "name": "external_tool_effect_driver_instance_id_driver_instance_id_fk", + "tableFrom": "external_tool_effect", + "tableTo": "driver_instance", + "columnsFrom": ["driver_instance_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "external_tool_effect_session_run_id_session_run_id_fk": { + "name": "external_tool_effect_session_run_id_session_run_id_fk", + "tableFrom": "external_tool_effect", + "tableTo": "session_run", + "columnsFrom": ["session_run_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": { + "external_tool_effect_status_check": { + "name": "external_tool_effect_status_check", + "value": "\"external_tool_effect\".\"status\" IN ('intent', 'claimed', 'succeeded', 'unknown')" + }, + "external_tool_effect_claim_token_uuid_check": { + "name": "external_tool_effect_claim_token_uuid_check", + "value": "\"external_tool_effect\".\"claim_token\" IS NULL OR (length(\"external_tool_effect\".\"claim_token\") = 36 AND length(replace(\"external_tool_effect\".\"claim_token\", '-', '')) = 32 AND \"external_tool_effect\".\"claim_token\" = lower(\"external_tool_effect\".\"claim_token\") AND substr(\"external_tool_effect\".\"claim_token\", 9, 1) = '-' AND substr(\"external_tool_effect\".\"claim_token\", 14, 1) = '-' AND substr(\"external_tool_effect\".\"claim_token\", 15, 1) = '4' AND substr(\"external_tool_effect\".\"claim_token\", 19, 1) = '-' AND substr(\"external_tool_effect\".\"claim_token\", 20, 1) GLOB '[89ab]' AND substr(\"external_tool_effect\".\"claim_token\", 24, 1) = '-' AND replace(\"external_tool_effect\".\"claim_token\", '-', '') NOT GLOB '*[^0-9a-f]*')" + } + } + }, + "native_resume_ref": { + "name": "native_resume_ref", + "columns": { + "committed_session_run_id": { + "name": "committed_session_run_id", + "type": "text CHECK (\"committed_session_run_id\" = upper(\"committed_session_run_id\") AND length(\"committed_session_run_id\") = 26 AND substr(\"committed_session_run_id\", 1, 1) GLOB '[0-7]' AND \"committed_session_run_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "committed_value": { + "name": "committed_value", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "observed_driver_instance_id": { + "name": "observed_driver_instance_id", + "type": "text CHECK (\"observed_driver_instance_id\" = upper(\"observed_driver_instance_id\") AND length(\"observed_driver_instance_id\") = 26 AND substr(\"observed_driver_instance_id\", 1, 1) GLOB '[0-7]' AND \"observed_driver_instance_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "observed_event_seq": { + "name": "observed_event_seq", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "observed_session_run_id": { + "name": "observed_session_run_id", + "type": "text CHECK (\"observed_session_run_id\" = upper(\"observed_session_run_id\") AND length(\"observed_session_run_id\") = 26 AND substr(\"observed_session_run_id\", 1, 1) GLOB '[0-7]' AND \"observed_session_run_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "runtime_id": { + "name": "runtime_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "session_id": { + "name": "session_id", + "type": "text CHECK (\"session_id\" = upper(\"session_id\") AND length(\"session_id\") = 26 AND substr(\"session_id\", 1, 1) GLOB '[0-7]' AND \"session_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "native_resume_ref_runtime_updated_idx": { + "name": "native_resume_ref_runtime_updated_idx", + "columns": ["runtime_id", "updated_at"], + "isUnique": false + } + }, + "foreignKeys": { + "native_resume_ref_session_id_session_id_fk": { + "name": "native_resume_ref_session_id_session_id_fk", + "tableFrom": "native_resume_ref", + "tableTo": "session", + "columnsFrom": ["session_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": { + "native_resume_ref_observed_event_seq_check": { + "name": "native_resume_ref_observed_event_seq_check", + "value": "\"native_resume_ref\".\"observed_event_seq\" >= 0" + } + } + }, + "sandbox_backup_delete_intent": { + "name": "sandbox_backup_delete_intent", + "columns": { + "attempted_at": { + "name": "attempted_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "backup_id": { + "name": "backup_id", + "type": "text CHECK (\"backup_id\" = upper(\"backup_id\") AND length(\"backup_id\") = 26 AND substr(\"backup_id\", 1, 1) GLOB '[0-7]' AND \"backup_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "delete_after": { + "name": "delete_after", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "deleted_at": { + "name": "deleted_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "sandbox_backup_delete_intent_pending_idx": { + "name": "sandbox_backup_delete_intent_pending_idx", + "columns": ["delete_after", "attempted_at", "created_at", "backup_id"], + "isUnique": false, + "where": "\"sandbox_backup_delete_intent\".\"deleted_at\" IS NULL" + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": { + "sandbox_backup_delete_intent_time_check": { + "name": "sandbox_backup_delete_intent_time_check", + "value": "typeof(\"sandbox_backup_delete_intent\".\"created_at\") = 'integer' AND \"sandbox_backup_delete_intent\".\"created_at\" BETWEEN 0 AND 9007199254740991 AND typeof(\"sandbox_backup_delete_intent\".\"delete_after\") = 'integer' AND \"sandbox_backup_delete_intent\".\"delete_after\" BETWEEN \"sandbox_backup_delete_intent\".\"created_at\" AND 9007199254740991 AND (\"sandbox_backup_delete_intent\".\"attempted_at\" IS NULL OR (typeof(\"sandbox_backup_delete_intent\".\"attempted_at\") = 'integer' AND \"sandbox_backup_delete_intent\".\"attempted_at\" BETWEEN \"sandbox_backup_delete_intent\".\"delete_after\" AND 9007199254740991)) AND (\"sandbox_backup_delete_intent\".\"deleted_at\" IS NULL OR (typeof(\"sandbox_backup_delete_intent\".\"deleted_at\") = 'integer' AND \"sandbox_backup_delete_intent\".\"deleted_at\" BETWEEN coalesce(\"sandbox_backup_delete_intent\".\"attempted_at\", \"sandbox_backup_delete_intent\".\"delete_after\") AND 9007199254740991))" + } + } + }, + "sandbox_backup_staging": { + "name": "sandbox_backup_staging", + "columns": { + "actual_backup_id": { + "name": "actual_backup_id", + "type": "text CHECK (\"actual_backup_id\" = upper(\"actual_backup_id\") AND length(\"actual_backup_id\") = 26 AND substr(\"actual_backup_id\", 1, 1) GLOB '[0-7]' AND \"actual_backup_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "claim_owner": { + "name": "claim_owner", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "dir": { + "name": "dir", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "driver_generation": { + "name": "driver_generation", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "driver_instance_id": { + "name": "driver_instance_id", + "type": "text CHECK (\"driver_instance_id\" = upper(\"driver_instance_id\") AND length(\"driver_instance_id\") = 26 AND substr(\"driver_instance_id\", 1, 1) GLOB '[0-7]' AND \"driver_instance_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "id": { + "name": "id", + "type": "text CHECK (\"id\" = upper(\"id\") AND length(\"id\") = 26 AND substr(\"id\", 1, 1) GLOB '[0-7]' AND \"id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "operation_id": { + "name": "operation_id", + "type": "text CHECK (\"operation_id\" = upper(\"operation_id\") AND length(\"operation_id\") = 26 AND substr(\"operation_id\", 1, 1) GLOB '[0-7]' AND \"operation_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sandbox_id": { + "name": "sandbox_id", + "type": "text CHECK (\"sandbox_id\" = upper(\"sandbox_id\") AND length(\"sandbox_id\") = 26 AND substr(\"sandbox_id\", 1, 1) GLOB '[0-7]' AND \"sandbox_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "sandbox_incarnation": { + "name": "sandbox_incarnation", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "session_run_id": { + "name": "session_run_id", + "type": "text CHECK (\"session_run_id\" = upper(\"session_run_id\") AND length(\"session_run_id\") = 26 AND substr(\"session_run_id\", 1, 1) GLOB '[0-7]' AND \"session_run_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "ttl_seconds": { + "name": "ttl_seconds", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updates_subject_backup": { + "name": "updates_subject_backup", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "workspace_session_id": { + "name": "workspace_session_id", + "type": "text CHECK (\"workspace_session_id\" = upper(\"workspace_session_id\") AND length(\"workspace_session_id\") = 26 AND substr(\"workspace_session_id\", 1, 1) GLOB '[0-7]' AND \"workspace_session_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "sandbox_backup_staging_updated_idx": { + "name": "sandbox_backup_staging_updated_idx", + "columns": ["updated_at", "id"], + "isUnique": false + }, + "sandbox_backup_staging_actual_idx": { + "name": "sandbox_backup_staging_actual_idx", + "columns": ["actual_backup_id"], + "isUnique": true, + "where": "\"sandbox_backup_staging\".\"actual_backup_id\" IS NOT NULL" + }, + "sandbox_backup_staging_terminal_checkpoint_idx": { + "name": "sandbox_backup_staging_terminal_checkpoint_idx", + "columns": ["sandbox_id", "sandbox_incarnation", "dir", "session_run_id"], + "isUnique": true, + "where": "\"sandbox_backup_staging\".\"session_run_id\" IS NOT NULL" + }, + "sandbox_backup_staging_operation_checkpoint_idx": { + "name": "sandbox_backup_staging_operation_checkpoint_idx", + "columns": ["sandbox_id", "sandbox_incarnation", "operation_id", "dir"], + "isUnique": true, + "where": "\"sandbox_backup_staging\".\"operation_id\" IS NOT NULL" + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": { + "sandbox_backup_staging_claim_owner_check": { + "name": "sandbox_backup_staging_claim_owner_check", + "value": "\"sandbox_backup_staging\".\"claim_owner\" IS NULL OR (typeof(\"sandbox_backup_staging\".\"claim_owner\") = 'text' AND length(\"sandbox_backup_staging\".\"claim_owner\") > 0)" + }, + "sandbox_backup_staging_dir_check": { + "name": "sandbox_backup_staging_dir_check", + "value": "typeof(\"sandbox_backup_staging\".\"dir\") = 'text' AND length(\"sandbox_backup_staging\".\"dir\") > 0" + }, + "sandbox_backup_staging_incarnation_check": { + "name": "sandbox_backup_staging_incarnation_check", + "value": "typeof(\"sandbox_backup_staging\".\"sandbox_incarnation\") = 'integer' AND \"sandbox_backup_staging\".\"sandbox_incarnation\" BETWEEN 1 AND 9007199254740991" + }, + "sandbox_backup_staging_ttl_check": { + "name": "sandbox_backup_staging_ttl_check", + "value": "typeof(\"sandbox_backup_staging\".\"ttl_seconds\") = 'integer' AND \"sandbox_backup_staging\".\"ttl_seconds\" BETWEEN 1 AND 9007199254740991" + }, + "sandbox_backup_staging_timestamps_check": { + "name": "sandbox_backup_staging_timestamps_check", + "value": "typeof(\"sandbox_backup_staging\".\"created_at\") = 'integer' AND \"sandbox_backup_staging\".\"created_at\" BETWEEN 0 AND 9007199254740991 AND typeof(\"sandbox_backup_staging\".\"updated_at\") = 'integer' AND \"sandbox_backup_staging\".\"updated_at\" BETWEEN \"sandbox_backup_staging\".\"created_at\" AND 9007199254740991" + }, + "sandbox_backup_staging_scope_check": { + "name": "sandbox_backup_staging_scope_check", + "value": "((\"sandbox_backup_staging\".\"operation_id\" IS NOT NULL AND \"sandbox_backup_staging\".\"claim_owner\" IS NOT NULL AND \"sandbox_backup_staging\".\"session_run_id\" IS NULL AND \"sandbox_backup_staging\".\"driver_instance_id\" IS NULL AND \"sandbox_backup_staging\".\"driver_generation\" IS NULL) OR (\"sandbox_backup_staging\".\"operation_id\" IS NULL AND \"sandbox_backup_staging\".\"claim_owner\" IS NULL AND \"sandbox_backup_staging\".\"session_run_id\" IS NOT NULL AND \"sandbox_backup_staging\".\"workspace_session_id\" IS NOT NULL AND \"sandbox_backup_staging\".\"driver_instance_id\" IS NOT NULL AND typeof(\"sandbox_backup_staging\".\"driver_generation\") = 'integer' AND \"sandbox_backup_staging\".\"driver_generation\" BETWEEN 0 AND 9007199254740991)) AND (\"sandbox_backup_staging\".\"updates_subject_backup\" = false OR (\"sandbox_backup_staging\".\"operation_id\" IS NOT NULL AND \"sandbox_backup_staging\".\"workspace_session_id\" IS NULL))" + }, + "sandbox_backup_staging_updates_subject_check": { + "name": "sandbox_backup_staging_updates_subject_check", + "value": "typeof(\"sandbox_backup_staging\".\"updates_subject_backup\") = 'integer' AND \"sandbox_backup_staging\".\"updates_subject_backup\" IN (false, true)" + } + } + }, + "sandbox_backup": { + "name": "sandbox_backup", + "columns": { + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "dir": { + "name": "dir", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "id": { + "name": "id", + "type": "text CHECK (\"id\" = upper(\"id\") AND length(\"id\") = 26 AND substr(\"id\", 1, 1) GLOB '[0-7]' AND \"id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "keep": { + "name": "keep", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "operation_id": { + "name": "operation_id", + "type": "text CHECK (\"operation_id\" = upper(\"operation_id\") AND length(\"operation_id\") = 26 AND substr(\"operation_id\", 1, 1) GLOB '[0-7]' AND \"operation_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sandbox_id": { + "name": "sandbox_id", + "type": "text CHECK (\"sandbox_id\" = upper(\"sandbox_id\") AND length(\"sandbox_id\") = 26 AND substr(\"sandbox_id\", 1, 1) GLOB '[0-7]' AND \"sandbox_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "sandbox_incarnation": { + "name": "sandbox_incarnation", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "session_run_id": { + "name": "session_run_id", + "type": "text CHECK (\"session_run_id\" = upper(\"session_run_id\") AND length(\"session_run_id\") = 26 AND substr(\"session_run_id\", 1, 1) GLOB '[0-7]' AND \"session_run_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "staging_id": { + "name": "staging_id", + "type": "text CHECK (\"staging_id\" = upper(\"staging_id\") AND length(\"staging_id\") = 26 AND substr(\"staging_id\", 1, 1) GLOB '[0-7]' AND \"staging_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "ttl_seconds": { + "name": "ttl_seconds", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "workspace_session_id": { + "name": "workspace_session_id", + "type": "text CHECK (\"workspace_session_id\" = upper(\"workspace_session_id\") AND length(\"workspace_session_id\") = 26 AND substr(\"workspace_session_id\", 1, 1) GLOB '[0-7]' AND \"workspace_session_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "sandbox_backup_sandbox_status_dir_created_idx": { + "name": "sandbox_backup_sandbox_status_dir_created_idx", + "columns": ["sandbox_id", "status", "dir", "created_at", "id"], + "isUnique": false + }, + "sandbox_backup_workspace_status_updated_idx": { + "name": "sandbox_backup_workspace_status_updated_idx", + "columns": ["workspace_session_id", "status", "updated_at", "id"], + "isUnique": false + }, + "sandbox_backup_staging_idx": { + "name": "sandbox_backup_staging_idx", + "columns": ["staging_id"], + "isUnique": true + }, + "sandbox_backup_terminal_checkpoint_idx": { + "name": "sandbox_backup_terminal_checkpoint_idx", + "columns": ["sandbox_id", "sandbox_incarnation", "dir", "session_run_id"], + "isUnique": true, + "where": "\"sandbox_backup\".\"session_run_id\" IS NOT NULL" + }, + "sandbox_backup_operation_checkpoint_idx": { + "name": "sandbox_backup_operation_checkpoint_idx", + "columns": ["sandbox_id", "sandbox_incarnation", "operation_id", "dir"], + "isUnique": true, + "where": "\"sandbox_backup\".\"operation_id\" IS NOT NULL" + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": { + "sandbox_backup_status_check": { + "name": "sandbox_backup_status_check", + "value": "\"sandbox_backup\".\"status\" IN ('ready', 'pruned')" + }, + "sandbox_backup_dir_check": { + "name": "sandbox_backup_dir_check", + "value": "typeof(\"sandbox_backup\".\"dir\") = 'text' AND length(\"sandbox_backup\".\"dir\") > 0" + }, + "sandbox_backup_keep_check": { + "name": "sandbox_backup_keep_check", + "value": "typeof(\"sandbox_backup\".\"keep\") = 'integer' AND \"sandbox_backup\".\"keep\" IN (false, true)" + }, + "sandbox_backup_incarnation_check": { + "name": "sandbox_backup_incarnation_check", + "value": "typeof(\"sandbox_backup\".\"sandbox_incarnation\") = 'integer' AND \"sandbox_backup\".\"sandbox_incarnation\" BETWEEN 0 AND 9007199254740991 AND (\"sandbox_backup\".\"sandbox_incarnation\" > 0 OR \"sandbox_backup\".\"staging_id\" = \"sandbox_backup\".\"id\")" + }, + "sandbox_backup_ttl_check": { + "name": "sandbox_backup_ttl_check", + "value": "typeof(\"sandbox_backup\".\"ttl_seconds\") = 'integer' AND \"sandbox_backup\".\"ttl_seconds\" BETWEEN 1 AND 9007199254740991" + }, + "sandbox_backup_timestamps_check": { + "name": "sandbox_backup_timestamps_check", + "value": "typeof(\"sandbox_backup\".\"created_at\") = 'integer' AND \"sandbox_backup\".\"created_at\" BETWEEN 0 AND 9007199254740991 AND typeof(\"sandbox_backup\".\"updated_at\") = 'integer' AND \"sandbox_backup\".\"updated_at\" BETWEEN \"sandbox_backup\".\"created_at\" AND 9007199254740991" + }, + "sandbox_backup_scope_check": { + "name": "sandbox_backup_scope_check", + "value": "(\"sandbox_backup\".\"session_run_id\" IS NULL OR \"sandbox_backup\".\"workspace_session_id\" IS NOT NULL) AND ((\"sandbox_backup\".\"operation_id\" IS NOT NULL) <> (\"sandbox_backup\".\"session_run_id\" IS NOT NULL) OR (\"sandbox_backup\".\"operation_id\" IS NULL AND \"sandbox_backup\".\"session_run_id\" IS NULL AND \"sandbox_backup\".\"workspace_session_id\" IS NULL AND \"sandbox_backup\".\"staging_id\" = \"sandbox_backup\".\"id\" AND \"sandbox_backup\".\"sandbox_incarnation\" = 0))" + } + } + }, + "sandbox_session": { + "name": "sandbox_session", + "columns": { + "cloudflare_session_id": { + "name": "cloudflare_session_id", + "type": "text CHECK (\"cloudflare_session_id\" = upper(\"cloudflare_session_id\") AND length(\"cloudflare_session_id\") = 26 AND substr(\"cloudflare_session_id\", 1, 1) GLOB '[0-7]' AND \"cloudflare_session_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "cleanup_operation_id": { + "name": "cleanup_operation_id", + "type": "text CHECK (\"cleanup_operation_id\" = upper(\"cleanup_operation_id\") AND length(\"cleanup_operation_id\") = 26 AND substr(\"cleanup_operation_id\", 1, 1) GLOB '[0-7]' AND \"cleanup_operation_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "cwd": { + "name": "cwd", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "origin_json": { + "name": "origin_json", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "sandbox_id": { + "name": "sandbox_id", + "type": "text CHECK (\"sandbox_id\" = upper(\"sandbox_id\") AND length(\"sandbox_id\") = 26 AND substr(\"sandbox_id\", 1, 1) GLOB '[0-7]' AND \"sandbox_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "sandbox_incarnation": { + "name": "sandbox_incarnation", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "session_id": { + "name": "session_id", + "type": "text CHECK (\"session_id\" = upper(\"session_id\") AND length(\"session_id\") = 26 AND substr(\"session_id\", 1, 1) GLOB '[0-7]' AND \"session_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "sandbox_session_status_updated_idx": { + "name": "sandbox_session_status_updated_idx", + "columns": ["status", "updated_at", "session_id"], + "isUnique": false + }, + "sandbox_session_sandbox_status_idx": { + "name": "sandbox_session_sandbox_status_idx", + "columns": ["sandbox_id", "status", "updated_at"], + "isUnique": false + }, + "sandbox_session_cloudflare_session_idx": { + "name": "sandbox_session_cloudflare_session_idx", + "columns": ["cloudflare_session_id"], + "isUnique": true + } + }, + "foreignKeys": { + "sandbox_session_session_id_session_id_fk": { + "name": "sandbox_session_session_id_session_id_fk", + "tableFrom": "sandbox_session", + "tableTo": "session", + "columnsFrom": ["session_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": { + "sandbox_session_cleanup_check": { + "name": "sandbox_session_cleanup_check", + "value": "(\"sandbox_session\".\"status\" = 'cleanup_pending' AND \"sandbox_session\".\"cleanup_operation_id\" IS NOT NULL) OR (\"sandbox_session\".\"status\" <> 'cleanup_pending' AND \"sandbox_session\".\"cleanup_operation_id\" IS NULL)" + }, + "sandbox_session_status_incarnation_check": { + "name": "sandbox_session_status_incarnation_check", + "value": "\"sandbox_session\".\"status\" IN ('active', 'cleanup_pending', 'closed', 'error') AND typeof(\"sandbox_session\".\"sandbox_incarnation\") = 'integer' AND \"sandbox_session\".\"sandbox_incarnation\" BETWEEN 0 AND 9007199254740991 AND (\"sandbox_session\".\"status\" IN ('closed', 'error') OR \"sandbox_session\".\"sandbox_incarnation\" > 0)" + } + } + }, + "sandbox": { + "name": "sandbox", + "columns": { + "agent_id": { + "name": "agent_id", + "type": "text CHECK (\"agent_id\" = upper(\"agent_id\") AND length(\"agent_id\") = 26 AND substr(\"agent_id\", 1, 1) GLOB '[0-7]' AND \"agent_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "app_id": { + "name": "app_id", + "type": "text CHECK (\"app_id\" = upper(\"app_id\") AND length(\"app_id\") = 26 AND substr(\"app_id\", 1, 1) GLOB '[0-7]' AND \"app_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "bind_mount_ready": { + "name": "bind_mount_ready", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "claim_expires_at": { + "name": "claim_expires_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "claim_owner": { + "name": "claim_owner", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "global_mounts_json": { + "name": "global_mounts_json", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'[]'" + }, + "id": { + "name": "id", + "type": "text CHECK (\"id\" = upper(\"id\") AND length(\"id\") = 26 AND substr(\"id\", 1, 1) GLOB '[0-7]' AND \"id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "inactive_deadline_at": { + "name": "inactive_deadline_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "incarnation": { + "name": "incarnation", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "last_backup_id": { + "name": "last_backup_id", + "type": "text CHECK (\"last_backup_id\" = upper(\"last_backup_id\") AND length(\"last_backup_id\") = 26 AND substr(\"last_backup_id\", 1, 1) GLOB '[0-7]' AND \"last_backup_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_error_code": { + "name": "last_error_code", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_restore_backup_id": { + "name": "last_restore_backup_id", + "type": "text CHECK (\"last_restore_backup_id\" = upper(\"last_restore_backup_id\") AND length(\"last_restore_backup_id\") = 26 AND substr(\"last_restore_backup_id\", 1, 1) GLOB '[0-7]' AND \"last_restore_backup_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "network_constraints_hash": { + "name": "network_constraints_hash", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "owner_account_id": { + "name": "owner_account_id", + "type": "text CHECK (\"owner_account_id\" = upper(\"owner_account_id\") AND length(\"owner_account_id\") = 26 AND substr(\"owner_account_id\", 1, 1) GLOB '[0-7]' AND \"owner_account_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "operation_kind": { + "name": "operation_kind", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status_changed_at": { + "name": "status_changed_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "status_event": { + "name": "status_event", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'runtime_subject.cold'" + }, + "status_operation_id": { + "name": "status_operation_id", + "type": "text CHECK (\"status_operation_id\" = upper(\"status_operation_id\") AND length(\"status_operation_id\") = 26 AND substr(\"status_operation_id\", 1, 1) GLOB '[0-7]' AND \"status_operation_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status_seq": { + "name": "status_seq", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "status_source": { + "name": "status_source", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'system'" + }, + "subject_id": { + "name": "subject_id", + "type": "text CHECK (\"subject_id\" = upper(\"subject_id\") AND length(\"subject_id\") = 26 AND substr(\"subject_id\", 1, 1) GLOB '[0-7]' AND \"subject_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "subject_kind": { + "name": "subject_kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "sandbox_subject_idx": { + "name": "sandbox_subject_idx", + "columns": ["kind", "subject_kind", "subject_id"], + "isUnique": true + }, + "sandbox_status_deadline_idx": { + "name": "sandbox_status_deadline_idx", + "columns": ["status", "inactive_deadline_at", "updated_at"], + "isUnique": false + }, + "sandbox_claim_idx": { + "name": "sandbox_claim_idx", + "columns": ["claim_expires_at", "claim_owner"], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": { + "sandbox_status_check": { + "name": "sandbox_status_check", + "value": "\"sandbox\".\"status\" IN ('cold', 'restoring', 'active', 'backing_up', 'destroying')" + }, + "sandbox_status_seq_check": { + "name": "sandbox_status_seq_check", + "value": "\"sandbox\".\"status_seq\" >= 0" + }, + "sandbox_incarnation_check": { + "name": "sandbox_incarnation_check", + "value": "typeof(\"sandbox\".\"incarnation\") = 'integer' AND \"sandbox\".\"incarnation\" BETWEEN 0 AND 9007199254740991 AND (\"sandbox\".\"status\" = 'cold' OR \"sandbox\".\"incarnation\" > 0)" + }, + "sandbox_identity_check": { + "name": "sandbox_identity_check", + "value": "(\"sandbox\".\"kind\" = 'pet' AND \"sandbox\".\"subject_kind\" = 'agent' AND \"sandbox\".\"subject_id\" = \"sandbox\".\"agent_id\") OR (\"sandbox\".\"kind\" = 'cattle' AND \"sandbox\".\"subject_kind\" = 'session')" + }, + "sandbox_network_constraints_hash_check": { + "name": "sandbox_network_constraints_hash_check", + "value": "(\"sandbox\".\"network_constraints_hash\" IS NULL AND \"sandbox\".\"status\" = 'cold') OR (\"sandbox\".\"network_constraints_hash\" IS NOT NULL AND length(\"sandbox\".\"network_constraints_hash\") = 64 AND \"sandbox\".\"network_constraints_hash\" = lower(\"sandbox\".\"network_constraints_hash\") AND \"sandbox\".\"network_constraints_hash\" NOT GLOB '*[^0-9a-f]*')" + }, + "sandbox_operation_state_check": { + "name": "sandbox_operation_state_check", + "value": "(\"sandbox\".\"status\" IN ('cold', 'active') AND \"sandbox\".\"operation_kind\" IS NULL AND \"sandbox\".\"status_operation_id\" IS NULL) OR (\"sandbox\".\"status\" = 'restoring' AND \"sandbox\".\"operation_kind\" = 'activate' AND \"sandbox\".\"status_operation_id\" IS NOT NULL) OR (\"sandbox\".\"status\" = 'backing_up' AND \"sandbox\".\"operation_kind\" IN ('hibernate', 'recreate', 'reset') AND \"sandbox\".\"status_operation_id\" IS NOT NULL) OR (\"sandbox\".\"status\" = 'destroying' AND \"sandbox\".\"operation_kind\" IN ('activate', 'hibernate', 'recreate', 'reset') AND \"sandbox\".\"status_operation_id\" IS NOT NULL)" + }, + "sandbox_claim_check": { + "name": "sandbox_claim_check", + "value": "(\"sandbox\".\"claim_owner\" IS NULL AND \"sandbox\".\"claim_expires_at\" IS NULL) OR (\"sandbox\".\"claim_owner\" IS NOT NULL AND typeof(\"sandbox\".\"claim_expires_at\") = 'integer' AND \"sandbox\".\"claim_expires_at\" BETWEEN 0 AND 9007199254740991)" + }, + "sandbox_operation_claim_check": { + "name": "sandbox_operation_claim_check", + "value": "\"sandbox\".\"status\" IN ('cold', 'active') OR \"sandbox\".\"claim_owner\" IS NOT NULL" + } + } + }, + "session_message": { + "name": "session_message", + "columns": { + "content_text": { + "name": "content_text", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_by_account_id": { + "name": "created_by_account_id", + "type": "text CHECK (\"created_by_account_id\" = upper(\"created_by_account_id\") AND length(\"created_by_account_id\") = 26 AND substr(\"created_by_account_id\", 1, 1) GLOB '[0-7]' AND \"created_by_account_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "id": { + "name": "id", + "type": "text CHECK (\"id\" = upper(\"id\") AND length(\"id\") = 26 AND substr(\"id\", 1, 1) GLOB '[0-7]' AND \"id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "plan_json": { + "name": "plan_json", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "projection_format": { + "name": "projection_format", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'materialized'" + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "segments_json": { + "name": "segments_json", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "seq": { + "name": "seq", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "session_id": { + "name": "session_id", + "type": "text CHECK (\"session_id\" = upper(\"session_id\") AND length(\"session_id\") = 26 AND substr(\"session_id\", 1, 1) GLOB '[0-7]' AND \"session_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "session_run_id": { + "name": "session_run_id", + "type": "text CHECK (\"session_run_id\" = upper(\"session_run_id\") AND length(\"session_run_id\") = 26 AND substr(\"session_run_id\", 1, 1) GLOB '[0-7]' AND \"session_run_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "session_message_session_seq_idx": { + "name": "session_message_session_seq_idx", + "columns": ["session_id", "seq"], + "isUnique": true + }, + "session_message_run_idx": { + "name": "session_message_run_idx", + "columns": ["session_run_id"], + "isUnique": false + } + }, + "foreignKeys": { + "session_message_session_id_session_id_fk": { + "name": "session_message_session_id_session_id_fk", + "tableFrom": "session_message", + "tableTo": "session", + "columnsFrom": ["session_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": { + "session_message_projection_format_check": { + "name": "session_message_projection_format_check", + "value": "\"session_message\".\"projection_format\" IN ('materialized', 'event_stream_v3')" + }, + "session_message_event_stream_v3_check": { + "name": "session_message_event_stream_v3_check", + "value": "\"session_message\".\"projection_format\" <> 'event_stream_v3' OR (\"session_message\".\"role\" = 'assistant' AND \"session_message\".\"session_run_id\" IS NOT NULL AND \"session_message\".\"content_text\" = '' AND \"session_message\".\"plan_json\" IS NULL AND \"session_message\".\"segments_json\" IS NULL)" + } + } + }, + "session": { + "name": "session", + "columns": { + "agent_id": { + "name": "agent_id", + "type": "text CHECK (\"agent_id\" = upper(\"agent_id\") AND length(\"agent_id\") = 26 AND substr(\"agent_id\", 1, 1) GLOB '[0-7]' AND \"agent_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "archived_at": { + "name": "archived_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "auto_title_event_seq": { + "name": "auto_title_event_seq", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "cleanup_operation_kind": { + "name": "cleanup_operation_kind", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "end_user_id": { + "name": "end_user_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "attributed_user_id": { + "name": "attributed_user_id", + "type": "text CHECK (\"attributed_user_id\" = upper(\"attributed_user_id\") AND length(\"attributed_user_id\") = 26 AND substr(\"attributed_user_id\", 1, 1) GLOB '[0-7]' AND \"attributed_user_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "creator_account_id": { + "name": "creator_account_id", + "type": "text CHECK (\"creator_account_id\" = upper(\"creator_account_id\") AND length(\"creator_account_id\") = 26 AND substr(\"creator_account_id\", 1, 1) GLOB '[0-7]' AND \"creator_account_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "deployment_version_id": { + "name": "deployment_version_id", + "type": "text CHECK (\"deployment_version_id\" = upper(\"deployment_version_id\") AND length(\"deployment_version_id\") = 26 AND substr(\"deployment_version_id\", 1, 1) GLOB '[0-7]' AND \"deployment_version_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "deployment_version_number": { + "name": "deployment_version_number", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "id": { + "name": "id", + "type": "text CHECK (\"id\" = upper(\"id\") AND length(\"id\") = 26 AND substr(\"id\", 1, 1) GLOB '[0-7]' AND \"id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "last_message_at": { + "name": "last_message_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_run_id": { + "name": "last_run_id", + "type": "text CHECK (\"last_run_id\" = upper(\"last_run_id\") AND length(\"last_run_id\") = 26 AND substr(\"last_run_id\", 1, 1) GLOB '[0-7]' AND \"last_run_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "message_seq_cursor": { + "name": "message_seq_cursor", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "metadata_json": { + "name": "metadata_json", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'{}'" + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "app_id": { + "name": "app_id", + "type": "text CHECK (\"app_id\" = upper(\"app_id\") AND length(\"app_id\") = 26 AND substr(\"app_id\", 1, 1) GLOB '[0-7]' AND \"app_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "renamed": { + "name": "renamed", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "runtime_id": { + "name": "runtime_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status_operation_id": { + "name": "status_operation_id", + "type": "text CHECK (\"status_operation_id\" = upper(\"status_operation_id\") AND length(\"status_operation_id\") = 26 AND substr(\"status_operation_id\", 1, 1) GLOB '[0-7]' AND \"status_operation_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status_seq": { + "name": "status_seq", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "runtime_event_seq_cursor": { + "name": "runtime_event_seq_cursor", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "runtime_provisioning_heartbeat_at": { + "name": "runtime_provisioning_heartbeat_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "runtime_provisioning_operation_id": { + "name": "runtime_provisioning_operation_id", + "type": "text CHECK (\"runtime_provisioning_operation_id\" = upper(\"runtime_provisioning_operation_id\") AND length(\"runtime_provisioning_operation_id\") = 26 AND substr(\"runtime_provisioning_operation_id\", 1, 1) GLOB '[0-7]' AND \"runtime_provisioning_operation_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "runtime_provisioning_run_id": { + "name": "runtime_provisioning_run_id", + "type": "text CHECK (\"runtime_provisioning_run_id\" = upper(\"runtime_provisioning_run_id\") AND length(\"runtime_provisioning_run_id\") = 26 AND substr(\"runtime_provisioning_run_id\", 1, 1) GLOB '[0-7]' AND \"runtime_provisioning_run_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "runtime_provisioning_sandbox_id": { + "name": "runtime_provisioning_sandbox_id", + "type": "text CHECK (\"runtime_provisioning_sandbox_id\" = upper(\"runtime_provisioning_sandbox_id\") AND length(\"runtime_provisioning_sandbox_id\") = 26 AND substr(\"runtime_provisioning_sandbox_id\", 1, 1) GLOB '[0-7]' AND \"runtime_provisioning_sandbox_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "runtime_provisioning_sandbox_session_id": { + "name": "runtime_provisioning_sandbox_session_id", + "type": "text CHECK (\"runtime_provisioning_sandbox_session_id\" = upper(\"runtime_provisioning_sandbox_session_id\") AND length(\"runtime_provisioning_sandbox_session_id\") = 26 AND substr(\"runtime_provisioning_sandbox_session_id\", 1, 1) GLOB '[0-7]' AND \"runtime_provisioning_sandbox_session_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "runtime_provisioning_sandbox_incarnation": { + "name": "runtime_provisioning_sandbox_incarnation", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'preview'" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "workspace_checkpoint_required": { + "name": "workspace_checkpoint_required", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + } + }, + "indexes": { + "session_agent_updated_idx": { + "name": "session_agent_updated_idx", + "columns": ["agent_id", "updated_at", "id"], + "isUnique": false + }, + "session_app_creator_archived_updated_idx": { + "name": "session_app_creator_archived_updated_idx", + "columns": ["app_id", "creator_account_id", "archived_at", "updated_at", "id"], + "isUnique": false + }, + "session_app_attributed_archived_updated_idx": { + "name": "session_app_attributed_archived_updated_idx", + "columns": ["app_id", "attributed_user_id", "archived_at", "updated_at", "id"], + "isUnique": false + }, + "session_app_creator_type_archived_updated_idx": { + "name": "session_app_creator_type_archived_updated_idx", + "columns": ["app_id", "creator_account_id", "type", "archived_at", "updated_at", "id"], + "isUnique": false + }, + "session_app_attributed_type_archived_updated_idx": { + "name": "session_app_attributed_type_archived_updated_idx", + "columns": ["app_id", "attributed_user_id", "type", "archived_at", "updated_at", "id"], + "isUnique": false + }, + "session_status_operation_updated_idx": { + "name": "session_status_operation_updated_idx", + "columns": ["status", "status_operation_id", "updated_at"], + "isUnique": false + }, + "session_cleanup_operation_updated_idx": { + "name": "session_cleanup_operation_updated_idx", + "columns": ["cleanup_operation_kind", "status", "updated_at", "id"], + "isUnique": false + }, + "session_runtime_provisioning_heartbeat_idx": { + "name": "session_runtime_provisioning_heartbeat_idx", + "columns": ["runtime_provisioning_heartbeat_at", "id"], + "isUnique": false + }, + "session_runtime_provisioning_sandbox_idx": { + "name": "session_runtime_provisioning_sandbox_idx", + "columns": ["runtime_provisioning_sandbox_id"], + "isUnique": true, + "where": "\"session\".\"runtime_provisioning_operation_id\" IS NOT NULL" + }, + "session_status_updated_idx": { + "name": "session_status_updated_idx", + "columns": ["status", "updated_at", "id"], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": { + "session_cleanup_operation_kind_check": { + "name": "session_cleanup_operation_kind_check", + "value": "\"session\".\"cleanup_operation_kind\" IS NULL OR (\"session\".\"cleanup_operation_kind\" IN ('archive', 'delete') AND \"session\".\"archived_at\" IS NOT NULL AND \"session\".\"status\" IN ('IDLE', 'RESCHEDULING') AND (\"session\".\"status_operation_id\" IS NOT NULL OR (\"session\".\"cleanup_operation_kind\" = 'archive' AND \"session\".\"status\" = 'IDLE')))" + }, + "session_runtime_provisioning_lease_check": { + "name": "session_runtime_provisioning_lease_check", + "value": "(\"session\".\"runtime_provisioning_operation_id\" IS NULL AND \"session\".\"runtime_provisioning_run_id\" IS NULL AND \"session\".\"runtime_provisioning_sandbox_id\" IS NULL AND \"session\".\"runtime_provisioning_heartbeat_at\" IS NULL) OR (\"session\".\"runtime_provisioning_operation_id\" IS NOT NULL AND \"session\".\"runtime_provisioning_sandbox_id\" IS NOT NULL AND \"session\".\"runtime_provisioning_heartbeat_at\" IS NOT NULL AND typeof(\"session\".\"runtime_provisioning_heartbeat_at\") = 'integer' AND \"session\".\"runtime_provisioning_heartbeat_at\" >= 0 AND \"session\".\"archived_at\" IS NULL AND \"session\".\"cleanup_operation_kind\" IS NULL AND \"session\".\"status_operation_id\" IS NULL)" + }, + "session_runtime_provisioning_sandbox_pair_check": { + "name": "session_runtime_provisioning_sandbox_pair_check", + "value": "(\"session\".\"runtime_provisioning_sandbox_session_id\" IS NULL AND \"session\".\"runtime_provisioning_sandbox_incarnation\" IS NULL) OR (\"session\".\"runtime_provisioning_operation_id\" IS NOT NULL AND typeof(\"session\".\"runtime_provisioning_sandbox_incarnation\") = 'integer' AND \"session\".\"runtime_provisioning_sandbox_incarnation\" BETWEEN 0 AND 9007199254740991)" + }, + "session_status_check": { + "name": "session_status_check", + "value": "\"session\".\"status\" IN ('IDLE', 'RUNNING', 'RESCHEDULING', 'TERMINATED')" + }, + "session_auto_title_event_seq_check": { + "name": "session_auto_title_event_seq_check", + "value": "\"session\".\"auto_title_event_seq\" IS NULL OR \"session\".\"auto_title_event_seq\" >= 0" + }, + "session_status_seq_check": { + "name": "session_status_seq_check", + "value": "\"session\".\"status_seq\" >= 0" + } + } + }, + "session_execution_snapshot": { + "name": "session_execution_snapshot", + "columns": { + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "plan_json": { + "name": "plan_json", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "session_id": { + "name": "session_id", + "type": "text CHECK (\"session_id\" = upper(\"session_id\") AND length(\"session_id\") = 26 AND substr(\"session_id\", 1, 1) GLOB '[0-7]' AND \"session_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": true, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "session_execution_snapshot_session_id_session_id_fk": { + "name": "session_execution_snapshot_session_id_session_id_fk", + "tableFrom": "session_execution_snapshot", + "tableTo": "session", + "columnsFrom": ["session_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "session_run_skill": { + "name": "session_run_skill", + "columns": { + "blob_sha256": { + "name": "blob_sha256", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "materialization_status": { + "name": "materialization_status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "mount_path": { + "name": "mount_path", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "resolution_mode": { + "name": "resolution_mode", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "session_run_id": { + "name": "session_run_id", + "type": "text CHECK (\"session_run_id\" = upper(\"session_run_id\") AND length(\"session_run_id\") = 26 AND substr(\"session_run_id\", 1, 1) GLOB '[0-7]' AND \"session_run_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "skill_id": { + "name": "skill_id", + "type": "text CHECK (\"skill_id\" = upper(\"skill_id\") AND length(\"skill_id\") = 26 AND substr(\"skill_id\", 1, 1) GLOB '[0-7]' AND \"skill_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "skill_name": { + "name": "skill_name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "snapshot_id": { + "name": "snapshot_id", + "type": "text CHECK (\"snapshot_id\" = upper(\"snapshot_id\") AND length(\"snapshot_id\") = 26 AND substr(\"snapshot_id\", 1, 1) GLOB '[0-7]' AND \"snapshot_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "warning_code": { + "name": "warning_code", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "session_run_skill_run_resolution_idx": { + "name": "session_run_skill_run_resolution_idx", + "columns": ["session_run_id", "resolution_mode"], + "isUnique": false + } + }, + "foreignKeys": { + "session_run_skill_session_run_id_session_run_id_fk": { + "name": "session_run_skill_session_run_id_session_run_id_fk", + "tableFrom": "session_run_skill", + "tableTo": "session_run", + "columnsFrom": ["session_run_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "session_run_skill_session_run_id_skill_id_pk": { + "columns": ["session_run_id", "skill_id"], + "name": "session_run_skill_session_run_id_skill_id_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "session_run": { + "name": "session_run", + "columns": { + "agent_id": { + "name": "agent_id", + "type": "text CHECK (\"agent_id\" = upper(\"agent_id\") AND length(\"agent_id\") = 26 AND substr(\"agent_id\", 1, 1) GLOB '[0-7]' AND \"agent_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "bound_capability_agent_id": { + "name": "bound_capability_agent_id", + "type": "text CHECK (\"bound_capability_agent_id\" = upper(\"bound_capability_agent_id\") AND length(\"bound_capability_agent_id\") = 26 AND substr(\"bound_capability_agent_id\", 1, 1) GLOB '[0-7]' AND \"bound_capability_agent_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "bound_capability_app_id": { + "name": "bound_capability_app_id", + "type": "text CHECK (\"bound_capability_app_id\" = upper(\"bound_capability_app_id\") AND length(\"bound_capability_app_id\") = 26 AND substr(\"bound_capability_app_id\", 1, 1) GLOB '[0-7]' AND \"bound_capability_app_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "bound_capability_binding_env": { + "name": "bound_capability_binding_env", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "bound_capability_binding_name": { + "name": "bound_capability_binding_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "bound_capability_deployment_id": { + "name": "bound_capability_deployment_id", + "type": "text CHECK (\"bound_capability_deployment_id\" = upper(\"bound_capability_deployment_id\") AND length(\"bound_capability_deployment_id\") = 26 AND substr(\"bound_capability_deployment_id\", 1, 1) GLOB '[0-7]' AND \"bound_capability_deployment_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "bound_capability_deployment_run_id": { + "name": "bound_capability_deployment_run_id", + "type": "text CHECK (\"bound_capability_deployment_run_id\" = upper(\"bound_capability_deployment_run_id\") AND length(\"bound_capability_deployment_run_id\") = 26 AND substr(\"bound_capability_deployment_run_id\", 1, 1) GLOB '[0-7]' AND \"bound_capability_deployment_run_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "completed_at": { + "name": "completed_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_by_account_id": { + "name": "created_by_account_id", + "type": "text CHECK (\"created_by_account_id\" = upper(\"created_by_account_id\") AND length(\"created_by_account_id\") = 26 AND substr(\"created_by_account_id\", 1, 1) GLOB '[0-7]' AND \"created_by_account_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "deployment_version_id": { + "name": "deployment_version_id", + "type": "text CHECK (\"deployment_version_id\" = upper(\"deployment_version_id\") AND length(\"deployment_version_id\") = 26 AND substr(\"deployment_version_id\", 1, 1) GLOB '[0-7]' AND \"deployment_version_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "deployment_version_number": { + "name": "deployment_version_number", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "driver_instance_id": { + "name": "driver_instance_id", + "type": "text CHECK (\"driver_instance_id\" = upper(\"driver_instance_id\") AND length(\"driver_instance_id\") = 26 AND substr(\"driver_instance_id\", 1, 1) GLOB '[0-7]' AND \"driver_instance_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "error_code": { + "name": "error_code", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "error_details_json": { + "name": "error_details_json", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "error_retryable": { + "name": "error_retryable", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "id": { + "name": "id", + "type": "text CHECK (\"id\" = upper(\"id\") AND length(\"id\") = 26 AND substr(\"id\", 1, 1) GLOB '[0-7]' AND \"id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "runtime_id": { + "name": "runtime_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "session_id": { + "name": "session_id", + "type": "text CHECK (\"session_id\" = upper(\"session_id\") AND length(\"session_id\") = 26 AND substr(\"session_id\", 1, 1) GLOB '[0-7]' AND \"session_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "started_at": { + "name": "started_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status_changed_at": { + "name": "status_changed_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "status_event": { + "name": "status_event", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'run.queue'" + }, + "status_operation_id": { + "name": "status_operation_id", + "type": "text CHECK (\"status_operation_id\" = upper(\"status_operation_id\") AND length(\"status_operation_id\") = 26 AND substr(\"status_operation_id\", 1, 1) GLOB '[0-7]' AND \"status_operation_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status_seq": { + "name": "status_seq", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "status_source": { + "name": "status_source", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'system'" + }, + "terminal_reconciliation_attempted_at": { + "name": "terminal_reconciliation_attempted_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "trace_id": { + "name": "trace_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "trigger": { + "name": "trigger", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "session_run_driver_instance_idx": { + "name": "session_run_driver_instance_idx", + "columns": ["driver_instance_id", "created_at"], + "isUnique": false + }, + "session_run_active_driver_lease_idx": { + "name": "session_run_active_driver_lease_idx", + "columns": ["driver_instance_id"], + "isUnique": true, + "where": "\"session_run\".\"driver_instance_id\" IS NOT NULL AND \"session_run\".\"status\" IN ('queued', 'booting', 'running', 'waiting_input')" + }, + "session_run_session_created_at_idx": { + "name": "session_run_session_created_at_idx", + "columns": ["session_id", "created_at"], + "isUnique": false + }, + "session_run_session_status_idx": { + "name": "session_run_session_status_idx", + "columns": ["session_id", "status"], + "isUnique": false + }, + "session_run_terminal_reconciliation_attempt_idx": { + "name": "session_run_terminal_reconciliation_attempt_idx", + "columns": ["coalesce(\"terminal_reconciliation_attempted_at\", \"updated_at\")", "id"], + "isUnique": false + } + }, + "foreignKeys": { + "session_run_session_id_session_id_fk": { + "name": "session_run_session_id_session_id_fk", + "tableFrom": "session_run", + "tableTo": "session", + "columnsFrom": ["session_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": { + "session_run_error_retryable_check": { + "name": "session_run_error_retryable_check", + "value": "\"session_run\".\"error_retryable\" IS NULL OR (\"session_run\".\"error_retryable\" IN (false, true) AND \"session_run\".\"error_code\" IS NOT NULL AND \"session_run\".\"error_details_json\" IS NOT NULL AND \"session_run\".\"error_message\" IS NOT NULL)" + }, + "session_run_status_check": { + "name": "session_run_status_check", + "value": "\"session_run\".\"status\" IN ('queued', 'booting', 'running', 'waiting_input', 'completed', 'failed', 'cancelled', 'expired')" + }, + "session_run_status_seq_check": { + "name": "session_run_status_seq_check", + "value": "\"session_run\".\"status_seq\" >= 0" + }, + "session_run_terminal_reconciliation_attempted_at_check": { + "name": "session_run_terminal_reconciliation_attempted_at_check", + "value": "\"session_run\".\"terminal_reconciliation_attempted_at\" IS NULL OR \"session_run\".\"terminal_reconciliation_attempted_at\" >= 0" + } + } + }, + "session_agent_task_snapshot": { + "name": "session_agent_task_snapshot", + "columns": { + "driver_instance_id": { + "name": "driver_instance_id", + "type": "text CHECK (\"driver_instance_id\" = upper(\"driver_instance_id\") AND length(\"driver_instance_id\") = 26 AND substr(\"driver_instance_id\", 1, 1) GLOB '[0-7]' AND \"driver_instance_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "run_id": { + "name": "run_id", + "type": "text CHECK (\"run_id\" = upper(\"run_id\") AND length(\"run_id\") = 26 AND substr(\"run_id\", 1, 1) GLOB '[0-7]' AND \"run_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "seq": { + "name": "seq", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "session_id": { + "name": "session_id", + "type": "text CHECK (\"session_id\" = upper(\"session_id\") AND length(\"session_id\") = 26 AND substr(\"session_id\", 1, 1) GLOB '[0-7]' AND \"session_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tasks_json": { + "name": "tasks_json", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "session_agent_task_snapshot_run_id_session_run_id_fk": { + "name": "session_agent_task_snapshot_run_id_session_run_id_fk", + "tableFrom": "session_agent_task_snapshot", + "tableTo": "session_run", + "columnsFrom": ["run_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "session_agent_task_snapshot_session_id_session_id_fk": { + "name": "session_agent_task_snapshot_session_id_session_id_fk", + "tableFrom": "session_agent_task_snapshot", + "tableTo": "session", + "columnsFrom": ["session_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "session_event": { + "name": "session_event", + "columns": { + "agent_id": { + "name": "agent_id", + "type": "text CHECK (\"agent_id\" = upper(\"agent_id\") AND length(\"agent_id\") = 26 AND substr(\"agent_id\", 1, 1) GLOB '[0-7]' AND \"agent_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "artifact_attempt_id": { + "name": "artifact_attempt_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "artifact_manifest_json": { + "name": "artifact_manifest_json", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "artifact_manifest_sha256": { + "name": "artifact_manifest_sha256", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "content_text": { + "name": "content_text", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "ended_at": { + "name": "ended_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "event_type": { + "name": "event_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "family": { + "name": "family", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "id": { + "name": "id", + "type": "text CHECK (\"id\" = upper(\"id\") AND length(\"id\") = 26 AND substr(\"id\", 1, 1) GLOB '[0-7]' AND \"id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "mcp_command_id": { + "name": "mcp_command_id", + "type": "text CHECK (\"mcp_command_id\" = upper(\"mcp_command_id\") AND length(\"mcp_command_id\") = 26 AND substr(\"mcp_command_id\", 1, 1) GLOB '[0-7]' AND \"mcp_command_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "occurred_at": { + "name": "occurred_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "process_status": { + "name": "process_status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "process_type": { + "name": "process_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "run_id": { + "name": "run_id", + "type": "text CHECK (\"run_id\" = upper(\"run_id\") AND length(\"run_id\") = 26 AND substr(\"run_id\", 1, 1) GLOB '[0-7]' AND \"run_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "runtime_operation_event_json": { + "name": "runtime_operation_event_json", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "semantic_hash": { + "name": "semantic_hash", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "seq": { + "name": "seq", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "session_id": { + "name": "session_id", + "type": "text CHECK (\"session_id\" = upper(\"session_id\") AND length(\"session_id\") = 26 AND substr(\"session_id\", 1, 1) GLOB '[0-7]' AND \"session_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "source_event_id": { + "name": "source_event_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "stream_id": { + "name": "stream_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "terminal_event_json": { + "name": "terminal_event_json", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "tool_call_id": { + "name": "tool_call_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "tool_input_delta_json": { + "name": "tool_input_delta_json", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "tool_input_json": { + "name": "tool_input_json", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "tool_name": { + "name": "tool_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "tool_output_delta_text": { + "name": "tool_output_delta_text", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "tool_output_text": { + "name": "tool_output_text", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "tool_parent_message_id": { + "name": "tool_parent_message_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "tool_result_message_id": { + "name": "tool_result_message_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "tool_status": { + "name": "tool_status", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "tokens": { + "name": "tokens", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "trace_id": { + "name": "trace_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "visibility": { + "name": "visibility", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "session_event_agent_family_created_idx": { + "name": "session_event_agent_family_created_idx", + "columns": ["agent_id", "family", "created_at", "id"], + "isUnique": false + }, + "session_event_artifact_attempt_idx": { + "name": "session_event_artifact_attempt_idx", + "columns": ["artifact_attempt_id"], + "isUnique": true, + "where": "\"session_event\".\"artifact_attempt_id\" IS NOT NULL" + }, + "session_event_agent_visibility_created_idx": { + "name": "session_event_agent_visibility_created_idx", + "columns": ["agent_id", "visibility", "created_at", "id"], + "isUnique": false + }, + "session_event_agent_created_idx": { + "name": "session_event_agent_created_idx", + "columns": ["agent_id", "created_at", "id"], + "isUnique": false + }, + "session_event_session_visibility_seq_idx": { + "name": "session_event_session_visibility_seq_idx", + "columns": ["session_id", "visibility", "seq"], + "isUnique": false + }, + "session_event_run_event_type_idx": { + "name": "session_event_run_event_type_idx", + "columns": ["run_id", "event_type"], + "isUnique": false + }, + "session_event_run_stream_process_seq_idx": { + "name": "session_event_run_stream_process_seq_idx", + "columns": ["run_id", "stream_id", "process_type", "seq"], + "isUnique": false + }, + "session_event_run_tool_call_seq_idx": { + "name": "session_event_run_tool_call_seq_idx", + "columns": ["run_id", "tool_call_id", "seq"], + "isUnique": false + }, + "session_event_session_seq_idx": { + "name": "session_event_session_seq_idx", + "columns": ["session_id", "seq"], + "isUnique": true + }, + "session_event_session_source_idx": { + "name": "session_event_session_source_idx", + "columns": ["session_id", "source_event_id"], + "isUnique": true + }, + "session_event_run_terminal_winner_idx": { + "name": "session_event_run_terminal_winner_idx", + "columns": ["session_id", "run_id"], + "isUnique": true, + "where": "\"session_event\".\"semantic_hash\" IS NOT NULL AND \"session_event\".\"run_id\" IS NOT NULL AND \"session_event\".\"event_type\" IN ('run.cancelled', 'run.completed', 'run.failed')" + }, + "session_event_mcp_terminal_winner_idx": { + "name": "session_event_mcp_terminal_winner_idx", + "columns": ["session_id", "mcp_command_id"], + "isUnique": true, + "where": "\"session_event\".\"mcp_command_id\" IS NOT NULL" + } + }, + "foreignKeys": { + "session_event_session_id_session_id_fk": { + "name": "session_event_session_id_session_id_fk", + "tableFrom": "session_event", + "tableTo": "session", + "columnsFrom": ["session_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": { + "session_event_artifact_manifest_check": { + "name": "session_event_artifact_manifest_check", + "value": "(\"session_event\".\"artifact_attempt_id\" IS NULL AND \"session_event\".\"artifact_manifest_json\" IS NULL AND \"session_event\".\"artifact_manifest_sha256\" IS NULL) OR (\"session_event\".\"artifact_attempt_id\" IS NOT NULL AND \"session_event\".\"artifact_manifest_json\" IS NOT NULL AND json_valid(\"session_event\".\"artifact_manifest_json\") = 1 AND json_extract(\"session_event\".\"artifact_manifest_json\", '$.version') IS 1 AND json_type(\"session_event\".\"artifact_manifest_json\", '$.captureStatus') IS 'text' AND json_extract(\"session_event\".\"artifact_manifest_json\", '$.captureStatus') IN ('complete', 'omitted_file_limit', 'omitted_runtime_unavailable', 'omitted_size_limit', 'omitted_source_changed', 'omitted_source_missing') AND json_type(\"session_event\".\"artifact_manifest_json\", '$.mode') IS 'text' AND json_extract(\"session_event\".\"artifact_manifest_json\", '$.mode') IN ('delta', 'snapshot') AND (json_extract(\"session_event\".\"artifact_manifest_json\", '$.captureStatus') = 'complete' OR json_array_length(\"session_event\".\"artifact_manifest_json\", '$.files') = 0) AND json_extract(\"session_event\".\"artifact_manifest_json\", '$.sourceEventId') IS \"session_event\".\"source_event_id\" AND json_extract(\"session_event\".\"artifact_manifest_json\", '$.semanticHash') IS \"session_event\".\"semantic_hash\" AND json_type(\"session_event\".\"artifact_manifest_json\", '$.files') IS 'array' AND \"session_event\".\"artifact_manifest_sha256\" IS NOT NULL AND length(\"session_event\".\"artifact_manifest_sha256\") = 64 AND \"session_event\".\"artifact_manifest_sha256\" = lower(\"session_event\".\"artifact_manifest_sha256\") AND \"session_event\".\"artifact_manifest_sha256\" NOT GLOB '*[^0-9a-f]*' AND \"session_event\".\"semantic_hash\" IS NOT NULL AND \"session_event\".\"event_type\" IN ('file.change.updated', 'file.changed', 'run.completed'))" + }, + "session_event_mcp_command_check": { + "name": "session_event_mcp_command_check", + "value": "\"session_event\".\"mcp_command_id\" IS NULL OR (\"session_event\".\"event_type\" = 'tool.call.updated' AND \"session_event\".\"tool_status\" IS NOT NULL AND \"session_event\".\"tool_status\" IN ('completed', 'failed', 'cancelled'))" + }, + "session_event_runtime_operation_event_json_check": { + "name": "session_event_runtime_operation_event_json_check", + "value": "\"session_event\".\"runtime_operation_event_json\" IS NULL OR (json_valid(\"session_event\".\"runtime_operation_event_json\") = 1 AND json_extract(\"session_event\".\"runtime_operation_event_json\", '$.kind') IS 'agent.task.updated' AND json_type(\"session_event\".\"runtime_operation_event_json\", '$.payload') IS 'object' AND json_extract(\"session_event\".\"runtime_operation_event_json\", '$.payload.status') IN ('updating', 'ready') AND \"session_event\".\"semantic_hash\" IS NOT NULL AND \"session_event\".\"event_type\" = 'agent.task.updated')" + }, + "session_event_semantic_hash_check": { + "name": "session_event_semantic_hash_check", + "value": "\"session_event\".\"semantic_hash\" IS NULL OR (length(\"session_event\".\"semantic_hash\") = 64 AND \"session_event\".\"semantic_hash\" = lower(\"session_event\".\"semantic_hash\") AND \"session_event\".\"semantic_hash\" NOT GLOB '*[^0-9a-f]*')" + }, + "session_event_terminal_event_json_check": { + "name": "session_event_terminal_event_json_check", + "value": "(\"session_event\".\"terminal_event_json\" IS NULL AND NOT (\"session_event\".\"semantic_hash\" IS NOT NULL AND \"session_event\".\"event_type\" IN ('run.cancelled', 'run.completed', 'run.failed'))) OR (\"session_event\".\"terminal_event_json\" IS NOT NULL AND json_valid(\"session_event\".\"terminal_event_json\") = 1 AND \"session_event\".\"semantic_hash\" IS NOT NULL AND \"session_event\".\"event_type\" IN ('run.cancelled', 'run.completed', 'run.failed'))" + }, + "session_event_tool_input_kind_check": { + "name": "session_event_tool_input_kind_check", + "value": "\"session_event\".\"tool_input_delta_json\" IS NULL OR \"session_event\".\"tool_input_json\" IS NULL" + }, + "session_event_tool_output_kind_check": { + "name": "session_event_tool_output_kind_check", + "value": "\"session_event\".\"tool_output_delta_text\" IS NULL OR \"session_event\".\"tool_output_text\" IS NULL" + }, + "session_event_tool_status_check": { + "name": "session_event_tool_status_check", + "value": "\"session_event\".\"tool_status\" IS NULL OR \"session_event\".\"tool_status\" IN ('running', 'completed', 'failed', 'cancelled')" + } + } + }, + "session_model_call": { + "name": "session_model_call", + "columns": { + "cache_creation_tokens": { + "name": "cache_creation_tokens", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "cache_read_tokens": { + "name": "cache_read_tokens", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "call_key": { + "name": "call_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "completed_at": { + "name": "completed_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "cost_currency": { + "name": "cost_currency", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "driver_instance_id": { + "name": "driver_instance_id", + "type": "text CHECK (\"driver_instance_id\" = upper(\"driver_instance_id\") AND length(\"driver_instance_id\") = 26 AND substr(\"driver_instance_id\", 1, 1) GLOB '[0-7]' AND \"driver_instance_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "error_code": { + "name": "error_code", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "id": { + "name": "id", + "type": "text CHECK (\"id\" = upper(\"id\") AND length(\"id\") = 26 AND substr(\"id\", 1, 1) GLOB '[0-7]' AND \"id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "input_tokens": { + "name": "input_tokens", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "metadata_json": { + "name": "metadata_json", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "native_call_id": { + "name": "native_call_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "output_tokens": { + "name": "output_tokens", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "source_event_seq": { + "name": "source_event_seq", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "session_id": { + "name": "session_id", + "type": "text CHECK (\"session_id\" = upper(\"session_id\") AND length(\"session_id\") = 26 AND substr(\"session_id\", 1, 1) GLOB '[0-7]' AND \"session_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "session_run_id": { + "name": "session_run_id", + "type": "text CHECK (\"session_run_id\" = upper(\"session_run_id\") AND length(\"session_run_id\") = 26 AND substr(\"session_run_id\", 1, 1) GLOB '[0-7]' AND \"session_run_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "started_at": { + "name": "started_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "total_cost_usd_micros": { + "name": "total_cost_usd_micros", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "trace_id": { + "name": "trace_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "session_model_call_run_created_idx": { + "name": "session_model_call_run_created_idx", + "columns": ["session_run_id", "created_at"], + "isUnique": false + }, + "session_model_call_session_created_idx": { + "name": "session_model_call_session_created_idx", + "columns": ["session_id", "created_at"], + "isUnique": false + }, + "session_model_call_run_key_idx": { + "name": "session_model_call_run_key_idx", + "columns": ["session_run_id", "call_key"], + "isUnique": true + }, + "session_model_call_native_idx": { + "name": "session_model_call_native_idx", + "columns": ["driver_instance_id", "native_call_id"], + "isUnique": true + } + }, + "foreignKeys": { + "session_model_call_session_id_session_id_fk": { + "name": "session_model_call_session_id_session_id_fk", + "tableFrom": "session_model_call", + "tableTo": "session", + "columnsFrom": ["session_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "session_model_call_session_run_id_session_run_id_fk": { + "name": "session_model_call_session_run_id_session_run_id_fk", + "tableFrom": "session_model_call", + "tableTo": "session_run", + "columnsFrom": ["session_run_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": { + "session_model_call_source_event_seq_check": { + "name": "session_model_call_source_event_seq_check", + "value": "\"session_model_call\".\"source_event_seq\" >= 0" + } + } + }, + "session_permission_request": { + "name": "session_permission_request", + "columns": { + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "driver_instance_id": { + "name": "driver_instance_id", + "type": "text CHECK (\"driver_instance_id\" = upper(\"driver_instance_id\") AND length(\"driver_instance_id\") = 26 AND substr(\"driver_instance_id\", 1, 1) GLOB '[0-7]' AND \"driver_instance_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "raw_input": { + "name": "raw_input", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "request_id": { + "name": "request_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "run_id": { + "name": "run_id", + "type": "text CHECK (\"run_id\" = upper(\"run_id\") AND length(\"run_id\") = 26 AND substr(\"run_id\", 1, 1) GLOB '[0-7]' AND \"run_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "session_id": { + "name": "session_id", + "type": "text CHECK (\"session_id\" = upper(\"session_id\") AND length(\"session_id\") = 26 AND substr(\"session_id\", 1, 1) GLOB '[0-7]' AND \"session_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "tool_call_id": { + "name": "tool_call_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "tool_kind": { + "name": "tool_kind", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "session_permission_request_run_idx": { + "name": "session_permission_request_run_idx", + "columns": ["session_id", "run_id"], + "isUnique": false + } + }, + "foreignKeys": { + "session_permission_request_session_id_session_id_fk": { + "name": "session_permission_request_session_id_session_id_fk", + "tableFrom": "session_permission_request", + "tableTo": "session", + "columnsFrom": ["session_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "session_permission_request_session_id_request_id_pk": { + "columns": ["session_id", "request_id"], + "name": "session_permission_request_session_id_request_id_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "session_readiness_snapshot": { + "name": "session_readiness_snapshot", + "columns": { + "readiness_json": { + "name": "readiness_json", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "session_id": { + "name": "session_id", + "type": "text CHECK (\"session_id\" = upper(\"session_id\") AND length(\"session_id\") = 26 AND substr(\"session_id\", 1, 1) GLOB '[0-7]' AND \"session_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "session_readiness_snapshot_session_id_session_id_fk": { + "name": "session_readiness_snapshot_session_id_session_id_fk", + "tableFrom": "session_readiness_snapshot", + "tableTo": "session", + "columnsFrom": ["session_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "skill_snapshot_entry": { + "name": "skill_snapshot_entry", + "columns": { + "entry_kind": { + "name": "entry_kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "is_executable": { + "name": "is_executable", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "mime_type": { + "name": "mime_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "path": { + "name": "path", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "sha256": { + "name": "sha256", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "size": { + "name": "size", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "snapshot_id": { + "name": "snapshot_id", + "type": "text CHECK (\"snapshot_id\" = upper(\"snapshot_id\") AND length(\"snapshot_id\") = 26 AND substr(\"snapshot_id\", 1, 1) GLOB '[0-7]' AND \"snapshot_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "skill_snapshot_entry_snapshot_id_path_pk": { + "columns": ["snapshot_id", "path"], + "name": "skill_snapshot_entry_snapshot_id_path_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "skill_snapshot": { + "name": "skill_snapshot", + "columns": { + "author": { + "name": "author", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "blob_key": { + "name": "blob_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "blob_sha256": { + "name": "blob_sha256", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "blob_size": { + "name": "blob_size", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "id": { + "name": "id", + "type": "text CHECK (\"id\" = upper(\"id\") AND length(\"id\") = 26 AND substr(\"id\", 1, 1) GLOB '[0-7]' AND \"id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "app_id": { + "name": "app_id", + "type": "text CHECK (\"app_id\" = upper(\"app_id\") AND length(\"app_id\") = 26 AND substr(\"app_id\", 1, 1) GLOB '[0-7]' AND \"app_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "skill_markdown_path": { + "name": "skill_markdown_path", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "uncompressed_size": { + "name": "uncompressed_size", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "version": { + "name": "version", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "skill_snapshot_app_created_at_idx": { + "name": "skill_snapshot_app_created_at_idx", + "columns": ["app_id", "created_at"], + "isUnique": false + }, + "skill_snapshot_blob_sha256_idx": { + "name": "skill_snapshot_blob_sha256_idx", + "columns": ["app_id", "blob_sha256"], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "skill": { + "name": "skill", + "columns": { + "author": { + "name": "author", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "current_snapshot_id": { + "name": "current_snapshot_id", + "type": "text CHECK (\"current_snapshot_id\" = upper(\"current_snapshot_id\") AND length(\"current_snapshot_id\") = 26 AND substr(\"current_snapshot_id\", 1, 1) GLOB '[0-7]' AND \"current_snapshot_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "forked_from_owner_name": { + "name": "forked_from_owner_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "forked_from_skill_id": { + "name": "forked_from_skill_id", + "type": "text CHECK (\"forked_from_skill_id\" = upper(\"forked_from_skill_id\") AND length(\"forked_from_skill_id\") = 26 AND substr(\"forked_from_skill_id\", 1, 1) GLOB '[0-7]' AND \"forked_from_skill_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "forked_from_skill_name": { + "name": "forked_from_skill_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "id": { + "name": "id", + "type": "text CHECK (\"id\" = upper(\"id\") AND length(\"id\") = 26 AND substr(\"id\", 1, 1) GLOB '[0-7]' AND \"id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "owner_account_id": { + "name": "owner_account_id", + "type": "text CHECK (\"owner_account_id\" = upper(\"owner_account_id\") AND length(\"owner_account_id\") = 26 AND substr(\"owner_account_id\", 1, 1) GLOB '[0-7]' AND \"owner_account_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "app_id": { + "name": "app_id", + "type": "text CHECK (\"app_id\" = upper(\"app_id\") AND length(\"app_id\") = 26 AND substr(\"app_id\", 1, 1) GLOB '[0-7]' AND \"app_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "source_kind": { + "name": "source_kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "version": { + "name": "version", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "skill_app_updated_at_idx": { + "name": "skill_app_updated_at_idx", + "columns": ["app_id", "updated_at"], + "isUnique": false + }, + "skill_owner_account_updated_at_idx": { + "name": "skill_owner_account_updated_at_idx", + "columns": ["owner_account_id", "updated_at"], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "account": { + "name": "account", + "columns": { + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "email_verified": { + "name": "email_verified", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "id": { + "name": "id", + "type": "text CHECK (\"id\" = upper(\"id\") AND length(\"id\") = 26 AND substr(\"id\", 1, 1) GLOB '[0-7]' AND \"id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "image_url": { + "name": "image_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_active_organization_id": { + "name": "last_active_organization_id", + "type": "text CHECK (\"last_active_organization_id\" = upper(\"last_active_organization_id\") AND length(\"last_active_organization_id\") = 26 AND substr(\"last_active_organization_id\", 1, 1) GLOB '[0-7]' AND \"last_active_organization_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "system_agent_model": { + "name": "system_agent_model", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "account_email_idx": { + "name": "account_email_idx", + "columns": ["email"], + "isUnique": true + }, + "account_last_active_organization_idx": { + "name": "account_last_active_organization_idx", + "columns": ["last_active_organization_id"], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "usage_daily_rollup": { + "name": "usage_daily_rollup", + "columns": { + "actor_user_id": { + "name": "actor_user_id", + "type": "text CHECK (\"actor_user_id\" = upper(\"actor_user_id\") AND length(\"actor_user_id\") = 26 AND substr(\"actor_user_id\", 1, 1) GLOB '[0-7]' AND \"actor_user_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "agent_id": { + "name": "agent_id", + "type": "text CHECK (\"agent_id\" = upper(\"agent_id\") AND length(\"agent_id\") = 26 AND substr(\"agent_id\", 1, 1) GLOB '[0-7]' AND \"agent_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "agent_owner_user_id": { + "name": "agent_owner_user_id", + "type": "text CHECK (\"agent_owner_user_id\" = upper(\"agent_owner_user_id\") AND length(\"agent_owner_user_id\") = 26 AND substr(\"agent_owner_user_id\", 1, 1) GLOB '[0-7]' AND \"agent_owner_user_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "agent_publication_state_at_run": { + "name": "agent_publication_state_at_run", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "cache_creation_tokens": { + "name": "cache_creation_tokens", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "cache_read_tokens": { + "name": "cache_read_tokens", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "date": { + "name": "date", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "input_tokens": { + "name": "input_tokens", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "organization_id": { + "name": "organization_id", + "type": "text CHECK (\"organization_id\" = upper(\"organization_id\") AND length(\"organization_id\") = 26 AND substr(\"organization_id\", 1, 1) GLOB '[0-7]' AND \"organization_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "app_id": { + "name": "app_id", + "type": "text CHECK (\"app_id\" = upper(\"app_id\") AND length(\"app_id\") = 26 AND substr(\"app_id\", 1, 1) GLOB '[0-7]' AND \"app_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "output_tokens": { + "name": "output_tokens", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "request_count": { + "name": "request_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "run_purpose": { + "name": "run_purpose", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "total_cost_usd_micros": { + "name": "total_cost_usd_micros", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "unpriced_request_count": { + "name": "unpriced_request_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "usage_daily_rollup_app_date_idx": { + "name": "usage_daily_rollup_app_date_idx", + "columns": ["app_id", "date"], + "isUnique": false + }, + "usage_daily_rollup_organization_date_idx": { + "name": "usage_daily_rollup_organization_date_idx", + "columns": ["organization_id", "date"], + "isUnique": false + }, + "usage_daily_rollup_agent_date_idx": { + "name": "usage_daily_rollup_agent_date_idx", + "columns": ["agent_id", "date"], + "isUnique": false + }, + "usage_daily_rollup_actor_date_idx": { + "name": "usage_daily_rollup_actor_date_idx", + "columns": ["actor_user_id", "date"], + "isUnique": false + }, + "usage_daily_rollup_owner_date_idx": { + "name": "usage_daily_rollup_owner_date_idx", + "columns": ["agent_owner_user_id", "date"], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "usage_daily_rollup_organization_id_app_id_agent_id_actor_user_id_agent_owner_user_id_date_agent_publication_state_at_run_run_purpose_provider_model_pk": { + "columns": [ + "organization_id", + "app_id", + "agent_id", + "actor_user_id", + "agent_owner_user_id", + "date", + "agent_publication_state_at_run", + "run_purpose", + "provider", + "model" + ], + "name": "usage_daily_rollup_organization_id_app_id_agent_id_actor_user_id_agent_owner_user_id_date_agent_publication_state_at_run_run_purpose_provider_model_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "usage_event_rollup_receipt": { + "name": "usage_event_rollup_receipt", + "columns": { + "rolled_up_at": { + "name": "rolled_up_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "source_event_id": { + "name": "source_event_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "usage_event_rollup_receipt_rolled_up_at_idx": { + "name": "usage_event_rollup_receipt_rolled_up_at_idx", + "columns": ["rolled_up_at"], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "usage_event_rollup_receipt_source_source_event_id_pk": { + "columns": ["source", "source_event_id"], + "name": "usage_event_rollup_receipt_source_source_event_id_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "usage_event": { + "name": "usage_event", + "columns": { + "actor_user_id": { + "name": "actor_user_id", + "type": "text CHECK (\"actor_user_id\" = upper(\"actor_user_id\") AND length(\"actor_user_id\") = 26 AND substr(\"actor_user_id\", 1, 1) GLOB '[0-7]' AND \"actor_user_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "agent_id": { + "name": "agent_id", + "type": "text CHECK (\"agent_id\" = upper(\"agent_id\") AND length(\"agent_id\") = 26 AND substr(\"agent_id\", 1, 1) GLOB '[0-7]' AND \"agent_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "agent_owner_user_id": { + "name": "agent_owner_user_id", + "type": "text CHECK (\"agent_owner_user_id\" = upper(\"agent_owner_user_id\") AND length(\"agent_owner_user_id\") = 26 AND substr(\"agent_owner_user_id\", 1, 1) GLOB '[0-7]' AND \"agent_owner_user_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "agent_publication_state_at_run": { + "name": "agent_publication_state_at_run", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "agent_revision_id": { + "name": "agent_revision_id", + "type": "text CHECK (\"agent_revision_id\" = upper(\"agent_revision_id\") AND length(\"agent_revision_id\") = 26 AND substr(\"agent_revision_id\", 1, 1) GLOB '[0-7]' AND \"agent_revision_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "cache_creation_tokens": { + "name": "cache_creation_tokens", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "cache_read_tokens": { + "name": "cache_read_tokens", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "id": { + "name": "id", + "type": "text CHECK (\"id\" = upper(\"id\") AND length(\"id\") = 26 AND substr(\"id\", 1, 1) GLOB '[0-7]' AND \"id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "input_tokens": { + "name": "input_tokens", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "organization_id": { + "name": "organization_id", + "type": "text CHECK (\"organization_id\" = upper(\"organization_id\") AND length(\"organization_id\") = 26 AND substr(\"organization_id\", 1, 1) GLOB '[0-7]' AND \"organization_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "app_id": { + "name": "app_id", + "type": "text CHECK (\"app_id\" = upper(\"app_id\") AND length(\"app_id\") = 26 AND substr(\"app_id\", 1, 1) GLOB '[0-7]' AND \"app_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "output_tokens": { + "name": "output_tokens", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "price_snapshot_json": { + "name": "price_snapshot_json", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "pricing_status": { + "name": "pricing_status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "run_purpose": { + "name": "run_purpose", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "runtime_id": { + "name": "runtime_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "session_id": { + "name": "session_id", + "type": "text CHECK (\"session_id\" = upper(\"session_id\") AND length(\"session_id\") = 26 AND substr(\"session_id\", 1, 1) GLOB '[0-7]' AND \"session_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "session_run_id": { + "name": "session_run_id", + "type": "text CHECK (\"session_run_id\" = upper(\"session_run_id\") AND length(\"session_run_id\") = 26 AND substr(\"session_run_id\", 1, 1) GLOB '[0-7]' AND \"session_run_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "source_event_id": { + "name": "source_event_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "source_event_seq": { + "name": "source_event_seq", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "total_cost_usd_micros": { + "name": "total_cost_usd_micros", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "usage_contract": { + "name": "usage_contract", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "usage_event_app_created_idx": { + "name": "usage_event_app_created_idx", + "columns": ["app_id", "created_at"], + "isUnique": false + }, + "usage_event_organization_created_idx": { + "name": "usage_event_organization_created_idx", + "columns": ["organization_id", "created_at"], + "isUnique": false + }, + "usage_event_agent_created_idx": { + "name": "usage_event_agent_created_idx", + "columns": ["agent_id", "created_at"], + "isUnique": false + }, + "usage_event_actor_created_idx": { + "name": "usage_event_actor_created_idx", + "columns": ["actor_user_id", "created_at"], + "isUnique": false + }, + "usage_event_owner_created_idx": { + "name": "usage_event_owner_created_idx", + "columns": ["agent_owner_user_id", "created_at"], + "isUnique": false + }, + "usage_event_session_run_idx": { + "name": "usage_event_session_run_idx", + "columns": ["session_run_id"], + "isUnique": false + }, + "usage_event_source_event_idx": { + "name": "usage_event_source_event_idx", + "columns": ["source", "source_event_id"], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": { + "usage_event_source_event_seq_check": { + "name": "usage_event_source_event_seq_check", + "value": "\"usage_event\".\"source_event_seq\" >= 0" + } + } + }, + "vendor_credential": { + "name": "vendor_credential", + "columns": { + "api_base": { + "name": "api_base", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "api_key_secret_id": { + "name": "api_key_secret_id", + "type": "text CHECK (\"api_key_secret_id\" = upper(\"api_key_secret_id\") AND length(\"api_key_secret_id\") = 26 AND substr(\"api_key_secret_id\", 1, 1) GLOB '[0-7]' AND \"api_key_secret_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "id": { + "name": "id", + "type": "text CHECK (\"id\" = upper(\"id\") AND length(\"id\") = 26 AND substr(\"id\", 1, 1) GLOB '[0-7]' AND \"id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "is_default": { + "name": "is_default", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "models": { + "name": "models", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "app_id": { + "name": "app_id", + "type": "text CHECK (\"app_id\" = upper(\"app_id\") AND length(\"app_id\") = 26 AND substr(\"app_id\", 1, 1) GLOB '[0-7]' AND \"app_id\" NOT GLOB '*[^0-9A-HJKMNP-TV-Z]*')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "vendor_id": { + "name": "vendor_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "vendor_credential_app_vendor_idx": { + "name": "vendor_credential_app_vendor_idx", + "columns": ["app_id", "vendor_id"], + "isUnique": false + }, + "vendor_credential_app_vendor_name_idx": { + "name": "vendor_credential_app_vendor_name_idx", + "columns": ["app_id", "vendor_id", "name"], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + } + }, + "views": {}, + "enums": {}, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + }, + "internal": { + "indexes": { + "file_record_listing_idx": { + "columns": { + "lower(\"name\")": { + "isExpression": true + } + } + }, + "session_run_terminal_reconciliation_attempt_idx": { + "columns": { + "coalesce(\"terminal_reconciliation_attempted_at\", \"updated_at\")": { + "isExpression": true + } + } + } + } + } +} diff --git a/pkgs/db/drizzle/meta/_journal.json b/pkgs/db/drizzle/meta/_journal.json index c626e569..dae0e0e7 100644 --- a/pkgs/db/drizzle/meta/_journal.json +++ b/pkgs/db/drizzle/meta/_journal.json @@ -92,6 +92,62 @@ "when": 1787948328549, "tag": "0012_agent-task-snapshot-state", "breakpoints": true + }, + { + "idx": 13, + "version": "6", + "when": 1788015369618, + "tag": "0013_durable-mcp-effect-v3", + "breakpoints": true + }, + { + "idx": 14, + "version": "6", + "when": 1788018895233, + "tag": "0014_session-event-stream-identity", + "breakpoints": true + }, + { + "idx": 15, + "version": "6", + "when": 1788043914090, + "tag": "0015_session-cleanup-operation", + "breakpoints": true + }, + { + "idx": 16, + "version": "6", + "when": 1788053277514, + "tag": "0016_durable-event-side-effects", + "breakpoints": true + }, + { + "idx": 17, + "version": "6", + "when": 1788069417581, + "tag": "0017_terminal-reconciliation-scheduling", + "breakpoints": true + }, + { + "idx": 18, + "version": "6", + "when": 1788081629455, + "tag": "0018_runtime-operation-ready-authority", + "breakpoints": true + }, + { + "idx": 19, + "version": "6", + "when": 1788124070926, + "tag": "0019_runtime-subject-operation-authority", + "breakpoints": true + }, + { + "idx": 20, + "version": "6", + "when": 1788154612356, + "tag": "0020_sandbox-backup-object-authority", + "breakpoints": true } ] } diff --git a/pkgs/db/package.json b/pkgs/db/package.json index fa871f7a..9b042817 100644 --- a/pkgs/db/package.json +++ b/pkgs/db/package.json @@ -3,11 +3,13 @@ "private": true, "type": "module", "exports": { - ".": "./src/index.ts" + ".": "./src/index.ts", + "./deploy-schema-guard": "./src/deploy-schema-guard.ts" }, "scripts": { "db:generate": "vp exec drizzle-kit generate --config drizzle.config.ts", "lint": "vp lint .", + "schema:check": "vp exec bun scripts/check-schema.ts", "tc": "vp exec tsc --noEmit", "test": "vp exec bun test tests" }, @@ -20,6 +22,6 @@ "@types/node": "^25.8.0", "drizzle-kit": "^0.31.10", "typescript": "^6.0.3", - "vite-plus": "^0.1.23" + "vite-plus": "^0.3.0" } } diff --git a/pkgs/db/scripts/check-schema.ts b/pkgs/db/scripts/check-schema.ts new file mode 100644 index 00000000..e00c2599 --- /dev/null +++ b/pkgs/db/scripts/check-schema.ts @@ -0,0 +1,51 @@ +#!/usr/bin/env bun +import { readFile } from "node:fs/promises"; +import { isDeepStrictEqual } from "node:util"; + +import { generateSQLiteDrizzleJson } from "drizzle-kit/api"; + +import { + assertProdSchemaMatches, + createProdSchemaCatalogFromDrizzleSnapshot, + DRIZZLE_MIGRATION_PROD_SCHEMA_CATALOG, +} from "../src/deploy-schema-guard"; +import * as schema from "../src/index"; +import { latestDrizzleSnapshotFilename } from "./drizzle-migrations"; + +function comparableSnapshot(value: Record): unknown { + const { _meta, id, prevId, ...snapshot } = value; + void _meta; + void id; + void prevId; + return snapshot; +} + +const metaDirectory = new URL("../drizzle/meta/", import.meta.url); +const snapshotFilename = latestDrizzleSnapshotFilename; +const checkedIn = JSON.parse( + await readFile(new URL(snapshotFilename, metaDirectory), "utf8"), +) as Record; +const generated = JSON.parse(JSON.stringify(await generateSQLiteDrizzleJson(schema))) as Record< + string, + unknown +>; + +if (!isDeepStrictEqual(comparableSnapshot(checkedIn), comparableSnapshot(generated))) { + throw new Error( + `Drizzle source schema differs from ${snapshotFilename}; generate and review an append-only migration.`, + ); +} + +assertProdSchemaMatches( + createProdSchemaCatalogFromDrizzleSnapshot(checkedIn), + DRIZZLE_MIGRATION_PROD_SCHEMA_CATALOG, +); + +const args = process.argv.slice(2); +if (args.length === 0) { + process.stdout.write(`Drizzle source schema matches ${snapshotFilename}.\n`); +} else if (args.length === 1 && args[0] === "--catalog") { + process.stdout.write(`${JSON.stringify(checkedIn)}\n`); +} else { + throw new Error("Usage: bun pkgs/db/scripts/check-schema.ts [--catalog]"); +} diff --git a/pkgs/db/scripts/drizzle-migrations.ts b/pkgs/db/scripts/drizzle-migrations.ts new file mode 100644 index 00000000..67458808 --- /dev/null +++ b/pkgs/db/scripts/drizzle-migrations.ts @@ -0,0 +1,128 @@ +import { readdirSync, readFileSync } from "node:fs"; +import { fileURLToPath } from "node:url"; + +import { readMigrationFiles } from "drizzle-orm/migrator"; + +export interface MigrationDatabase { + execute(sql: string): void; +} + +export interface MigrationJournalEntry { + readonly idx: number; + readonly tag: string; +} + +interface MigrationJournal { + readonly entries: readonly MigrationJournalEntry[]; +} + +const migrationsFolder = fileURLToPath(new URL("../drizzle/", import.meta.url)); +const journal = JSON.parse( + readFileSync(new URL("../drizzle/meta/_journal.json", import.meta.url), "utf8"), +) as MigrationJournal; +const files = readMigrationFiles({ migrationsFolder }); +const migrationTagPattern = /^\d{4}_[A-Za-z0-9][A-Za-z0-9_-]*$/u; + +export function assertDrizzleMigrationFiles( + entries: readonly MigrationJournalEntry[], + sqlFilenames: readonly string[], + loadedFileCount: number, +): void { + const expectedFilenames = entries.map(({ tag }) => `${tag}.sql`); + if ( + entries.length === 0 || + loadedFileCount !== entries.length || + new Set(expectedFilenames).size !== entries.length || + entries.some( + ({ idx, tag }, position) => + idx !== position || + !tag.startsWith(`${String(position).padStart(4, "0")}_`) || + !migrationTagPattern.test(tag), + ) || + sqlFilenames.length !== expectedFilenames.length || + expectedFilenames.some((filename, index) => sqlFilenames[index] !== filename) + ) { + throw new Error("The Drizzle migration journal must exactly match its ordered SQL files."); + } +} + +assertDrizzleMigrationFiles( + journal.entries, + readdirSync(migrationsFolder) + .filter((filename) => filename.endsWith(".sql")) + .toSorted(), + files.length, +); + +export const drizzleMigrations = journal.entries.map((entry, index) => { + const file = files[index]; + if (file === undefined) throw new Error(`Migration ${entry.tag} has no SQL file.`); + return { + bps: file.bps, + folderMillis: file.folderMillis, + hash: file.hash, + index, + sql: file.sql.filter((statement) => statement.trim() !== ""), + tag: entry.tag, + }; +}); + +export const latestDrizzleSnapshotFilename = `${String(drizzleMigrations.length - 1).padStart( + 4, + "0", +)}_snapshot.json`; + +export function getDrizzleMigration(tag: string) { + const migration = drizzleMigrations.find((candidate) => candidate.tag === tag); + if (migration === undefined) { + throw new Error(`Migration ${tag} is missing from the Drizzle journal.`); + } + return migration; +} + +export function applyDrizzleMigration(database: MigrationDatabase, tag: string): void { + const migration = getDrizzleMigration(tag); + + database.execute("BEGIN"); + try { + for (const statement of migration.sql) database.execute(statement.trim()); + database.execute("COMMIT"); + } catch (error) { + database.execute("ROLLBACK"); + throw error; + } +} + +export function applyDrizzleMigrations(database: MigrationDatabase): void { + for (const migration of drizzleMigrations) { + applyDrizzleMigration(database, migration.tag); + } +} + +export async function applyDrizzleMigrationAsync( + database: MigrationDatabase, + tag: string, +): Promise { + applyDrizzleMigration(database, tag); +} + +export function applyDrizzleMigrationsBefore(database: MigrationDatabase, tag: string): void { + const target = getDrizzleMigration(tag); + for (const migration of drizzleMigrations.slice(0, target.index)) { + applyDrizzleMigration(database, migration.tag); + } +} + +export function applyDrizzleMigrationsFrom(database: MigrationDatabase, tag: string): void { + const target = getDrizzleMigration(tag); + for (const migration of drizzleMigrations.slice(target.index)) { + applyDrizzleMigration(database, migration.tag); + } +} + +export function applyDrizzleMigrationsThrough(database: MigrationDatabase, tag: string): void { + const target = getDrizzleMigration(tag); + for (const migration of drizzleMigrations.slice(0, target.index + 1)) { + applyDrizzleMigration(database, migration.tag); + } +} diff --git a/pkgs/db/src/deploy-schema-guard.ts b/pkgs/db/src/deploy-schema-guard.ts new file mode 100644 index 00000000..27245ad7 --- /dev/null +++ b/pkgs/db/src/deploy-schema-guard.ts @@ -0,0 +1,841 @@ +import { DatabaseSync } from "node:sqlite"; + +import { applyDrizzleMigrations } from "../scripts/drizzle-migrations"; + +export interface ProdSchemaColumn { + readonly defaultValue: string | null; + readonly hidden: 0 | 1 | 2 | 3; + readonly name: string; + readonly notNull: boolean; + readonly primaryKeyPosition: number; + readonly type: string; +} + +export interface ProdSchemaIndex { + readonly columns: readonly string[]; + readonly name: string; + readonly predicate: string | null; + readonly unique: boolean; +} + +export interface ProdSchemaForeignKey { + readonly columnsFrom: readonly string[]; + readonly columnsTo: readonly string[]; + readonly onDelete: string; + readonly onUpdate: string; + readonly tableTo: string; +} + +export interface ProdSchemaCheck { + readonly expression: string; +} + +export interface ProdSchemaTrigger { + readonly name: string; + readonly sql: string; + readonly tableName: string; +} + +export interface ProdSchemaTable { + readonly autoincrement: boolean; + readonly checks: readonly ProdSchemaCheck[]; + readonly columns: readonly ProdSchemaColumn[]; + readonly definition: string | null; + readonly foreignKeys: readonly ProdSchemaForeignKey[]; + readonly indexes: readonly ProdSchemaIndex[]; + readonly name: string; +} + +export interface ProdSchemaCatalog { + readonly tables: readonly ProdSchemaTable[]; + readonly triggers: readonly ProdSchemaTrigger[]; +} + +type Row = Readonly>; + +const PROD_SCHEMA_TABLE_INTROSPECTION_SQL = `SELECT name, sql +FROM sqlite_master +WHERE type = 'table' AND name NOT LIKE 'sqlite_%' +ORDER BY name`; +const PROD_SCHEMA_TRIGGER_INTROSPECTION_SQL = `SELECT name, tbl_name AS table_name, sql +FROM sqlite_master +WHERE type = 'trigger' +ORDER BY name`; +const PROD_SCHEMA_INTROSPECTION_STATEMENT_COUNT = 5; +const PROD_SCHEMA_TABLES_PER_STATEMENT = 5; + +interface ProdSchemaIntrospectionPlan { + readonly statements: readonly string[]; + readonly tableChunkCount: number; +} + +function schemaString(value: string): string { + return `'${value.replaceAll("'", "''")}'`; +} + +function createProdSchemaIntrospectionPlan( + tableNames: readonly string[], +): ProdSchemaIntrospectionPlan { + const names = tableNames.toSorted(); + if ( + names.length === 0 || + new Set(names).size !== names.length || + names.some((name) => typeof name !== "string" || name.length === 0 || name.includes("\0")) + ) { + throw new Error("Production schema introspection requires unique table names."); + } + + const chunks = Array.from( + { length: Math.ceil(names.length / PROD_SCHEMA_TABLES_PER_STATEMENT) }, + (_, index) => + names.slice( + index * PROD_SCHEMA_TABLES_PER_STATEMENT, + (index + 1) * PROD_SCHEMA_TABLES_PER_STATEMENT, + ), + ); + + return { + statements: [ + PROD_SCHEMA_TABLE_INTROSPECTION_SQL, + ...chunks.map((chunk) => + chunk + .map((name) => { + const table = schemaString(name); + return `SELECT ${table} AS table_name, info.name, info.type, +info."notnull" AS not_null, info.dflt_value, info.pk, info.hidden +FROM pragma_table_xinfo(${table}) AS info`; + }) + .join("\nUNION ALL\n"), + ), + ...chunks.map((chunk) => + chunk + .map((name) => { + const table = schemaString(name); + return `SELECT ${table} AS table_name, indexes.name, indexes."unique" AS is_unique, +indexes.origin, indexes.partial, schema.sql +FROM pragma_index_list(${table}) AS indexes +LEFT JOIN sqlite_master AS schema + ON schema.type = 'index' AND schema.name = indexes.name +WHERE indexes.origin <> 'pk'`; + }) + .join("\nUNION ALL\n"), + ), + ...chunks.map((chunk) => + chunk + .map((name) => { + const table = schemaString(name); + return `SELECT ${table} AS table_name, foreign_keys.id, foreign_keys.seq, +foreign_keys."table" AS table_to, foreign_keys."from" AS column_from, +foreign_keys."to" AS column_to, foreign_keys.on_update, foreign_keys.on_delete +FROM pragma_foreign_key_list(${table}) AS foreign_keys`; + }) + .join("\nUNION ALL\n"), + ), + PROD_SCHEMA_TRIGGER_INTROSPECTION_SQL, + ], + tableChunkCount: chunks.length, + }; +} + +export function createProdSchemaIntrospectionStatements( + tableNames: readonly string[], +): readonly string[] { + return createProdSchemaIntrospectionPlan(tableNames).statements; +} + +interface SqlToken { + readonly kind: "string" | "symbol" | "word"; + readonly value: string; +} + +function tokenizeSql(sql: string): SqlToken[] { + const tokens: SqlToken[] = []; + + for (let index = 0; index < sql.length;) { + const character = sql[index] ?? ""; + if (/\s/u.test(character)) { + index += 1; + continue; + } + if (character === "'") { + const start = index; + index += 1; + while (index < sql.length) { + if (sql[index] !== "'") { + index += 1; + continue; + } + if (sql[index + 1] === "'") { + index += 2; + continue; + } + index += 1; + break; + } + if (sql[index - 1] !== "'") throw new Error("SQL contains an unterminated string literal."); + tokens.push({ kind: "string", value: sql.slice(start, index) }); + continue; + } + if (character === '"' || character === "`" || character === "[") { + const closing = character === "[" ? "]" : character; + let value = ""; + index += 1; + while (index < sql.length) { + if (sql[index] !== closing) { + value += sql[index]; + index += 1; + continue; + } + if (character !== "[" && sql[index + 1] === closing) { + value += closing; + index += 2; + continue; + } + index += 1; + break; + } + if (sql[index - 1] !== closing) throw new Error("SQL contains an unterminated identifier."); + tokens.push({ kind: "word", value: value.toLowerCase() }); + continue; + } + if (/[A-Za-z0-9_$]/u.test(character)) { + const start = index; + while (index < sql.length && /[A-Za-z0-9_$]/u.test(sql[index] ?? "")) index += 1; + tokens.push({ kind: "word", value: sql.slice(start, index).toLowerCase() }); + continue; + } + + const pair = sql.slice(index, index + 2); + if (["!=", "<=", "<>", ">=", "||", "->"].includes(pair)) { + tokens.push({ kind: "symbol", value: pair }); + index += 2; + continue; + } + tokens.push({ kind: "symbol", value: character }); + index += 1; + } + + return tokens; +} + +export function normalizeSql(sql: string): string { + return tokenizeSql(sql) + .filter((token) => token.value !== ";") + .map((token) => token.value) + .join(" "); +} + +function normalizeExpressionTokens(tokens: readonly SqlToken[], tableName: string): string { + const normalized: string[] = []; + for (let index = 0; index < tokens.length; index += 1) { + if (tokens[index]?.value === tableName.toLowerCase() && tokens[index + 1]?.value === ".") { + index += 1; + continue; + } + const value = tokens[index]?.value; + normalized.push(value === "true" ? "1" : value === "false" ? "0" : (value ?? "")); + } + return normalized.filter((value) => value !== ";").join(" "); +} + +function normalizeExpression(sql: string, tableName: string): string { + return normalizeExpressionTokens(tokenizeSql(sql), tableName); +} + +function extractChecks(sql: string, tableName: string): ProdSchemaCheck[] { + const tokens = tokenizeSql(sql); + const checks: ProdSchemaCheck[] = []; + + for (let index = 0; index < tokens.length; index += 1) { + if (tokens[index]?.kind !== "word" || tokens[index]?.value !== "check") continue; + if (tokens[index + 1]?.value !== "(") throw new Error("CHECK is missing its expression."); + + let depth = 1; + let end = index + 2; + while (end < tokens.length && depth > 0) { + if (tokens[end]?.value === "(") depth += 1; + if (tokens[end]?.value === ")") depth -= 1; + end += 1; + } + if (depth !== 0) throw new Error("CHECK contains unbalanced parentheses."); + + checks.push({ + expression: normalizeExpressionTokens(tokens.slice(index + 2, end - 1), tableName), + }); + index = end - 1; + } + + return checks.toSorted((left, right) => left.expression.localeCompare(right.expression)); +} + +function indexPredicate(sql: string | null, tableName: string): string | null { + if (sql === null) return null; + const tokens = tokenizeSql(sql); + const where = tokens.findIndex((token) => token.kind === "word" && token.value === "where"); + return where === -1 ? null : normalizeExpressionTokens(tokens.slice(where + 1), tableName); +} + +function indexColumns(sql: string, tableName: string): string[] { + const tokens = tokenizeSql(sql); + const on = tokens.findIndex((token) => token.kind === "word" && token.value === "on"); + if (on === -1 || tokens[on + 2]?.value !== "(") { + throw new Error(`Schema index on ${tableName} is missing its key columns.`); + } + + const columns: string[] = []; + let depth = 1; + let start = on + 3; + for (let index = start; index < tokens.length; index += 1) { + const value = tokens[index]?.value; + if (value === "(") depth += 1; + if (value === ")") depth -= 1; + if ((value === "," && depth === 1) || depth === 0) { + const column = normalizeExpressionTokens(tokens.slice(start, index), tableName); + if (!column) throw new Error(`Schema index on ${tableName} contains an empty key.`); + columns.push(column); + start = index + 1; + } + if (depth === 0) return columns; + } + throw new Error(`Schema index on ${tableName} contains unbalanced parentheses.`); +} + +function requireString(row: Row, key: string): string { + const value = row[key]; + if (typeof value !== "string") throw new Error(`Schema field ${key} must be a string.`); + return value; +} + +function requireNullableString(row: Row, key: string): string | null { + const value = row[key]; + if (value !== null && typeof value !== "string") { + throw new Error(`Schema field ${key} must be a string or null.`); + } + return value; +} + +function requireInteger(row: Row, key: string): number { + const value = row[key]; + if (!Number.isSafeInteger(value)) throw new Error(`Schema field ${key} must be an integer.`); + return value as number; +} + +function requireBit(row: Row, key: string): boolean { + const value = requireInteger(row, key); + if (value !== 0 && value !== 1) throw new Error(`Schema field ${key} must be 0 or 1.`); + return value === 1; +} + +function requireHiddenColumnKind(row: Row): 0 | 1 | 2 | 3 { + const value = requireInteger(row, "hidden"); + if (value !== 0 && value !== 1 && value !== 2 && value !== 3) { + throw new Error("Schema field hidden must be between 0 and 3."); + } + return value; +} + +function requireRows(value: unknown): Row[] { + if (!Array.isArray(value)) throw new Error("Schema introspection result must be an array."); + return value.map((row) => { + if (typeof row !== "object" || row === null || Array.isArray(row)) { + throw new Error("Schema introspection row must be an object."); + } + return row as Row; + }); +} + +function aggregateProdSchemaIntrospectionRows( + statementRows: readonly unknown[], + tableNames: readonly string[], +): Row[][] { + const { statements, tableChunkCount } = createProdSchemaIntrospectionPlan(tableNames); + if (statementRows.length !== statements.length) { + throw new Error("Schema introspection has an unexpected statement count."); + } + const rows = statementRows.map(requireRows); + const indexStart = 1 + tableChunkCount; + const foreignKeyStart = indexStart + tableChunkCount; + const triggerIndex = foreignKeyStart + tableChunkCount; + return [ + rows[0] ?? [], + rows.slice(1, indexStart).flat(), + rows.slice(indexStart, foreignKeyStart).flat(), + rows.slice(foreignKeyStart, triggerIndex).flat(), + rows[triggerIndex] ?? [], + ]; +} + +export function createProdSchemaCatalogFromIntrospectionRows( + statementRows: readonly unknown[], + tableNames: readonly string[], +): ProdSchemaCatalog { + return createProdSchemaCatalog(aggregateProdSchemaIntrospectionRows(statementRows, tableNames)); +} + +export function parseGeneratedProdSchemaCatalog(raw: string): ProdSchemaCatalog { + const catalog = createProdSchemaCatalogFromDrizzleSnapshot(JSON.parse(raw)); + const definitions = new Map( + DRIZZLE_MIGRATION_PROD_SCHEMA_CATALOG.tables.map(({ definition, name }) => [name, definition]), + ); + return { + ...catalog, + tables: catalog.tables.map((table) => { + const definition = definitions.get(table.name); + if (definition === undefined || definition === null) { + throw new Error(`Drizzle snapshot table ${table.name} has no migration definition.`); + } + return Object.assign(table, { definition }); + }), + }; +} + +export function createProdSchemaCatalog(statementRows: readonly unknown[]): ProdSchemaCatalog { + if (statementRows.length !== PROD_SCHEMA_INTROSPECTION_STATEMENT_COUNT) { + throw new Error("Schema introspection has an unexpected statement count."); + } + const [tableRows, columnRows, indexRows, foreignKeyRows, triggerRows] = + statementRows.map(requireRows); + if (!tableRows || !columnRows || !indexRows || !foreignKeyRows || !triggerRows) { + throw new Error("Schema introspection is incomplete."); + } + + const columnsByTable = Map.groupBy(columnRows, (row) => requireString(row, "table_name")); + const indexesByTable = Map.groupBy(indexRows, (row) => requireString(row, "table_name")); + const foreignKeysByTable = Map.groupBy(foreignKeyRows, (row) => requireString(row, "table_name")); + const seenTables = new Set(); + + const tables = tableRows.map((tableRow): ProdSchemaTable => { + const name = requireString(tableRow, "name"); + const sql = requireString(tableRow, "sql"); + if (seenTables.has(name)) throw new Error(`Schema contains duplicate table ${name}.`); + seenTables.add(name); + + const seenColumns = new Set(); + const columns = (columnsByTable.get(name) ?? []).map((row): ProdSchemaColumn => { + const columnName = requireString(row, "name"); + if (seenColumns.has(columnName)) { + throw new Error(`Schema table ${name} contains duplicate column ${columnName}.`); + } + seenColumns.add(columnName); + const defaultValue = requireNullableString(row, "dflt_value"); + return { + defaultValue: defaultValue === null ? null : normalizeExpression(defaultValue, name), + hidden: requireHiddenColumnKind(row), + name: columnName, + notNull: requireBit(row, "not_null"), + primaryKeyPosition: requireInteger(row, "pk"), + type: normalizeSql(requireString(row, "type")), + }; + }); + + const indexes = (indexesByTable.get(name) ?? []).map((row): ProdSchemaIndex => { + const indexName = requireString(row, "name"); + if (requireString(row, "origin") !== "c") { + throw new Error(`Schema table ${name} contains unsupported inline UNIQUE constraints.`); + } + const sqlValue = requireNullableString(row, "sql"); + if (sqlValue === null) throw new Error(`Schema index ${indexName} has no SQL definition.`); + const partial = requireBit(row, "partial"); + const predicate = indexPredicate(sqlValue, name); + if (partial !== (predicate !== null)) { + throw new Error(`Schema index ${indexName} has inconsistent partial-index metadata.`); + } + return { + columns: indexColumns(sqlValue, name), + name: indexName, + predicate, + unique: requireBit(row, "is_unique"), + }; + }); + + const foreignKeyGroups = Map.groupBy(foreignKeysByTable.get(name) ?? [], (row) => + requireInteger(row, "id"), + ); + const foreignKeys = [...foreignKeyGroups.values()].map((rows): ProdSchemaForeignKey => { + const ordered = rows.toSorted( + (left, right) => requireInteger(left, "seq") - requireInteger(right, "seq"), + ); + const first = ordered[0]; + if (!first) throw new Error(`Schema table ${name} contains an empty foreign key.`); + for (const [position, row] of ordered.entries()) { + if (requireInteger(row, "seq") !== position) { + throw new Error(`Schema table ${name} contains a non-contiguous foreign key.`); + } + } + return { + columnsFrom: ordered.map((row) => requireString(row, "column_from")), + columnsTo: ordered.map((row) => requireString(row, "column_to")), + onDelete: requireString(first, "on_delete").toLowerCase(), + onUpdate: requireString(first, "on_update").toLowerCase(), + tableTo: requireString(first, "table_to"), + }; + }); + + return { + autoincrement: tokenizeSql(sql).some((token) => token.value === "autoincrement"), + checks: extractChecks(sql, name), + columns: columns.toSorted((left, right) => left.name.localeCompare(right.name)), + definition: normalizeSql(sql), + foreignKeys: foreignKeys.toSorted((left, right) => + JSON.stringify(left).localeCompare(JSON.stringify(right)), + ), + indexes: indexes.toSorted((left, right) => left.name.localeCompare(right.name)), + name, + }; + }); + + const triggers = triggerRows.map((row): ProdSchemaTrigger => ({ + name: requireString(row, "name"), + sql: requireString(row, "sql"), + tableName: requireString(row, "table_name"), + })); + + return { + tables: tables.toSorted((left, right) => left.name.localeCompare(right.name)), + triggers: triggers.toSorted((left, right) => left.name.localeCompare(right.name)), + }; +} + +function requireObject(value: unknown, label: string): Record { + if (typeof value !== "object" || value === null || Array.isArray(value)) { + throw new Error(`${label} must be an object.`); + } + return value as Record; +} + +function requireBoolean(value: unknown, label: string): boolean { + if (typeof value !== "boolean") throw new Error(`${label} must be a boolean.`); + return value; +} + +function requireStringArray(value: unknown, label: string): string[] { + if (!Array.isArray(value) || value.some((entry) => typeof entry !== "string")) { + throw new Error(`${label} must be a string array.`); + } + return value as string[]; +} + +function snapshotDefault(column: Record, tableName: string): string | null { + if (!("default" in column)) return null; + const value = column["default"]; + if (typeof value !== "string" && typeof value !== "number" && typeof value !== "boolean") { + throw new Error(`Drizzle column default in ${tableName} is invalid.`); + } + return normalizeExpression(String(value), tableName); +} + +function snapshotHiddenColumnKind( + column: Record, + tableName: string, + columnName: string, +): 0 | 2 | 3 { + if (!("generated" in column)) return 0; + const generated = requireObject( + column["generated"], + `Drizzle generated column ${tableName}.${columnName}`, + ); + if (typeof generated["as"] !== "string") { + throw new Error(`Drizzle generated column ${tableName}.${columnName} has no expression.`); + } + if (generated["type"] === "virtual") return 2; + if (generated["type"] === "stored") return 3; + throw new Error(`Drizzle generated column ${tableName}.${columnName} has an invalid mode.`); +} + +export function createProdSchemaCatalogFromDrizzleSnapshot(value: unknown): ProdSchemaCatalog { + const snapshot = requireObject(value, "Drizzle snapshot"); + const snapshotTables = requireObject(snapshot["tables"], "Drizzle snapshot tables"); + if (Object.keys(snapshotTables).length === 0) throw new Error("Drizzle snapshot has no tables."); + + const tables = Object.entries(snapshotTables).map(([tableKey, rawTable]): ProdSchemaTable => { + const table = requireObject(rawTable, `Drizzle table ${tableKey}`); + const name = table["name"]; + if (name !== tableKey) + throw new Error(`Drizzle table key ${tableKey} does not match its name.`); + const snapshotColumns = requireObject(table["columns"], `Drizzle columns for ${name}`); + if ( + Object.keys( + requireObject(table["uniqueConstraints"], `Drizzle unique constraints for ${name}`), + ).length > 0 + ) { + throw new Error(`Drizzle table ${name} contains unsupported inline UNIQUE constraints.`); + } + const compositePrimaryKeys = Object.values( + requireObject(table["compositePrimaryKeys"], `Drizzle primary keys for ${name}`), + ); + if (compositePrimaryKeys.length > 1) { + throw new Error(`Drizzle table ${name} contains multiple composite primary keys.`); + } + const compositeColumns = + compositePrimaryKeys.length === 0 + ? [] + : requireStringArray( + requireObject(compositePrimaryKeys[0], `Drizzle primary key for ${name}`)["columns"], + `Drizzle primary key columns for ${name}`, + ); + + const checks: ProdSchemaCheck[] = []; + const columns = Object.entries(snapshotColumns).map( + ([columnKey, rawColumn]): ProdSchemaColumn => { + const column = requireObject(rawColumn, `Drizzle column ${name}.${columnKey}`); + if (column["name"] !== columnKey) { + throw new Error(`Drizzle column key ${name}.${columnKey} does not match its name.`); + } + const declaredType = column["type"]; + if (typeof declaredType !== "string") { + throw new Error(`Drizzle column ${name}.${columnKey} has no type.`); + } + checks.push(...extractChecks(declaredType, name)); + const typeTokens = tokenizeSql(declaredType); + const checkIndex = typeTokens.findIndex( + (token) => token.kind === "word" && token.value === "check", + ); + const type = (checkIndex === -1 ? typeTokens : typeTokens.slice(0, checkIndex)) + .map((token) => token.value) + .join(" "); + const primaryKey = requireBoolean( + column["primaryKey"], + `Drizzle primary-key flag for ${name}.${columnKey}`, + ); + const compositePosition = compositeColumns.indexOf(columnKey); + if (primaryKey && compositePosition !== -1) { + throw new Error(`Drizzle column ${name}.${columnKey} has conflicting primary keys.`); + } + return { + defaultValue: snapshotDefault(column, name), + hidden: snapshotHiddenColumnKind(column, name, columnKey), + name: columnKey, + notNull: requireBoolean( + column["notNull"], + `Drizzle nullability for ${name}.${columnKey}`, + ), + primaryKeyPosition: primaryKey ? 1 : compositePosition === -1 ? 0 : compositePosition + 1, + type, + }; + }, + ); + + for (const rawCheck of Object.values( + requireObject(table["checkConstraints"], `Drizzle checks for ${name}`), + )) { + const check = requireObject(rawCheck, `Drizzle check for ${name}`); + if (typeof check["value"] !== "string") { + throw new Error(`Drizzle check for ${name} has no expression.`); + } + checks.push({ expression: normalizeExpression(check["value"], name) }); + } + + const indexes = Object.entries( + requireObject(table["indexes"], `Drizzle indexes for ${name}`), + ).map(([indexKey, rawIndex]): ProdSchemaIndex => { + const index = requireObject(rawIndex, `Drizzle index ${indexKey}`); + if (index["name"] !== indexKey) { + throw new Error(`Drizzle index key ${indexKey} does not match its name.`); + } + const where = index["where"]; + if (where !== undefined && typeof where !== "string") { + throw new Error(`Drizzle index ${indexKey} has an invalid predicate.`); + } + return { + columns: requireStringArray(index["columns"], `Drizzle index columns for ${indexKey}`).map( + (column) => normalizeExpression(column, name), + ), + name: indexKey, + predicate: where === undefined ? null : normalizeExpression(where, name), + unique: requireBoolean(index["isUnique"], `Drizzle uniqueness for ${indexKey}`), + }; + }); + + const foreignKeys = Object.values( + requireObject(table["foreignKeys"], `Drizzle foreign keys for ${name}`), + ).map((rawForeignKey): ProdSchemaForeignKey => { + const foreignKey = requireObject(rawForeignKey, `Drizzle foreign key for ${name}`); + if ( + typeof foreignKey["tableTo"] !== "string" || + typeof foreignKey["onDelete"] !== "string" || + typeof foreignKey["onUpdate"] !== "string" + ) { + throw new Error(`Drizzle foreign key for ${name} is incomplete.`); + } + return { + columnsFrom: requireStringArray( + foreignKey["columnsFrom"], + `Drizzle foreign-key source columns for ${name}`, + ), + columnsTo: requireStringArray( + foreignKey["columnsTo"], + `Drizzle foreign-key target columns for ${name}`, + ), + onDelete: foreignKey["onDelete"], + onUpdate: foreignKey["onUpdate"], + tableTo: foreignKey["tableTo"], + }; + }); + + return { + autoincrement: Object.values(snapshotColumns).some( + (rawColumn) => + requireObject(rawColumn, `Drizzle column for ${name}`)["autoincrement"] === true, + ), + checks: checks.toSorted((left, right) => left.expression.localeCompare(right.expression)), + columns: columns.toSorted((left, right) => left.name.localeCompare(right.name)), + definition: null, + foreignKeys: foreignKeys.toSorted((left, right) => + JSON.stringify(left).localeCompare(JSON.stringify(right)), + ), + indexes: indexes.toSorted((left, right) => left.name.localeCompare(right.name)), + name, + }; + }); + + return { + tables: tables.toSorted((left, right) => left.name.localeCompare(right.name)), + triggers: MANAGED_PROD_SCHEMA_TRIGGERS, + }; +} + +// The channels subsystem was removed by #577 without a destructive data migration. +const RETAINED_LEGACY_TABLES = new Set([ + "agent_channel_binding", + "channel_event_receipt", + "channel_final_delivery_job", + "channel_runtime_state", + "channel_thread_session", + "wechat_channel_account", + "wechat_channel_pairing", + "wechat_context_token", +]); + +const PROTOCOL_V3_CUTOVER_TRIGGER_PREFIX = "__protocol_v3_cutover_"; +export const PROTOCOL_V3_MIGRATION_INTENT_TABLE = "__protocol_v3_migration_intent"; +export const PROTOCOL_V3_MIGRATION_INTENT_TABLE_SQL = `CREATE TABLE "${PROTOCOL_V3_MIGRATION_INTENT_TABLE}" ( + "id" integer PRIMARY KEY CHECK ("id" = 1) REFERENCES "__protocol_v3_cutover" ("id") ON DELETE CASCADE, + "started_at" integer NOT NULL DEFAULT (CAST(unixepoch('subsec') * 1000 AS INTEGER)) +)`; +const normalizedProtocolV3MigrationIntentTableSql = normalizeSql( + PROTOCOL_V3_MIGRATION_INTENT_TABLE_SQL, +); + +function isProtocolV3CutoverTrigger({ name }: ProdSchemaTrigger): boolean { + return name.startsWith(PROTOCOL_V3_CUTOVER_TRIGGER_PREFIX); +} + +function loadDrizzleMigrationSchemaCatalog(): ProdSchemaCatalog { + const database = new DatabaseSync(":memory:"); + try { + database.exec("PRAGMA foreign_keys = ON"); + applyDrizzleMigrations({ execute: (sql) => database.exec(sql) }); + const tableNames = database + .prepare(PROD_SCHEMA_TABLE_INTROSPECTION_SQL) + .all() + .map((row) => requireString(row, "name")); + return createProdSchemaCatalogFromIntrospectionRows( + createProdSchemaIntrospectionStatements(tableNames).map((statement) => + database.prepare(statement).all(), + ), + tableNames, + ); + } finally { + database.close(); + } +} + +export const DRIZZLE_MIGRATION_PROD_SCHEMA_CATALOG = loadDrizzleMigrationSchemaCatalog(); + +const transientProdSchemaTriggers = new Map( + DRIZZLE_MIGRATION_PROD_SCHEMA_CATALOG.triggers + .filter(isProtocolV3CutoverTrigger) + .map((trigger) => [trigger.name, trigger]), +); + +export const MANAGED_PROD_SCHEMA_TRIGGERS = DRIZZLE_MIGRATION_PROD_SCHEMA_CATALOG.triggers.filter( + (trigger) => !isProtocolV3CutoverTrigger(trigger), +); + +function prodSchemaTriggersMatch(left: ProdSchemaTrigger, right: ProdSchemaTrigger): boolean { + return left.tableName === right.tableName && normalizeSql(left.sql) === normalizeSql(right.sql); +} + +export function findProdSchemaDifferences( + expected: ProdSchemaCatalog, + live: ProdSchemaCatalog, +): string[] { + const expectedTables = new Map(expected.tables.map((table) => [table.name, table])); + const liveTables = new Map(live.tables.map((table) => [table.name, table])); + const differences: string[] = []; + + for (const [name, expectedTable] of expectedTables) { + const liveTable = liveTables.get(name); + if (!liveTable) { + differences.push(`missing table ${name}`); + continue; + } + for (const field of ["columns", "indexes", "foreignKeys", "checks", "autoincrement"] as const) { + if (JSON.stringify(expectedTable[field]) !== JSON.stringify(liveTable[field])) { + differences.push(`${name}.${field} differs`); + } + } + if (expectedTable.definition !== null && expectedTable.definition !== liveTable.definition) { + differences.push(`${name}.definition differs`); + } + } + + for (const [name, liveTable] of liveTables) { + const normalized = name.toLowerCase(); + if (normalized === PROTOCOL_V3_MIGRATION_INTENT_TABLE) { + if ( + name !== PROTOCOL_V3_MIGRATION_INTENT_TABLE || + liveTable.definition !== normalizedProtocolV3MigrationIntentTableSql + ) { + differences.push(`${name}.definition differs`); + } + continue; + } + const isInternal = + normalized === "d1_migrations" || + normalized === "__production_deploy_lease" || + normalized === "__protocol_v3_cutover" || + normalized.startsWith("_cf_") || + normalized.startsWith("sqlite_"); + if (!expectedTables.has(name) && !isInternal && !RETAINED_LEGACY_TABLES.has(name)) { + differences.push(`unexpected table ${name}`); + } + } + + const expectedTriggers = new Map(expected.triggers.map((trigger) => [trigger.name, trigger])); + const liveTriggers = new Map(live.triggers.map((trigger) => [trigger.name, trigger])); + for (const [name, expectedTrigger] of expectedTriggers) { + const liveTrigger = liveTriggers.get(name); + if (!liveTrigger) { + differences.push(`missing trigger ${name}`); + } else if (!prodSchemaTriggersMatch(liveTrigger, expectedTrigger)) { + differences.push(`trigger ${name} differs`); + } + } + for (const [name, trigger] of liveTriggers) { + if (expectedTriggers.has(name) || trigger.tableName.toLowerCase().startsWith("_cf_")) continue; + const transientTrigger = transientProdSchemaTriggers.get(name); + if (transientTrigger !== undefined) { + if (!prodSchemaTriggersMatch(trigger, transientTrigger)) { + differences.push(`trigger ${name} differs`); + } + } else { + differences.push(`unexpected trigger ${name}`); + } + } + return differences; +} + +export function assertProdSchemaMatches( + expected: ProdSchemaCatalog, + live: ProdSchemaCatalog, +): void { + const differences = findProdSchemaDifferences(expected, live); + if (differences.length === 0) return; + throw new Error( + `Prod D1 schema differs from the latest Drizzle snapshot:\n${differences + .slice(0, 20) + .map((difference) => ` - ${difference}`) + .join("\n")}${differences.length > 20 ? `\n - …and ${differences.length - 20} more` : ""}`, + ); +} diff --git a/pkgs/db/src/schema/api-command.schema.ts b/pkgs/db/src/schema/api-command.schema.ts index aed3d4f5..901992a4 100644 --- a/pkgs/db/src/schema/api-command.schema.ts +++ b/pkgs/db/src/schema/api-command.schema.ts @@ -1,5 +1,6 @@ import type { SemanticPlatformId } from "@mosoo/id"; -import { index, integer, sqliteTable, text, uniqueIndex } from "drizzle-orm/sqlite-core"; +import { sql } from "drizzle-orm"; +import { check, index, integer, sqliteTable, text, uniqueIndex } from "drizzle-orm/sqlite-core"; import { platformIdColumn } from "./id-column"; @@ -7,8 +8,10 @@ export type ApiCommandId = SemanticPlatformId<"ApiCommandId">; export type ApiCommandKind = | "app_deployment_run_dispatch" + | "app_deployment_script_reconciliation" | "cost_ledger_reconciliation" | "environment_package_artifact_build" + | "sandbox_backup_reconciliation" | "scheduled_maintenance" | "session_run_dispatch"; export type ApiCommandStatus = "dead_lettered" | "failed" | "queued" | "running" | "succeeded"; @@ -22,6 +25,7 @@ export const apiCommandsTable = sqliteTable( completedAt: integer("completed_at"), createdAt: integer("created_at").notNull(), dedupeKey: text("dedupe_key").notNull(), + deliveryGeneration: integer("delivery_generation").notNull().default(1), id: platformIdColumn("id").primaryKey(), kind: text("kind").$type().notNull(), lastErrorCode: text("last_error_code"), @@ -31,6 +35,10 @@ export const apiCommandsTable = sqliteTable( updatedAt: integer("updated_at").notNull(), }, (table) => [ + check( + "api_command_delivery_generation_check", + sql`typeof(${table.deliveryGeneration}) = 'integer' AND ${table.deliveryGeneration} BETWEEN 1 AND 9007199254740991`, + ), uniqueIndex("api_command_dedupe_idx").on(table.dedupeKey), index("api_command_status_updated_idx").on(table.status, table.updatedAt), index("api_command_claim_idx").on(table.status, table.claimExpiresAt), diff --git a/pkgs/db/src/schema/app.schema.ts b/pkgs/db/src/schema/app.schema.ts index 15e94326..1374dc73 100644 --- a/pkgs/db/src/schema/app.schema.ts +++ b/pkgs/db/src/schema/app.schema.ts @@ -9,11 +9,13 @@ import type { import { sql } from "drizzle-orm"; import { check, index, integer, sqliteTable, text, uniqueIndex } from "drizzle-orm/sqlite-core"; +import { apiCommandsTable } from "./api-command.schema"; +import type { ApiCommandId } from "./api-command.schema"; import { platformIdColumn } from "./id-column"; import { vaultSecretsTable } from "./mcp.schema"; export type AppDeploymentSourceKind = "github_public"; -export type AppDeploymentTargetKind = "cloudflare_pages" | "cloudflare_worker"; +export type AppDeploymentTargetKind = "cloudflare_static_assets" | "cloudflare_worker"; export type AppDeploymentRunStatus = | "activating" | "building" @@ -37,6 +39,7 @@ export const appsTable = sqliteTable("app", { export const appDeploymentsTable = sqliteTable( "app_deployment", { + activeScriptName: text("active_script_name"), appId: platformIdColumn("app_id").notNull(), createdAt: integer("created_at").notNull(), defaultBranch: text("default_branch").notNull(), @@ -53,6 +56,10 @@ export const appDeploymentsTable = sqliteTable( updatedAt: integer("updated_at").notNull(), }, (table) => [ + check( + "app_deployment_traffic_authority_check", + sql`(${table.activeScriptName} IS NULL AND ${table.lastSuccessfulUrl} IS NULL) OR (${table.deletedAt} IS NULL AND typeof(${table.activeScriptName}) = 'text' AND length(${table.activeScriptName}) > 0 AND typeof(${table.lastSuccessfulUrl}) = 'text' AND length(${table.lastSuccessfulUrl}) > 0)`, + ), check("app_deployment_source_kind_check", sql`${table.sourceKind} IN ('github_public')`), uniqueIndex("app_deployment_active_app_idx") .on(table.appId) @@ -94,7 +101,7 @@ export const appDeploymentRunsTable = sqliteTable( ), check( "app_deployment_run_target_kind_check", - sql`${table.targetKind} IS NULL OR ${table.targetKind} IN ('cloudflare_pages', 'cloudflare_worker')`, + sql`${table.targetKind} IS NULL OR ${table.targetKind} IN ('cloudflare_static_assets', 'cloudflare_worker')`, ), index("app_deployment_run_app_id_idx").on(table.appId, table.id), index("app_deployment_run_deployment_id_idx").on(table.deploymentId, table.id), @@ -106,6 +113,65 @@ export const appDeploymentRunsTable = sqliteTable( ], ); +export const appDeploymentScriptsTable = sqliteTable( + "app_deployment_script", + { + attemptCount: integer("attempt_count").notNull(), + commandId: platformIdColumn("command_id") + .notNull() + .references(() => apiCommandsTable.id, { onDelete: "restrict" }), + deliveryGeneration: integer("delivery_generation").notNull(), + deploymentId: platformIdColumn("deployment_id") + .notNull() + .references(() => appDeploymentsTable.id, { onDelete: "restrict" }), + externalDeletedAt: integer("external_deleted_at"), + lastReconciledAt: integer("last_reconciled_at"), + nextReconcileAt: integer("next_reconcile_at"), + reconcileCount: integer("reconcile_count").notNull().default(0), + reconcileExpiresAt: integer("reconcile_expires_at"), + reconcileOwner: text("reconcile_owner"), + registeredAt: integer("registered_at").notNull(), + registeredClaimOwner: text("registered_claim_owner").notNull(), + retireAfter: integer("retire_after"), + runId: platformIdColumn("run_id") + .notNull() + .references(() => appDeploymentRunsTable.id, { onDelete: "restrict" }), + scriptName: text("script_name").primaryKey(), + uploadStartedAt: integer("upload_started_at"), + }, + (table) => [ + check( + "app_deployment_script_attempt_check", + sql`typeof(${table.attemptCount}) = 'integer' AND ${table.attemptCount} BETWEEN 1 AND 9007199254740991`, + ), + check( + "app_deployment_script_delivery_check", + sql`typeof(${table.deliveryGeneration}) = 'integer' AND ${table.deliveryGeneration} BETWEEN 1 AND 9007199254740991`, + ), + check( + "app_deployment_script_name_check", + sql`typeof(${table.scriptName}) = 'text' AND length(${table.scriptName}) BETWEEN 36 AND 63 AND substr(${table.scriptName}, 1, 4) = 'app-' AND ${table.scriptName} NOT GLOB '*[^0-9a-z-]*' AND substr(${table.scriptName}, -1, 1) GLOB '[0-9a-z]'`, + ), + check( + "app_deployment_script_registered_owner_check", + sql`typeof(${table.registeredClaimOwner}) = 'text' AND length(${table.registeredClaimOwner}) > 0`, + ), + check( + "app_deployment_script_reconcile_count_check", + sql`typeof(${table.reconcileCount}) = 'integer' AND ${table.reconcileCount} BETWEEN 0 AND 9007199254740991`, + ), + check( + "app_deployment_script_reconcile_lease_check", + sql`(${table.reconcileOwner} IS NULL AND ${table.reconcileExpiresAt} IS NULL) OR (typeof(${table.reconcileOwner}) = 'text' AND length(${table.reconcileOwner}) > 0 AND typeof(${table.reconcileExpiresAt}) = 'integer' AND ${table.reconcileExpiresAt} BETWEEN 0 AND 9007199254740991)`, + ), + check( + "app_deployment_script_time_check", + sql`typeof(${table.registeredAt}) = 'integer' AND ${table.registeredAt} BETWEEN 0 AND 9007199254740991 AND (${table.uploadStartedAt} IS NULL OR (typeof(${table.uploadStartedAt}) = 'integer' AND ${table.uploadStartedAt} BETWEEN ${table.registeredAt} AND 9007199254740991)) AND (${table.retireAfter} IS NULL OR (typeof(${table.retireAfter}) = 'integer' AND ${table.retireAfter} BETWEEN ${table.registeredAt} AND 9007199254740991)) AND (${table.nextReconcileAt} IS NULL OR (typeof(${table.nextReconcileAt}) = 'integer' AND ${table.nextReconcileAt} BETWEEN ${table.registeredAt} AND 9007199254740991)) AND (${table.lastReconciledAt} IS NULL OR (typeof(${table.lastReconciledAt}) = 'integer' AND ${table.lastReconciledAt} BETWEEN ${table.registeredAt} AND 9007199254740991)) AND (${table.externalDeletedAt} IS NULL OR (${table.retireAfter} IS NOT NULL AND typeof(${table.externalDeletedAt}) = 'integer' AND ${table.externalDeletedAt} BETWEEN ${table.registeredAt} AND 9007199254740991))`, + ), + index("app_deployment_script_reconcile_idx").on(table.nextReconcileAt, table.scriptName), + ], +); + /** * Retired App Secrets storage (GitHub #429 / PR #504). The product surface was * rolled back in favor of the deployment-scoped bound capability, so no runtime @@ -132,4 +198,5 @@ export const appDeploymentSecretsTable = sqliteTable( export type AppDeploymentRow = typeof appDeploymentsTable.$inferSelect; export type AppDeploymentRunRow = typeof appDeploymentRunsTable.$inferSelect; +export type AppDeploymentScriptRow = typeof appDeploymentScriptsTable.$inferSelect; export type AppRow = typeof appsTable.$inferSelect; diff --git a/pkgs/db/src/schema/environment.schema.ts b/pkgs/db/src/schema/environment.schema.ts index 740e6b40..2717e364 100644 --- a/pkgs/db/src/schema/environment.schema.ts +++ b/pkgs/db/src/schema/environment.schema.ts @@ -1,8 +1,16 @@ import type { EnvironmentNetworkPolicy } from "@mosoo/contracts/environment"; -import type { AccountId, EnvironmentId, EnvironmentRevisionId, AppId } from "@mosoo/id"; +import type { + AccountId, + AppId, + EnvironmentId, + EnvironmentRevisionId, + SandboxBackupId, +} from "@mosoo/id"; import { sql } from "drizzle-orm"; import { check, index, integer, sqliteTable, text, uniqueIndex } from "drizzle-orm/sqlite-core"; +import { apiCommandsTable } from "./api-command.schema"; +import type { ApiCommandId } from "./api-command.schema"; import { platformIdColumn } from "./id-column"; export const environmentsTable = sqliteTable( @@ -61,5 +69,111 @@ export const environmentRevisionsTable = sqliteTable( ], ); +export const environmentPackageArtifactBackupsTable = sqliteTable( + "environment_package_artifact_backup", + { + appId: platformIdColumn("app_id").notNull(), + attemptCount: integer("attempt_count").notNull(), + backupId: platformIdColumn("backup_id").primaryKey(), + commandId: platformIdColumn("command_id") + .notNull() + .references(() => apiCommandsTable.id, { onDelete: "restrict" }), + committedAt: integer("committed_at").notNull(), + deliveryGeneration: integer("delivery_generation").notNull(), + expiresAt: integer("expires_at").notNull(), + inputDigest: text("input_digest").notNull(), + manifestGeneration: integer("manifest_generation").notNull(), + pathsJson: text("paths_json").notNull(), + }, + (table) => [ + check( + "environment_package_artifact_backup_attempt_check", + sql`typeof(${table.attemptCount}) = 'integer' AND ${table.attemptCount} BETWEEN 1 AND 9007199254740991`, + ), + check( + "environment_package_artifact_backup_delivery_check", + sql`typeof(${table.deliveryGeneration}) = 'integer' AND ${table.deliveryGeneration} BETWEEN 1 AND 9007199254740991`, + ), + check( + "environment_package_artifact_backup_generation_check", + sql`typeof(${table.manifestGeneration}) = 'integer' AND ${table.manifestGeneration} BETWEEN 1 AND 9007199254740991`, + ), + check( + "environment_package_artifact_backup_digest_check", + sql`length(${table.inputDigest}) = 64 AND ${table.inputDigest} = lower(${table.inputDigest}) AND ${table.inputDigest} NOT GLOB '*[^0-9a-f]*'`, + ), + check( + "environment_package_artifact_backup_paths_check", + sql`json_valid(${table.pathsJson}) = 1 AND json_type(${table.pathsJson}) IS 'object' AND json_type(${table.pathsJson}, '$.executable') IS 'array' AND json_type(${table.pathsJson}, '$.node') IS 'array' AND json_type(${table.pathsJson}, '$.python') IS 'array'`, + ), + check( + "environment_package_artifact_backup_time_check", + sql`typeof(${table.committedAt}) = 'integer' AND ${table.committedAt} BETWEEN 0 AND 9007199254740991 AND typeof(${table.expiresAt}) = 'integer' AND ${table.expiresAt} BETWEEN ${table.committedAt} + 86400001 AND 9007199254740991`, + ), + index("environment_package_artifact_backup_expiry_idx").on(table.expiresAt, table.backupId), + uniqueIndex("environment_package_artifact_backup_key_idx").on(table.appId, table.inputDigest), + ], +); + +export const environmentPackageArtifactBackupStagingTable = sqliteTable( + "environment_package_artifact_backup_staging", + { + actualBackupId: platformIdColumn("actual_backup_id"), + appId: platformIdColumn("app_id").notNull(), + attemptCount: integer("attempt_count").notNull(), + claimOwner: text("claim_owner").notNull(), + commandId: platformIdColumn("command_id") + .primaryKey() + .references(() => apiCommandsTable.id, { onDelete: "restrict" }), + createdAt: integer("created_at").notNull(), + deliveryGeneration: integer("delivery_generation").notNull(), + dir: text("dir").notNull(), + inputDigest: text("input_digest").notNull(), + pathsJson: text("paths_json").notNull(), + updatedAt: integer("updated_at").notNull(), + }, + (table) => [ + check( + "environment_package_artifact_backup_staging_attempt_check", + sql`typeof(${table.attemptCount}) = 'integer' AND ${table.attemptCount} BETWEEN 1 AND 9007199254740991`, + ), + check( + "environment_package_artifact_backup_staging_claim_owner_check", + sql`typeof(${table.claimOwner}) = 'text' AND length(${table.claimOwner}) > 0`, + ), + check( + "environment_package_artifact_backup_staging_delivery_check", + sql`typeof(${table.deliveryGeneration}) = 'integer' AND ${table.deliveryGeneration} BETWEEN 1 AND 9007199254740991`, + ), + check( + "environment_package_artifact_backup_staging_digest_check", + sql`length(${table.inputDigest}) = 64 AND ${table.inputDigest} = lower(${table.inputDigest}) AND ${table.inputDigest} NOT GLOB '*[^0-9a-f]*'`, + ), + check( + "environment_package_artifact_backup_staging_dir_check", + sql`typeof(${table.dir}) = 'text' AND length(${table.dir}) > 0`, + ), + check( + "environment_package_artifact_backup_staging_paths_check", + sql`json_valid(${table.pathsJson}) = 1 AND json_type(${table.pathsJson}) = 'object' AND json_type(${table.pathsJson}, '$.executable') = 'array' AND json_type(${table.pathsJson}, '$.node') = 'array' AND json_type(${table.pathsJson}, '$.python') = 'array'`, + ), + check( + "environment_package_artifact_backup_staging_time_check", + sql`typeof(${table.createdAt}) = 'integer' AND ${table.createdAt} BETWEEN 0 AND 9007199254740991 AND typeof(${table.updatedAt}) = 'integer' AND ${table.updatedAt} BETWEEN ${table.createdAt} AND 9007199254740991`, + ), + uniqueIndex("environment_package_artifact_backup_staging_actual_idx") + .on(table.actualBackupId) + .where(sql`${table.actualBackupId} IS NOT NULL`), + uniqueIndex("environment_package_artifact_backup_staging_intent_idx").on( + table.appId, + table.inputDigest, + ), + index("environment_package_artifact_backup_staging_updated_idx").on( + table.updatedAt, + table.commandId, + ), + ], +); + export type EnvironmentRevisionRow = typeof environmentRevisionsTable.$inferSelect; export type EnvironmentRow = typeof environmentsTable.$inferSelect; diff --git a/pkgs/db/src/schema/file.schema.ts b/pkgs/db/src/schema/file.schema.ts index 2768b1eb..ce6d8e9d 100644 --- a/pkgs/db/src/schema/file.schema.ts +++ b/pkgs/db/src/schema/file.schema.ts @@ -6,11 +6,21 @@ import type { FileUploadStatus, FileUploadStrategy, } from "@mosoo/contracts/file"; -import type { AccountId, FileVersionId, FileId, PlatformId, UploadId } from "@mosoo/id"; +import type { + AccountId, + DriverInstanceId, + FileVersionId, + FileId, + PlatformId, + SessionId, + SessionRunId, + UploadId, +} from "@mosoo/id"; import { sql } from "drizzle-orm"; -import { index, integer, sqliteTable, text, uniqueIndex } from "drizzle-orm/sqlite-core"; +import { check, index, integer, sqliteTable, text, uniqueIndex } from "drizzle-orm/sqlite-core"; import { platformIdColumn } from "./id-column"; +import { sessionsTable } from "./session/core.schema"; export type FileVersionReason = "delete" | "directory_delete" | "move_overwrite" | "overwrite"; @@ -31,6 +41,7 @@ export const fileRecordsTable = sqliteTable( parentPath: text("parent_path").notNull(), path: text("path").notNull(), purpose: text("purpose").$type().notNull(), + runtimeEventSeq: integer("runtime_event_seq"), scopeId: platformIdColumn("scope_id"), scopeKind: text("scope_kind").$type().notNull(), sessionKind: text("session_kind").$type<"artifact" | "attachment">(), @@ -40,6 +51,11 @@ export const fileRecordsTable = sqliteTable( version: integer("version").notNull(), }, (table) => [ + check( + "file_record_runtime_event_seq_check", + sql`${table.runtimeEventSeq} IS NULL OR ${table.runtimeEventSeq} >= 0`, + ), + index("file_record_runtime_event_seq_idx").on(table.scopeId, table.runtimeEventSeq), uniqueIndex("file_record_object_key_idx").on(table.objectKey), uniqueIndex("file_record_unscoped_parent_path_name_status_idx") .on(table.scopeKind, table.parentPath, table.name, table.status) @@ -80,6 +96,99 @@ export const fileRecordsTable = sqliteTable( ], ); +export type RuntimeArtifactAttemptStatus = "accepted" | "deleting" | "staged" | "staging"; + +export const runtimeArtifactAttemptsTable = sqliteTable( + "runtime_artifact_attempt", + { + acceptedEventId: text("accepted_event_id"), + createdAt: integer("created_at").notNull(), + createdByAccountId: platformIdColumn("created_by_account_id").notNull(), + deleteAfter: integer("delete_after"), + driverConnectionId: text("driver_connection_id").notNull(), + driverGeneration: integer("driver_generation").notNull(), + driverInstanceId: platformIdColumn("driver_instance_id").notNull(), + eventType: text("event_type").notNull(), + expiresAt: integer("expires_at"), + id: text("id").primaryKey(), + manifestJson: text("manifest_json"), + manifestSha256: text("manifest_sha256"), + ownedObjectKeysJson: text("owned_object_keys_json").notNull().default("[]"), + runId: platformIdColumn("run_id").notNull(), + semanticHash: text("semantic_hash").notNull(), + sessionId: platformIdColumn("session_id").notNull(), + sourceEventId: text("source_event_id").notNull(), + status: text("status").$type().notNull(), + updatedAt: integer("updated_at").notNull(), + }, + (table) => [ + check( + "runtime_artifact_attempt_manifest_check", + sql`(${table.manifestJson} IS NULL AND ${table.manifestSha256} IS NULL) OR (${table.manifestJson} IS NOT NULL AND json_valid(${table.manifestJson}) = 1 AND json_extract(${table.manifestJson}, '$.version') IS 1 AND json_type(${table.manifestJson}, '$.captureStatus') IS 'text' AND json_extract(${table.manifestJson}, '$.captureStatus') IN ('complete', 'omitted_file_limit', 'omitted_runtime_unavailable', 'omitted_size_limit', 'omitted_source_changed', 'omitted_source_missing') AND json_type(${table.manifestJson}, '$.mode') IS 'text' AND json_extract(${table.manifestJson}, '$.mode') IN ('delta', 'snapshot') AND (json_extract(${table.manifestJson}, '$.captureStatus') = 'complete' OR json_array_length(${table.manifestJson}, '$.files') = 0) AND json_extract(${table.manifestJson}, '$.sourceEventId') IS ${table.sourceEventId} AND json_extract(${table.manifestJson}, '$.semanticHash') IS ${table.semanticHash} AND json_type(${table.manifestJson}, '$.files') IS 'array' AND ${table.manifestSha256} IS NOT NULL AND length(${table.manifestSha256}) = 64 AND ${table.manifestSha256} = lower(${table.manifestSha256}) AND ${table.manifestSha256} NOT GLOB '*[^0-9a-f]*')`, + ), + check( + "runtime_artifact_attempt_owned_keys_check", + sql`json_valid(${table.ownedObjectKeysJson}) = 1 AND json_type(${table.ownedObjectKeysJson}) IS 'array'`, + ), + check( + "runtime_artifact_attempt_semantic_hash_check", + sql`length(${table.semanticHash}) = 64 AND ${table.semanticHash} = lower(${table.semanticHash}) AND ${table.semanticHash} NOT GLOB '*[^0-9a-f]*'`, + ), + check( + "runtime_artifact_attempt_status_check", + sql`(${table.status} = 'staging' AND ${table.manifestJson} IS NULL AND ${table.acceptedEventId} IS NULL AND ${table.expiresAt} IS NOT NULL AND ${table.deleteAfter} IS NULL) OR (${table.status} = 'staged' AND ${table.manifestJson} IS NOT NULL AND ${table.acceptedEventId} IS NULL AND ${table.expiresAt} IS NOT NULL AND ${table.deleteAfter} IS NULL) OR (${table.status} = 'accepted' AND ${table.manifestJson} IS NOT NULL AND ${table.acceptedEventId} IS NOT NULL AND ${table.expiresAt} IS NULL AND ${table.deleteAfter} IS NULL AND json_array_length(${table.ownedObjectKeysJson}) = 0) OR (${table.status} = 'deleting' AND ${table.acceptedEventId} IS NULL AND ${table.deleteAfter} IS NOT NULL)`, + ), + check( + "runtime_artifact_attempt_time_check", + sql`${table.driverGeneration} >= 0 AND (${table.expiresAt} IS NULL OR ${table.expiresAt} >= ${table.createdAt}) AND (${table.deleteAfter} IS NULL OR ${table.deleteAfter} >= ${table.createdAt}) AND ${table.updatedAt} >= ${table.createdAt}`, + ), + uniqueIndex("runtime_artifact_attempt_accepted_event_idx") + .on(table.acceptedEventId) + .where(sql`${table.acceptedEventId} IS NOT NULL`), + index("runtime_artifact_attempt_cleanup_idx").on( + table.status, + table.expiresAt, + table.updatedAt, + table.id, + ), + index("runtime_artifact_attempt_session_status_idx").on( + table.sessionId, + table.status, + table.id, + ), + ], +); + +export const sessionArtifactHeadsTable = sqliteTable( + "session_artifact_head", + { + fileId: platformIdColumn("file_id"), + runtimeEventSeq: integer("runtime_event_seq").notNull(), + sessionId: platformIdColumn("session_id") + .notNull() + .references(() => sessionsTable.id, { onDelete: "cascade" }), + sourceEventId: text("source_event_id").notNull(), + sourcePath: text("source_path").notNull(), + updatedAt: integer("updated_at").notNull(), + }, + (table) => [ + check( + "session_artifact_head_path_check", + sql`length(${table.sourcePath}) > 8 AND substr(${table.sourcePath}, 1, 8) = 'outputs/' AND instr(${table.sourcePath}, char(0)) = 0 AND instr(${table.sourcePath}, '\\') = 0 AND ${table.sourcePath} NOT LIKE '%//%' AND ${table.sourcePath} NOT LIKE '%/./%' AND ${table.sourcePath} NOT LIKE '%/.' AND ${table.sourcePath} NOT LIKE '%/../%' AND ${table.sourcePath} NOT LIKE '%/..'`, + ), + check( + "session_artifact_head_seq_check", + sql`${table.runtimeEventSeq} >= 0 AND ${table.updatedAt} >= 0`, + ), + uniqueIndex("session_artifact_head_session_path_idx").on(table.sessionId, table.sourcePath), + index("session_artifact_head_session_seq_idx").on( + table.sessionId, + table.runtimeEventSeq, + table.sourcePath, + ), + ], +); + export const fileUploadsTable = sqliteTable( "file_upload", { @@ -142,5 +251,7 @@ export const fileVersionsTable = sqliteTable( ); export type FileRecordRow = typeof fileRecordsTable.$inferSelect; +export type RuntimeArtifactAttemptRow = typeof runtimeArtifactAttemptsTable.$inferSelect; +export type SessionArtifactHeadRow = typeof sessionArtifactHeadsTable.$inferSelect; export type FileUploadRow = typeof fileUploadsTable.$inferSelect; export type FileVersionRow = typeof fileVersionsTable.$inferSelect; diff --git a/pkgs/db/src/schema/runtime.schema.ts b/pkgs/db/src/schema/runtime.schema.ts index 2761a045..fd12b2a8 100644 --- a/pkgs/db/src/schema/runtime.schema.ts +++ b/pkgs/db/src/schema/runtime.schema.ts @@ -10,6 +10,7 @@ import type { DriverInstanceStatus, RuntimeSubjectErrorCode, SandboxBackupStatus, + SandboxOperationKind, SandboxSessionStatus, SandboxStatus, SandboxSubjectKind, @@ -50,8 +51,8 @@ import { sessionRunsTable } from "./session/runs.schema"; export const sandboxesTable = sqliteTable( "sandbox", { - agentId: platformIdColumn("agent_id"), - appId: platformIdColumn("app_id"), + agentId: platformIdColumn("agent_id").notNull(), + appId: platformIdColumn("app_id").notNull(), bindMountReady: integer("bind_mount_ready", { mode: "boolean" }).notNull().default(false), claimExpiresAt: integer("claim_expires_at"), claimOwner: text("claim_owner"), @@ -59,12 +60,15 @@ export const sandboxesTable = sqliteTable( globalMountsJson: text("global_mounts_json").notNull().default("[]"), id: platformIdColumn("id").primaryKey(), inactiveDeadlineAt: integer("inactive_deadline_at"), + incarnation: integer("incarnation").notNull().default(0), kind: text("kind").$type().notNull(), lastBackupId: platformIdColumn("last_backup_id"), lastError: text("last_error"), lastErrorCode: text("last_error_code").$type(), lastRestoreBackupId: platformIdColumn("last_restore_backup_id"), - ownerAccountId: platformIdColumn("owner_account_id"), + networkConstraintsHash: text("network_constraints_hash"), + ownerAccountId: platformIdColumn("owner_account_id").notNull(), + operationKind: text("operation_kind").$type(), status: text("status").$type().notNull(), statusChangedAt: integer("status_changed_at").notNull().default(0), statusEvent: text("status_event").notNull().default("runtime_subject.cold"), @@ -76,17 +80,35 @@ export const sandboxesTable = sqliteTable( updatedAt: integer("updated_at").notNull(), }, (table) => [ - // 'error' is a retired status: the app no longer writes it (a failed - // activation returns the subject to 'cold' with the diagnostic in - // last_error). It stays in this CHECK as a dead-but-allowed value so we - // avoid a full SQLite table rebuild; migration 0003 converges any existing - // 'error' rows to 'cold'. The SandboxStatus contract type omits it, so the - // application can never produce it. check( "sandbox_status_check", - sql`${table.status} IN ('cold', 'restoring', 'active', 'backing_up', 'destroying', 'error')`, + sql`${table.status} IN ('cold', 'restoring', 'active', 'backing_up', 'destroying')`, ), check("sandbox_status_seq_check", sql`${table.statusSeq} >= 0`), + check( + "sandbox_incarnation_check", + sql`typeof(${table.incarnation}) = 'integer' AND ${table.incarnation} BETWEEN 0 AND 9007199254740991 AND (${table.status} = 'cold' OR ${table.incarnation} > 0)`, + ), + check( + "sandbox_identity_check", + sql`(${table.kind} = 'pet' AND ${table.subjectKind} = 'agent' AND ${table.subjectId} = ${table.agentId}) OR (${table.kind} = 'cattle' AND ${table.subjectKind} = 'session')`, + ), + check( + "sandbox_network_constraints_hash_check", + sql`(${table.networkConstraintsHash} IS NULL AND ${table.status} = 'cold') OR (${table.networkConstraintsHash} IS NOT NULL AND length(${table.networkConstraintsHash}) = 64 AND ${table.networkConstraintsHash} = lower(${table.networkConstraintsHash}) AND ${table.networkConstraintsHash} NOT GLOB '*[^0-9a-f]*')`, + ), + check( + "sandbox_operation_state_check", + sql`(${table.status} IN ('cold', 'active') AND ${table.operationKind} IS NULL AND ${table.statusOperationId} IS NULL) OR (${table.status} = 'restoring' AND ${table.operationKind} = 'activate' AND ${table.statusOperationId} IS NOT NULL) OR (${table.status} = 'backing_up' AND ${table.operationKind} IN ('hibernate', 'recreate', 'reset') AND ${table.statusOperationId} IS NOT NULL) OR (${table.status} = 'destroying' AND ${table.operationKind} IN ('activate', 'hibernate', 'recreate', 'reset') AND ${table.statusOperationId} IS NOT NULL)`, + ), + check( + "sandbox_claim_check", + sql`(${table.claimOwner} IS NULL AND ${table.claimExpiresAt} IS NULL) OR (${table.claimOwner} IS NOT NULL AND typeof(${table.claimExpiresAt}) = 'integer' AND ${table.claimExpiresAt} BETWEEN 0 AND 9007199254740991)`, + ), + check( + "sandbox_operation_claim_check", + sql`${table.status} IN ('cold', 'active') OR ${table.claimOwner} IS NOT NULL`, + ), uniqueIndex("sandbox_subject_idx").on(table.kind, table.subjectKind, table.subjectId), index("sandbox_status_deadline_idx").on( table.status, @@ -101,10 +123,12 @@ export const sandboxSessionsTable = sqliteTable( "sandbox_session", { sandboxSessionId: platformIdColumn("cloudflare_session_id").notNull(), + cleanupOperationId: platformIdColumn("cleanup_operation_id"), createdAt: integer("created_at").notNull(), cwd: text("cwd").notNull(), originJson: text("origin_json").notNull(), sandboxId: platformIdColumn("sandbox_id").notNull(), + sandboxIncarnation: integer("sandbox_incarnation").notNull().default(0), sessionId: platformIdColumn("session_id") .primaryKey() .references(() => sessionsTable.id, { onDelete: "cascade" }), @@ -112,6 +136,15 @@ export const sandboxSessionsTable = sqliteTable( updatedAt: integer("updated_at").notNull(), }, (table) => [ + check( + "sandbox_session_cleanup_check", + sql`(${table.status} = 'cleanup_pending' AND ${table.cleanupOperationId} IS NOT NULL) OR (${table.status} <> 'cleanup_pending' AND ${table.cleanupOperationId} IS NULL)`, + ), + check( + "sandbox_session_status_incarnation_check", + sql`${table.status} IN ('active', 'cleanup_pending', 'closed', 'error') AND typeof(${table.sandboxIncarnation}) = 'integer' AND ${table.sandboxIncarnation} BETWEEN 0 AND 9007199254740991 AND (${table.status} IN ('closed', 'error') OR ${table.sandboxIncarnation} > 0)`, + ), + index("sandbox_session_status_updated_idx").on(table.status, table.updatedAt, table.sessionId), index("sandbox_session_sandbox_status_idx").on(table.sandboxId, table.status, table.updatedAt), uniqueIndex("sandbox_session_cloudflare_session_idx").on(table.sandboxSessionId), ], @@ -122,24 +155,147 @@ export const sandboxBackupsTable = sqliteTable( { createdAt: integer("created_at").notNull(), dir: text("dir").notNull(), - errorMessage: text("error_message"), id: platformIdColumn("id").primaryKey(), keep: integer("keep", { mode: "boolean" }).notNull().default(false), + operationId: platformIdColumn("operation_id"), sandboxId: platformIdColumn("sandbox_id").notNull(), + sandboxIncarnation: integer("sandbox_incarnation").notNull(), sessionRunId: platformIdColumn("session_run_id"), + stagingId: platformIdColumn("staging_id").notNull(), status: text("status").$type().notNull(), ttlSeconds: integer("ttl_seconds").notNull(), updatedAt: integer("updated_at").notNull(), + workspaceSessionId: platformIdColumn("workspace_session_id"), }, (table) => [ - index("sandbox_backup_sandbox_status_created_idx").on( + index("sandbox_backup_sandbox_status_dir_created_idx").on( table.sandboxId, table.status, + table.dir, table.createdAt, + table.id, + ), + index("sandbox_backup_workspace_status_updated_idx").on( + table.workspaceSessionId, + table.status, + table.updatedAt, + table.id, ), + check("sandbox_backup_status_check", sql`${table.status} IN ('ready', 'pruned')`), + check( + "sandbox_backup_dir_check", + sql`typeof(${table.dir}) = 'text' AND length(${table.dir}) > 0`, + ), + check( + "sandbox_backup_keep_check", + sql`typeof(${table.keep}) = 'integer' AND ${table.keep} IN (false, true)`, + ), + check( + "sandbox_backup_incarnation_check", + sql`typeof(${table.sandboxIncarnation}) = 'integer' AND ${table.sandboxIncarnation} BETWEEN 0 AND 9007199254740991 AND (${table.sandboxIncarnation} > 0 OR ${table.stagingId} = ${table.id})`, + ), + check( + "sandbox_backup_ttl_check", + sql`typeof(${table.ttlSeconds}) = 'integer' AND ${table.ttlSeconds} BETWEEN 1 AND 9007199254740991`, + ), + check( + "sandbox_backup_timestamps_check", + sql`typeof(${table.createdAt}) = 'integer' AND ${table.createdAt} BETWEEN 0 AND 9007199254740991 AND typeof(${table.updatedAt}) = 'integer' AND ${table.updatedAt} BETWEEN ${table.createdAt} AND 9007199254740991`, + ), + check( + "sandbox_backup_scope_check", + sql`(${table.sessionRunId} IS NULL OR ${table.workspaceSessionId} IS NOT NULL) AND ((${table.operationId} IS NOT NULL) <> (${table.sessionRunId} IS NOT NULL) OR (${table.operationId} IS NULL AND ${table.sessionRunId} IS NULL AND ${table.workspaceSessionId} IS NULL AND ${table.stagingId} = ${table.id} AND ${table.sandboxIncarnation} = 0))`, + ), + uniqueIndex("sandbox_backup_staging_idx").on(table.stagingId), uniqueIndex("sandbox_backup_terminal_checkpoint_idx") - .on(table.sandboxId, table.dir, table.sessionRunId) - .where(sql`${table.sessionRunId} IS NOT NULL AND ${table.status} = 'ready'`), + .on(table.sandboxId, table.sandboxIncarnation, table.dir, table.sessionRunId) + .where(sql`${table.sessionRunId} IS NOT NULL`), + uniqueIndex("sandbox_backup_operation_checkpoint_idx") + .on(table.sandboxId, table.sandboxIncarnation, table.operationId, table.dir) + .where(sql`${table.operationId} IS NOT NULL`), + ], +); + +export const sandboxBackupDeleteIntentsTable = sqliteTable( + "sandbox_backup_delete_intent", + { + attemptedAt: integer("attempted_at"), + backupId: platformIdColumn("backup_id").primaryKey(), + createdAt: integer("created_at").notNull(), + deleteAfter: integer("delete_after").notNull(), + deletedAt: integer("deleted_at"), + }, + (table) => [ + index("sandbox_backup_delete_intent_pending_idx") + .on(table.deleteAfter, table.attemptedAt, table.createdAt, table.backupId) + .where(sql`${table.deletedAt} IS NULL`), + check( + "sandbox_backup_delete_intent_time_check", + sql`typeof(${table.createdAt}) = 'integer' AND ${table.createdAt} BETWEEN 0 AND 9007199254740991 AND typeof(${table.deleteAfter}) = 'integer' AND ${table.deleteAfter} BETWEEN ${table.createdAt} AND 9007199254740991 AND (${table.attemptedAt} IS NULL OR (typeof(${table.attemptedAt}) = 'integer' AND ${table.attemptedAt} BETWEEN ${table.deleteAfter} AND 9007199254740991)) AND (${table.deletedAt} IS NULL OR (typeof(${table.deletedAt}) = 'integer' AND ${table.deletedAt} BETWEEN coalesce(${table.attemptedAt}, ${table.deleteAfter}) AND 9007199254740991))`, + ), + ], +); + +export const sandboxBackupStagingTable = sqliteTable( + "sandbox_backup_staging", + { + actualBackupId: platformIdColumn("actual_backup_id"), + claimOwner: text("claim_owner"), + createdAt: integer("created_at").notNull(), + dir: text("dir").notNull(), + driverGeneration: integer("driver_generation"), + driverInstanceId: platformIdColumn("driver_instance_id"), + id: platformIdColumn("id").primaryKey(), + operationId: platformIdColumn("operation_id"), + sandboxId: platformIdColumn("sandbox_id").notNull(), + sandboxIncarnation: integer("sandbox_incarnation").notNull(), + sessionRunId: platformIdColumn("session_run_id"), + ttlSeconds: integer("ttl_seconds").notNull(), + updatedAt: integer("updated_at").notNull(), + updatesSubjectBackup: integer("updates_subject_backup", { mode: "boolean" }) + .notNull() + .default(false), + workspaceSessionId: platformIdColumn("workspace_session_id"), + }, + (table) => [ + index("sandbox_backup_staging_updated_idx").on(table.updatedAt, table.id), + check( + "sandbox_backup_staging_claim_owner_check", + sql`${table.claimOwner} IS NULL OR (typeof(${table.claimOwner}) = 'text' AND length(${table.claimOwner}) > 0)`, + ), + check( + "sandbox_backup_staging_dir_check", + sql`typeof(${table.dir}) = 'text' AND length(${table.dir}) > 0`, + ), + check( + "sandbox_backup_staging_incarnation_check", + sql`typeof(${table.sandboxIncarnation}) = 'integer' AND ${table.sandboxIncarnation} BETWEEN 1 AND 9007199254740991`, + ), + check( + "sandbox_backup_staging_ttl_check", + sql`typeof(${table.ttlSeconds}) = 'integer' AND ${table.ttlSeconds} BETWEEN 1 AND 9007199254740991`, + ), + check( + "sandbox_backup_staging_timestamps_check", + sql`typeof(${table.createdAt}) = 'integer' AND ${table.createdAt} BETWEEN 0 AND 9007199254740991 AND typeof(${table.updatedAt}) = 'integer' AND ${table.updatedAt} BETWEEN ${table.createdAt} AND 9007199254740991`, + ), + check( + "sandbox_backup_staging_scope_check", + sql`((${table.operationId} IS NOT NULL AND ${table.claimOwner} IS NOT NULL AND ${table.sessionRunId} IS NULL AND ${table.driverInstanceId} IS NULL AND ${table.driverGeneration} IS NULL) OR (${table.operationId} IS NULL AND ${table.claimOwner} IS NULL AND ${table.sessionRunId} IS NOT NULL AND ${table.workspaceSessionId} IS NOT NULL AND ${table.driverInstanceId} IS NOT NULL AND typeof(${table.driverGeneration}) = 'integer' AND ${table.driverGeneration} BETWEEN 0 AND 9007199254740991)) AND (${table.updatesSubjectBackup} = false OR (${table.operationId} IS NOT NULL AND ${table.workspaceSessionId} IS NULL))`, + ), + check( + "sandbox_backup_staging_updates_subject_check", + sql`typeof(${table.updatesSubjectBackup}) = 'integer' AND ${table.updatesSubjectBackup} IN (false, true)`, + ), + uniqueIndex("sandbox_backup_staging_actual_idx") + .on(table.actualBackupId) + .where(sql`${table.actualBackupId} IS NOT NULL`), + uniqueIndex("sandbox_backup_staging_terminal_checkpoint_idx") + .on(table.sandboxId, table.sandboxIncarnation, table.dir, table.sessionRunId) + .where(sql`${table.sessionRunId} IS NOT NULL`), + uniqueIndex("sandbox_backup_staging_operation_checkpoint_idx") + .on(table.sandboxId, table.sandboxIncarnation, table.operationId, table.dir) + .where(sql`${table.operationId} IS NOT NULL`), ], ); @@ -171,6 +327,7 @@ export const driverInstancesTable = sqliteTable( .$type<"acp-fallback" | "claude-agent-sdk" | "openai-runtime">() .notNull(), sandboxId: platformIdColumn("sandbox_id").notNull(), + sandboxIncarnation: integer("sandbox_incarnation").notNull().default(0), sandboxSessionId: platformIdColumn("sandbox_session_id").notNull(), status: text("status").$type().notNull(), statusChangedAt: integer("status_changed_at").notNull().default(0), @@ -186,6 +343,10 @@ export const driverInstancesTable = sqliteTable( sql`${table.status} IN ('provisioning', 'connecting', 'ready', 'stopping', 'stopped', 'failed')`, ), check("driver_instance_status_seq_check", sql`${table.statusSeq} >= 0`), + check( + "driver_instance_generation_incarnation_check", + sql`typeof(${table.generation}) = 'integer' AND ${table.generation} BETWEEN 0 AND 9007199254740991 AND typeof(${table.sandboxIncarnation}) = 'integer' AND ${table.sandboxIncarnation} BETWEEN 0 AND 9007199254740991 AND (${table.status} IN ('stopped', 'failed') OR ${table.sandboxIncarnation} > 0)`, + ), index("driver_instance_completed_idx").on(table.expiresAt, table.status), uniqueIndex("driver_instance_connection_idx") .on(table.connectionId) @@ -196,12 +357,13 @@ export const driverInstancesTable = sqliteTable( uniqueIndex("driver_instance_boot_token_hash_idx").on(table.bootTokenHash), index("driver_instance_sandbox_session_idx").on( table.sandboxId, + table.sandboxIncarnation, table.sandboxSessionId, table.status, table.updatedAt, ), uniqueIndex("driver_instance_live_sandbox_session_idx") - .on(table.sandboxId, table.sandboxSessionId) + .on(table.sandboxId, table.sandboxIncarnation, table.sandboxSessionId) .where(sql`${table.status} IN ('provisioning', 'connecting', 'ready', 'stopping')`), ], ); @@ -212,6 +374,7 @@ export const driverCommandsTable = sqliteTable( ackedAt: integer("acked_at"), completedAt: integer("completed_at"), deliveryConnectionId: text("delivery_connection_id"), + driverGeneration: integer("driver_generation"), driverInstanceId: platformIdColumn("driver_instance_id") .notNull() .references(() => driverInstancesTable.id, { onDelete: "cascade" }), @@ -226,6 +389,14 @@ export const driverCommandsTable = sqliteTable( status: text("status").$type().notNull(), }, (table) => [ + check( + "driver_command_generation_check", + sql`${table.driverGeneration} IS NULL OR (typeof(${table.driverGeneration}) = 'integer' AND ${table.driverGeneration} BETWEEN 0 AND 9007199254740991)`, + ), + check( + "driver_command_nonterminal_generation_check", + sql`${table.status} IN ('completed', 'failed', 'expired', 'cancelled') OR ${table.driverGeneration} IS NOT NULL`, + ), uniqueIndex("driver_command_instance_seq_idx").on(table.driverInstanceId, table.seq), index("driver_command_instance_status_idx").on( table.driverInstanceId, @@ -237,12 +408,13 @@ export const driverCommandsTable = sqliteTable( /** * The durable fence around a write-capable MCP call. A command is allowed to - * invoke the provider only after this record moves from intent to executing. + * invoke the provider only after this record moves from intent to claimed. */ export const externalToolEffectsTable = sqliteTable( "external_tool_effect", { attemptCount: integer("attempt_count").notNull().default(0), + claimToken: text("claim_token"), commandId: platformIdColumn("command_id") .notNull() .references(() => driverCommandsTable.id, { onDelete: "cascade" }), @@ -265,7 +437,11 @@ export const externalToolEffectsTable = sqliteTable( (table) => [ check( "external_tool_effect_status_check", - sql`${table.status} IN ('intent', 'executing', 'succeeded', 'unknown')`, + sql`${table.status} IN ('intent', 'claimed', 'succeeded', 'unknown')`, + ), + check( + "external_tool_effect_claim_token_uuid_check", + sql`${table.claimToken} IS NULL OR (length(${table.claimToken}) = 36 AND length(replace(${table.claimToken}, '-', '')) = 32 AND ${table.claimToken} = lower(${table.claimToken}) AND substr(${table.claimToken}, 9, 1) = '-' AND substr(${table.claimToken}, 14, 1) = '-' AND substr(${table.claimToken}, 15, 1) = '4' AND substr(${table.claimToken}, 19, 1) = '-' AND substr(${table.claimToken}, 20, 1) GLOB '[89ab]' AND substr(${table.claimToken}, 24, 1) = '-' AND replace(${table.claimToken}, '-', '') NOT GLOB '*[^0-9a-f]*')`, ), uniqueIndex("external_tool_effect_command_idx").on(table.commandId), uniqueIndex("external_tool_effect_idempotency_key_idx").on(table.idempotencyKey), @@ -282,6 +458,7 @@ export const externalToolEffectAttemptsTable = sqliteTable( "external_tool_effect_attempt", { attempt: integer("attempt").notNull(), + claimToken: text("claim_token").notNull(), completedAt: integer("completed_at"), createdAt: integer("created_at").notNull(), effectId: platformIdColumn("effect_id") @@ -297,7 +474,11 @@ export const externalToolEffectAttemptsTable = sqliteTable( }), check( "external_tool_effect_attempt_status_check", - sql`${table.status} IN ('executing', 'succeeded', 'unknown')`, + sql`${table.status} IN ('claimed', 'succeeded', 'unknown')`, + ), + check( + "external_tool_effect_attempt_claim_token_uuid_check", + sql`length(${table.claimToken}) = 36 AND length(replace(${table.claimToken}, '-', '')) = 32 AND ${table.claimToken} = lower(${table.claimToken}) AND substr(${table.claimToken}, 9, 1) = '-' AND substr(${table.claimToken}, 14, 1) = '-' AND substr(${table.claimToken}, 15, 1) = '4' AND substr(${table.claimToken}, 19, 1) = '-' AND substr(${table.claimToken}, 20, 1) GLOB '[89ab]' AND substr(${table.claimToken}, 24, 1) = '-' AND replace(${table.claimToken}, '-', '') NOT GLOB '*[^0-9a-f]*'`, ), index("external_tool_effect_attempt_status_idx").on(table.status, table.createdAt), ], @@ -341,6 +522,7 @@ export const nativeResumeRefsTable = sqliteTable( .$type<"acp_session_id" | "claude_session_id" | "openai_thread_id">() .notNull(), observedDriverInstanceId: platformIdColumn("observed_driver_instance_id"), + observedEventSeq: integer("observed_event_seq").notNull().default(0), observedSessionRunId: platformIdColumn("observed_session_run_id"), runtimeId: text("runtime_id") .$type<"acp-fallback" | "claude-agent-sdk" | "openai-runtime">() @@ -351,7 +533,10 @@ export const nativeResumeRefsTable = sqliteTable( updatedAt: integer("updated_at").notNull(), value: text("value").notNull(), }, - (table) => [index("native_resume_ref_runtime_updated_idx").on(table.runtimeId, table.updatedAt)], + (table) => [ + check("native_resume_ref_observed_event_seq_check", sql`${table.observedEventSeq} >= 0`), + index("native_resume_ref_runtime_updated_idx").on(table.runtimeId, table.updatedAt), + ], ); export type SandboxRow = typeof sandboxesTable.$inferSelect; diff --git a/pkgs/db/src/schema/session/core.schema.ts b/pkgs/db/src/schema/session/core.schema.ts index f68adf4f..c499ebbc 100644 --- a/pkgs/db/src/schema/session/core.schema.ts +++ b/pkgs/db/src/schema/session/core.schema.ts @@ -7,6 +7,8 @@ import type { PlatformId, AppId, RuntimeOperationId, + SandboxId, + SandboxSessionId, SessionId, SessionMessageId, SessionRunId, @@ -21,6 +23,8 @@ export const sessionsTable = sqliteTable( { agentId: platformIdColumn("agent_id").notNull(), archivedAt: integer("archived_at"), + autoTitleEventSeq: integer("auto_title_event_seq"), + cleanupOperationKind: text("cleanup_operation_kind").$type<"archive" | "delete">(), endUserId: text("end_user_id"), participantAccountId: platformIdColumn("attributed_user_id"), createdAt: integer("created_at").notNull(), @@ -42,6 +46,16 @@ export const sessionsTable = sqliteTable( statusOperationId: platformIdColumn("status_operation_id"), statusSeq: integer("status_seq").notNull().default(0), runtimeEventSeqCursor: integer("runtime_event_seq_cursor").notNull().default(0), + runtimeProvisioningHeartbeatAt: integer("runtime_provisioning_heartbeat_at"), + runtimeProvisioningOperationId: platformIdColumn( + "runtime_provisioning_operation_id", + ), + runtimeProvisioningRunId: platformIdColumn("runtime_provisioning_run_id"), + runtimeProvisioningSandboxId: platformIdColumn("runtime_provisioning_sandbox_id"), + runtimeProvisioningSandboxSessionId: platformIdColumn( + "runtime_provisioning_sandbox_session_id", + ), + runtimeProvisioningSandboxIncarnation: integer("runtime_provisioning_sandbox_incarnation"), title: text("title"), type: text("type").$type().notNull().default("preview"), updatedAt: integer("updated_at").notNull(), @@ -54,10 +68,26 @@ export const sessionsTable = sqliteTable( .default(false), }, (table) => [ + check( + "session_cleanup_operation_kind_check", + sql`${table.cleanupOperationKind} IS NULL OR (${table.cleanupOperationKind} IN ('archive', 'delete') AND ${table.archivedAt} IS NOT NULL AND ${table.status} IN ('IDLE', 'RESCHEDULING') AND (${table.statusOperationId} IS NOT NULL OR (${table.cleanupOperationKind} = 'archive' AND ${table.status} = 'IDLE')))`, + ), + check( + "session_runtime_provisioning_lease_check", + sql`(${table.runtimeProvisioningOperationId} IS NULL AND ${table.runtimeProvisioningRunId} IS NULL AND ${table.runtimeProvisioningSandboxId} IS NULL AND ${table.runtimeProvisioningHeartbeatAt} IS NULL) OR (${table.runtimeProvisioningOperationId} IS NOT NULL AND ${table.runtimeProvisioningSandboxId} IS NOT NULL AND ${table.runtimeProvisioningHeartbeatAt} IS NOT NULL AND typeof(${table.runtimeProvisioningHeartbeatAt}) = 'integer' AND ${table.runtimeProvisioningHeartbeatAt} >= 0 AND ${table.archivedAt} IS NULL AND ${table.cleanupOperationKind} IS NULL AND ${table.statusOperationId} IS NULL)`, + ), + check( + "session_runtime_provisioning_sandbox_pair_check", + sql`(${table.runtimeProvisioningSandboxSessionId} IS NULL AND ${table.runtimeProvisioningSandboxIncarnation} IS NULL) OR (${table.runtimeProvisioningOperationId} IS NOT NULL AND typeof(${table.runtimeProvisioningSandboxIncarnation}) = 'integer' AND ${table.runtimeProvisioningSandboxIncarnation} BETWEEN 0 AND 9007199254740991)`, + ), check( "session_status_check", sql`${table.status} IN ('IDLE', 'RUNNING', 'RESCHEDULING', 'TERMINATED')`, ), + check( + "session_auto_title_event_seq_check", + sql`${table.autoTitleEventSeq} IS NULL OR ${table.autoTitleEventSeq} >= 0`, + ), check("session_status_seq_check", sql`${table.statusSeq} >= 0`), index("session_agent_updated_idx").on(table.agentId, table.updatedAt, table.id), index("session_app_creator_archived_updated_idx").on( @@ -95,6 +125,19 @@ export const sessionsTable = sqliteTable( table.statusOperationId, table.updatedAt, ), + index("session_cleanup_operation_updated_idx").on( + table.cleanupOperationKind, + table.status, + table.updatedAt, + table.id, + ), + index("session_runtime_provisioning_heartbeat_idx").on( + table.runtimeProvisioningHeartbeatAt, + table.id, + ), + uniqueIndex("session_runtime_provisioning_sandbox_idx") + .on(table.runtimeProvisioningSandboxId) + .where(sql`${table.runtimeProvisioningOperationId} IS NOT NULL`), index("session_status_updated_idx").on(table.status, table.updatedAt, table.id), ], ); @@ -107,6 +150,10 @@ export const sessionMessagesTable = sqliteTable( createdByAccountId: platformIdColumn("created_by_account_id").notNull(), id: platformIdColumn("id").primaryKey(), planJson: text("plan_json"), + projectionFormat: text("projection_format") + .$type<"event_stream_v3" | "materialized">() + .notNull() + .default("materialized"), role: text("role").$type<"assistant" | "user">().notNull(), segmentsJson: text("segments_json"), seq: integer("seq").notNull(), @@ -116,6 +163,14 @@ export const sessionMessagesTable = sqliteTable( sessionRunId: platformIdColumn("session_run_id"), }, (table) => [ + check( + "session_message_projection_format_check", + sql`${table.projectionFormat} IN ('materialized', 'event_stream_v3')`, + ), + check( + "session_message_event_stream_v3_check", + sql`${table.projectionFormat} <> 'event_stream_v3' OR (${table.role} = 'assistant' AND ${table.sessionRunId} IS NOT NULL AND ${table.contentText} = '' AND ${table.planJson} IS NULL AND ${table.segmentsJson} IS NULL)`, + ), uniqueIndex("session_message_session_seq_idx").on(table.sessionId, table.seq), index("session_message_run_idx").on(table.sessionRunId), ], diff --git a/pkgs/db/src/schema/session/events.schema.ts b/pkgs/db/src/schema/session/events.schema.ts index 8f7b37e5..9165d925 100644 --- a/pkgs/db/src/schema/session/events.schema.ts +++ b/pkgs/db/src/schema/session/events.schema.ts @@ -7,13 +7,15 @@ import type { } from "@mosoo/contracts/session"; import type { AgentId, + DriverCommandId, DriverInstanceId, RuntimeEventId, SessionId, SessionModelCallId, SessionRunId, } from "@mosoo/id"; -import { index, integer, sqliteTable, text, uniqueIndex } from "drizzle-orm/sqlite-core"; +import { sql } from "drizzle-orm"; +import { check, index, integer, sqliteTable, text, uniqueIndex } from "drizzle-orm/sqlite-core"; import { platformIdColumn } from "../id-column"; import { sessionsTable } from "./core.schema"; @@ -38,6 +40,7 @@ export const sessionModelCallsTable = sqliteTable( nativeCallId: text("native_call_id"), outputTokens: integer("output_tokens"), provider: text("provider").notNull(), + sourceEventSeq: integer("source_event_seq").notNull().default(0), sessionId: platformIdColumn("session_id") .notNull() .references(() => sessionsTable.id, { onDelete: "cascade" }), @@ -51,6 +54,7 @@ export const sessionModelCallsTable = sqliteTable( updatedAt: integer("updated_at").notNull(), }, (table) => [ + check("session_model_call_source_event_seq_check", sql`${table.sourceEventSeq} >= 0`), index("session_model_call_run_created_idx").on(table.sessionRunId, table.createdAt), index("session_model_call_session_created_idx").on(table.sessionId, table.createdAt), uniqueIndex("session_model_call_run_key_idx").on(table.sessionRunId, table.callKey), @@ -62,36 +66,85 @@ export const sessionEventsTable = sqliteTable( "session_event", { agentId: platformIdColumn("agent_id").notNull(), + artifactAttemptId: text("artifact_attempt_id"), + artifactManifestJson: text("artifact_manifest_json"), + artifactManifestSha256: text("artifact_manifest_sha256"), contentText: text("content_text").notNull(), createdAt: integer("created_at").notNull(), endedAt: integer("ended_at").notNull(), eventType: text("event_type").notNull(), family: text("family").$type().notNull(), id: platformIdColumn("id").primaryKey(), + mcpCommandId: platformIdColumn("mcp_command_id"), occurredAt: integer("occurred_at").notNull(), processStatus: text("process_status").$type().notNull(), processType: text("process_type").$type().notNull(), runId: platformIdColumn("run_id"), + runtimeOperationEventJson: text("runtime_operation_event_json"), + semanticHash: text("semantic_hash"), seq: integer("seq").notNull(), sessionId: platformIdColumn("session_id") .notNull() .references(() => sessionsTable.id, { onDelete: "cascade" }), sourceEventId: text("source_event_id").notNull(), source: text("source").$type().notNull(), + streamId: text("stream_id"), + terminalEventJson: text("terminal_event_json"), toolCallId: text("tool_call_id"), + toolInputDeltaJson: text("tool_input_delta_json"), toolInputJson: text("tool_input_json"), toolName: text("tool_name"), + toolOutputDeltaText: text("tool_output_delta_text"), + toolOutputText: text("tool_output_text"), + toolParentMessageId: text("tool_parent_message_id"), + toolResultMessageId: text("tool_result_message_id"), + toolStatus: text("tool_status").$type<"cancelled" | "completed" | "failed" | "running">(), tokens: integer("tokens"), traceId: text("trace_id"), visibility: text("visibility").$type().notNull(), }, (table) => [ + check( + "session_event_artifact_manifest_check", + sql`(${table.artifactAttemptId} IS NULL AND ${table.artifactManifestJson} IS NULL AND ${table.artifactManifestSha256} IS NULL) OR (${table.artifactAttemptId} IS NOT NULL AND ${table.artifactManifestJson} IS NOT NULL AND json_valid(${table.artifactManifestJson}) = 1 AND json_extract(${table.artifactManifestJson}, '$.version') IS 1 AND json_type(${table.artifactManifestJson}, '$.captureStatus') IS 'text' AND json_extract(${table.artifactManifestJson}, '$.captureStatus') IN ('complete', 'omitted_file_limit', 'omitted_runtime_unavailable', 'omitted_size_limit', 'omitted_source_changed', 'omitted_source_missing') AND json_type(${table.artifactManifestJson}, '$.mode') IS 'text' AND json_extract(${table.artifactManifestJson}, '$.mode') IN ('delta', 'snapshot') AND (json_extract(${table.artifactManifestJson}, '$.captureStatus') = 'complete' OR json_array_length(${table.artifactManifestJson}, '$.files') = 0) AND json_extract(${table.artifactManifestJson}, '$.sourceEventId') IS ${table.sourceEventId} AND json_extract(${table.artifactManifestJson}, '$.semanticHash') IS ${table.semanticHash} AND json_type(${table.artifactManifestJson}, '$.files') IS 'array' AND ${table.artifactManifestSha256} IS NOT NULL AND length(${table.artifactManifestSha256}) = 64 AND ${table.artifactManifestSha256} = lower(${table.artifactManifestSha256}) AND ${table.artifactManifestSha256} NOT GLOB '*[^0-9a-f]*' AND ${table.semanticHash} IS NOT NULL AND ${table.eventType} IN ('file.change.updated', 'file.changed', 'run.completed'))`, + ), + check( + "session_event_mcp_command_check", + sql`${table.mcpCommandId} IS NULL OR (${table.eventType} = 'tool.call.updated' AND ${table.toolStatus} IS NOT NULL AND ${table.toolStatus} IN ('completed', 'failed', 'cancelled'))`, + ), + check( + "session_event_runtime_operation_event_json_check", + sql`${table.runtimeOperationEventJson} IS NULL OR (json_valid(${table.runtimeOperationEventJson}) = 1 AND json_extract(${table.runtimeOperationEventJson}, '$.kind') IS 'agent.task.updated' AND json_type(${table.runtimeOperationEventJson}, '$.payload') IS 'object' AND json_extract(${table.runtimeOperationEventJson}, '$.payload.status') IN ('updating', 'ready') AND ${table.semanticHash} IS NOT NULL AND ${table.eventType} = 'agent.task.updated')`, + ), + check( + "session_event_semantic_hash_check", + sql`${table.semanticHash} IS NULL OR (length(${table.semanticHash}) = 64 AND ${table.semanticHash} = lower(${table.semanticHash}) AND ${table.semanticHash} NOT GLOB '*[^0-9a-f]*')`, + ), + check( + "session_event_terminal_event_json_check", + sql`(${table.terminalEventJson} IS NULL AND NOT (${table.semanticHash} IS NOT NULL AND ${table.eventType} IN ('run.cancelled', 'run.completed', 'run.failed'))) OR (${table.terminalEventJson} IS NOT NULL AND json_valid(${table.terminalEventJson}) = 1 AND ${table.semanticHash} IS NOT NULL AND ${table.eventType} IN ('run.cancelled', 'run.completed', 'run.failed'))`, + ), + check( + "session_event_tool_input_kind_check", + sql`${table.toolInputDeltaJson} IS NULL OR ${table.toolInputJson} IS NULL`, + ), + check( + "session_event_tool_output_kind_check", + sql`${table.toolOutputDeltaText} IS NULL OR ${table.toolOutputText} IS NULL`, + ), + check( + "session_event_tool_status_check", + sql`${table.toolStatus} IS NULL OR ${table.toolStatus} IN ('running', 'completed', 'failed', 'cancelled')`, + ), index("session_event_agent_family_created_idx").on( table.agentId, table.family, table.createdAt, table.id, ), + uniqueIndex("session_event_artifact_attempt_idx") + .on(table.artifactAttemptId) + .where(sql`${table.artifactAttemptId} IS NOT NULL`), index("session_event_agent_visibility_created_idx").on( table.agentId, table.visibility, @@ -105,8 +158,23 @@ export const sessionEventsTable = sqliteTable( table.seq, ), index("session_event_run_event_type_idx").on(table.runId, table.eventType), + index("session_event_run_stream_process_seq_idx").on( + table.runId, + table.streamId, + table.processType, + table.seq, + ), + index("session_event_run_tool_call_seq_idx").on(table.runId, table.toolCallId, table.seq), uniqueIndex("session_event_session_seq_idx").on(table.sessionId, table.seq), uniqueIndex("session_event_session_source_idx").on(table.sessionId, table.sourceEventId), + uniqueIndex("session_event_run_terminal_winner_idx") + .on(table.sessionId, table.runId) + .where( + sql`${table.semanticHash} IS NOT NULL AND ${table.runId} IS NOT NULL AND ${table.eventType} IN ('run.cancelled', 'run.completed', 'run.failed')`, + ), + uniqueIndex("session_event_mcp_terminal_winner_idx") + .on(table.sessionId, table.mcpCommandId) + .where(sql`${table.mcpCommandId} IS NOT NULL`), ], ); diff --git a/pkgs/db/src/schema/session/runs.schema.ts b/pkgs/db/src/schema/session/runs.schema.ts index da835131..6ec33c9a 100644 --- a/pkgs/db/src/schema/session/runs.schema.ts +++ b/pkgs/db/src/schema/session/runs.schema.ts @@ -51,6 +51,7 @@ export const sessionRunsTable = sqliteTable( errorCode: text("error_code"), errorDetailsJson: text("error_details_json"), errorMessage: text("error_message"), + errorRetryable: integer("error_retryable", { mode: "boolean" }), id: platformIdColumn("id").primaryKey(), model: text("model"), provider: text("provider"), @@ -65,16 +66,25 @@ export const sessionRunsTable = sqliteTable( statusOperationId: platformIdColumn("status_operation_id"), statusSeq: integer("status_seq").notNull().default(0), statusSource: text("status_source").notNull().default("system"), + terminalReconciliationAttemptedAt: integer("terminal_reconciliation_attempted_at"), traceId: text("trace_id").notNull(), trigger: text("trigger").$type().notNull(), updatedAt: integer("updated_at").notNull(), }, (table) => [ + check( + "session_run_error_retryable_check", + sql`${table.errorRetryable} IS NULL OR (${table.errorRetryable} IN (false, true) AND ${table.errorCode} IS NOT NULL AND ${table.errorDetailsJson} IS NOT NULL AND ${table.errorMessage} IS NOT NULL)`, + ), check( "session_run_status_check", sql`${table.status} IN ('queued', 'booting', 'running', 'waiting_input', 'completed', 'failed', 'cancelled', 'expired')`, ), check("session_run_status_seq_check", sql`${table.statusSeq} >= 0`), + check( + "session_run_terminal_reconciliation_attempted_at_check", + sql`${table.terminalReconciliationAttemptedAt} IS NULL OR ${table.terminalReconciliationAttemptedAt} >= 0`, + ), index("session_run_driver_instance_idx").on(table.driverInstanceId, table.createdAt), uniqueIndex("session_run_active_driver_lease_idx") .on(table.driverInstanceId) @@ -83,6 +93,10 @@ export const sessionRunsTable = sqliteTable( ), index("session_run_session_created_at_idx").on(table.sessionId, table.createdAt), index("session_run_session_status_idx").on(table.sessionId, table.status), + index("session_run_terminal_reconciliation_attempt_idx").on( + sql`coalesce(${table.terminalReconciliationAttemptedAt}, ${table.updatedAt})`, + table.id, + ), ], ); diff --git a/pkgs/db/src/schema/usage.schema.ts b/pkgs/db/src/schema/usage.schema.ts index 629642e8..7e78c075 100644 --- a/pkgs/db/src/schema/usage.schema.ts +++ b/pkgs/db/src/schema/usage.schema.ts @@ -8,7 +8,9 @@ import type { SessionId, SessionRunId, } from "@mosoo/id"; +import { sql } from "drizzle-orm"; import { + check, index, integer, primaryKey, @@ -49,6 +51,7 @@ export const usageEventsTable = sqliteTable( sessionRunId: platformIdColumn("session_run_id"), source: text("source").notNull(), sourceEventId: text("source_event_id").notNull(), + sourceEventSeq: integer("source_event_seq").notNull().default(0), totalCostUsdMicros: integer("total_cost_usd_micros").notNull(), usageContract: text("usage_contract") .$type< @@ -59,6 +62,7 @@ export const usageEventsTable = sqliteTable( .notNull(), }, (table) => [ + check("usage_event_source_event_seq_check", sql`${table.sourceEventSeq} >= 0`), index("usage_event_app_created_idx").on(table.appId, table.createdAt), index("usage_event_organization_created_idx").on(table.organizationId, table.createdAt), index("usage_event_agent_created_idx").on(table.agentId, table.createdAt), diff --git a/pkgs/development-auth/package.json b/pkgs/development-auth/package.json index 9df1212e..ee33bc72 100644 --- a/pkgs/development-auth/package.json +++ b/pkgs/development-auth/package.json @@ -11,6 +11,6 @@ }, "devDependencies": { "typescript": "^6.0.3", - "vite-plus": "^0.1.23" + "vite-plus": "^0.3.0" } } diff --git a/pkgs/effects/package.json b/pkgs/effects/package.json index 02712021..35d4b51d 100644 --- a/pkgs/effects/package.json +++ b/pkgs/effects/package.json @@ -11,6 +11,6 @@ }, "devDependencies": { "typescript": "^6.0.3", - "vite-plus": "^0.1.23" + "vite-plus": "^0.3.0" } } diff --git a/pkgs/id/package.json b/pkgs/id/package.json index da302cc9..feffad4c 100644 --- a/pkgs/id/package.json +++ b/pkgs/id/package.json @@ -16,6 +16,6 @@ }, "devDependencies": { "typescript": "^6.0.3", - "vite-plus": "^0.1.23" + "vite-plus": "^0.3.0" } } diff --git a/pkgs/observability/package.json b/pkgs/observability/package.json index 9540584c..9c2e6b18 100644 --- a/pkgs/observability/package.json +++ b/pkgs/observability/package.json @@ -14,6 +14,6 @@ }, "devDependencies": { "typescript": "^6.0.3", - "vite-plus": "^0.1.23" + "vite-plus": "^0.3.0" } } diff --git a/pkgs/observability/src/metadata/log-metadata.ts b/pkgs/observability/src/metadata/log-metadata.ts index db22f61b..b5c551b9 100644 --- a/pkgs/observability/src/metadata/log-metadata.ts +++ b/pkgs/observability/src/metadata/log-metadata.ts @@ -206,7 +206,7 @@ export function normalizeLogMetadata(metadata: Record = {}): Lo } export function normalizeLogContext(context: Record = {}): LogContext { - return normalizeLogMetadata(context) as LogContext; + return normalizeLogMetadata(context); } export function toPrimitiveLogRecord(metadata: Record = {}): PrimitiveLogRecord { diff --git a/pkgs/public-api-client/package.json b/pkgs/public-api-client/package.json index 9d336db8..c716da82 100644 --- a/pkgs/public-api-client/package.json +++ b/pkgs/public-api-client/package.json @@ -16,6 +16,6 @@ "devDependencies": { "@types/bun": "^1.3.14", "typescript": "^6.0.3", - "vite-plus": "^0.1.23" + "vite-plus": "^0.3.0" } } diff --git a/pkgs/runtime-catalog/package.json b/pkgs/runtime-catalog/package.json index 80312b91..cd1abf9c 100644 --- a/pkgs/runtime-catalog/package.json +++ b/pkgs/runtime-catalog/package.json @@ -19,6 +19,6 @@ "devDependencies": { "jsonc-parser": "3.3.1", "typescript": "^6.0.3", - "vite-plus": "^0.1.23" + "vite-plus": "^0.3.0" } } diff --git a/pkgs/runtime-catalog/src/runtime-catalog.ts b/pkgs/runtime-catalog/src/runtime-catalog.ts index 341f1f3d..7db14f21 100644 --- a/pkgs/runtime-catalog/src/runtime-catalog.ts +++ b/pkgs/runtime-catalog/src/runtime-catalog.ts @@ -7,7 +7,6 @@ import { import type { ModelId, PresetModelEntry, - PresetModelProtocol, RuntimeModelIdentity, RuntimeModelProviderRef, } from "@mosoo/contracts/models"; @@ -167,7 +166,7 @@ function presetModel(input: (typeof GENERATED_PRESET_MODEL_CATALOG)[number]): Pr return { displayName: input.displayName, modelId: admitModelId(input.modelId), - protocol: input.protocol as PresetModelProtocol, + protocol: input.protocol, vendorId: admitProviderId(input.vendorId), vendorLabel: input.vendorLabel, }; diff --git a/pkgs/runtime-events/package.json b/pkgs/runtime-events/package.json index 517f686e..3a013600 100644 --- a/pkgs/runtime-events/package.json +++ b/pkgs/runtime-events/package.json @@ -17,6 +17,6 @@ }, "devDependencies": { "typescript": "^6.0.3", - "vite-plus": "^0.1.23" + "vite-plus": "^0.3.0" } } diff --git a/pkgs/runtime-events/src/process-draft.ts b/pkgs/runtime-events/src/process-draft.ts index a1ec78d9..417e4982 100644 --- a/pkgs/runtime-events/src/process-draft.ts +++ b/pkgs/runtime-events/src/process-draft.ts @@ -70,6 +70,7 @@ const sessionFamilyByDomain: Readonly> = { + "agent.tasks.replaced": "state", "runtime.config.updated": "diagnostics", "runtime.provisioning.updated": "provisioning", "runtime.sandbox.released": "sandbox", @@ -97,6 +98,7 @@ export function createProcessDraftFromRuntimeEvent(event: RuntimeEventEnvelope): }; } case "message.added": + case "message.cancelled": case "message.delta": case "message.completed": case "message.started": { @@ -107,7 +109,16 @@ export function createProcessDraftFromRuntimeEvent(event: RuntimeEventEnvelope): readRuntimeEventMessageRole(event) === "user" ? "user.message" : "agent.message.delta", }; } + case "message.failed": { + return { + content: "Message updated.", + status: "error", + type: + readRuntimeEventMessageRole(event) === "user" ? "user.message" : "agent.message.delta", + }; + } case "thought.delta": + case "thought.cancelled": case "thought.completed": case "thought.started": case "plan.updated": { @@ -149,16 +160,12 @@ export function createProcessDraftFromRuntimeEvent(event: RuntimeEventEnvelope): const toolCall = readRuntimeEventToolCallUpdate(event); return { content: toolCall.title ?? toolCall.kind ?? "Tool updated.", - type: - toolCall.status === "completed" || toolCall.status === "failed" - ? "tool.use.completed" - : "tool.use.started", + type: toolCall.status === "running" ? "tool.use.started" : "tool.use.completed", }; } case "mcp.tool.updated": case "tool.dynamic.updated": case "web.search.updated": - case "image.updated": case "agent.task.updated": case "review.updated": case "shell.command.updated": { @@ -168,8 +175,7 @@ export function createProcessDraftFromRuntimeEvent(event: RuntimeEventEnvelope): readRuntimeEventString(payload, "title") ?? readRuntimeEventString(payload, "kind") ?? "Tool updated.", - type: - status === "completed" || status === "failed" ? "tool.use.completed" : "tool.use.started", + type: status === "running" ? "tool.use.started" : "tool.use.completed", }; } case "file.changed": diff --git a/pkgs/runtime-events/src/runtime-event-payload.ts b/pkgs/runtime-events/src/runtime-event-payload.ts index d0f9db2b..1e2b009c 100644 --- a/pkgs/runtime-events/src/runtime-event-payload.ts +++ b/pkgs/runtime-events/src/runtime-event-payload.ts @@ -9,7 +9,7 @@ import type { DriverInstanceId, SessionId, SessionRunId } from "@mosoo/id"; import type { RuntimeEventEnvelope, RuntimeEventKind } from "./runtime-event"; export type RuntimeEventRecord = Record; -export type RuntimeEventToolStatus = "completed" | "failed" | "running"; +export type RuntimeEventToolStatus = "cancelled" | "completed" | "failed" | "running"; export type RuntimeEventMessageRole = "agent" | "user"; export type RuntimeRunLifecycleStatus = "IDLE" | "RESCHEDULING" | "RUNNING" | "TERMINATED"; export type RuntimeRunStatus = @@ -55,7 +55,9 @@ export interface RuntimeEventToolCallUpdate { readonly messageId: string | null; readonly parentMessageId: string | null; readonly rawInput: string | null; + readonly rawInputDelta: string | null; readonly rawOutput: string | null; + readonly rawOutputDelta: string | null; readonly status: RuntimeEventToolStatus; readonly title: string | null; readonly toolCallId: string; @@ -109,6 +111,15 @@ export interface RuntimeEventPayloadAdmissionContext { readonly traceId?: string | undefined; } +interface RuntimeEventPayloadSource { + readonly payload: unknown; +} + +interface RuntimeEventToolCallSource extends RuntimeEventPayloadSource { + readonly id: string; + readonly kind: string; +} + const runtimeTimingPaths = new Set(["cold", "prewarm", "unknown", "warm"]); const runtimeTimingSources = new Set(["api", "driver"]); const runtimeTimingStages = new Set([ @@ -132,7 +143,7 @@ const runStatuses = new Set([ "running", "waiting_input", ]); -const toolStatuses = new Set(["completed", "failed", "running"]); +const toolStatuses = new Set(["cancelled", "completed", "failed", "running"]); const payloadIdentityFields = new Set([ "occurredAt", "receivedAt", @@ -185,14 +196,12 @@ export function admitRuntimeEventPayload( return omitRuntimeEventPayloadIdentity(requireRuntimeEventPayloadRecord(kind, payload)); } case "message.added": + case "message.cancelled": case "message.completed": - case "message.started": - case "thought.completed": - case "thought.started": { + case "message.started": { const record = requireRuntimeEventPayloadRecord(kind, payload); requireOptionalMessageRole(record, kind); - requireOptionalString(record, "messageId", kind); - requireOptionalString(record, "thoughtId", kind); + requireRuntimeEventString(record, "messageId", kind); if (kind === "message.added" && !hasRuntimeEventTextContent(record)) { throw new Error("Runtime event message.added payload must include text content."); @@ -200,6 +209,21 @@ export function admitRuntimeEventPayload( return omitRuntimeEventPayloadIdentity(record); } + case "thought.cancelled": + case "thought.completed": + case "thought.started": { + const record = requireRuntimeEventPayloadRecord(kind, payload); + requireRuntimeEventString(record, "thoughtId", kind); + return omitRuntimeEventPayloadIdentity(record); + } + case "message.failed": { + const record = requireRuntimeEventPayloadRecord(kind, payload); + requireRuntimeEventString(record, "messageId", kind); + requireOptionalMessageRole(record, kind); + const admitted = omitRuntimeEventPayloadIdentity(record); + admitted["error"] = readStrictRuntimeRunError(kind, record["error"], "error"); + return admitted; + } case "message.delta": { const record = requireRuntimeEventPayloadRecord(kind, payload); @@ -207,7 +231,7 @@ export function admitRuntimeEventPayload( throw new Error("Runtime event message.delta payload must include text content."); } requireOptionalMessageRole(record, kind); - requireOptionalString(record, "messageId", kind); + requireRuntimeEventString(record, "messageId", kind); return omitRuntimeEventPayloadIdentity(record); } case "thought.delta": { @@ -216,7 +240,7 @@ export function admitRuntimeEventPayload( if (!hasRuntimeEventTextContent(record)) { throw new Error("Runtime event thought.delta payload must include text content."); } - requireOptionalString(record, "thoughtId", kind); + requireRuntimeEventString(record, "thoughtId", kind); return omitRuntimeEventPayloadIdentity(record); } case "tool.call.updated": { @@ -304,7 +328,7 @@ export function readRuntimeAgentTaskSnapshot(event: RuntimeEventEnvelope): Agent }; } -export function readRuntimeEventPayload(event: RuntimeEventEnvelope): RuntimeEventRecord { +export function readRuntimeEventPayload(event: RuntimeEventPayloadSource): RuntimeEventRecord { return isRuntimeEventRecord(event.payload) ? event.payload : {}; } @@ -381,7 +405,13 @@ export function readRuntimeEventPrimitiveRecord( } export function readRuntimeEventToolStatus(status: unknown): RuntimeEventToolStatus { - return status === "failed" ? "failed" : status === "completed" ? "completed" : "running"; + return status === "cancelled" + ? "cancelled" + : status === "failed" + ? "failed" + : status === "completed" + ? "completed" + : "running"; } export function readRuntimeEventToolStatusFromEvent( @@ -400,6 +430,20 @@ export function readRuntimeEventToolCallUpdate( return readStrictRuntimeToolCallUpdatePayload(event.payload); } +export function readRuntimeEventToolOutputSnapshot( + toolCall: RuntimeEventToolCallUpdate, +): string | null { + if (toolCall.rawOutputDelta !== null) { + return null; + } + + return ( + toolCall.rawOutput ?? + toolCall.content ?? + (toolCall.status === "failed" ? `${toolCall.title ?? toolCall.kind ?? "Tool"} failed.` : null) + ); +} + export function readRuntimeRunPayload(event: RuntimeEventEnvelope): RuntimeRunPayload { if (!isRuntimeRunPayloadKind(event.kind)) { throw new Error("Runtime run payload can only be read from run events."); @@ -447,15 +491,18 @@ export function readRuntimeEventMessageKey(event: RuntimeEventEnvelope): string switch (event.kind) { case "message.added": + case "message.cancelled": case "message.completed": case "message.delta": + case "message.failed": case "message.started": { - return readRuntimeEventString(payload, "messageId") ?? event.id; + return requireRuntimeEventString(payload, "messageId", event.kind); } + case "thought.cancelled": case "thought.completed": case "thought.delta": case "thought.started": { - return readRuntimeEventString(payload, "thoughtId") ?? event.id; + return requireRuntimeEventString(payload, "thoughtId", event.kind); } default: { return null; @@ -512,7 +559,7 @@ export function readRuntimeEventMessageDelta(event: RuntimeEventEnvelope): strin ); } -export function readRuntimeEventToolCallId(event: RuntimeEventEnvelope): string | null { +export function readRuntimeEventToolCallId(event: RuntimeEventToolCallSource): string | null { if (event.kind !== "tool.call.updated") { return null; } @@ -536,14 +583,32 @@ function readStrictRuntimeToolCallUpdatePayload(payload: unknown): RuntimeEventT const kind = "tool.call.updated"; const record = requireRuntimeEventPayloadRecord(kind, payload); const status = requireEnumValue(record, "status", toolStatuses, kind); + const rawInput = readOptionalRuntimeEventText(record, "rawInput", kind); + const rawInputDelta = readOptionalRuntimeEventText(record, "rawInputDelta", kind); + const rawOutput = readOptionalRuntimeEventText(record, "rawOutput", kind); + const rawOutputDelta = readOptionalRuntimeEventText(record, "rawOutputDelta", kind); + + if (rawInput !== null && rawInputDelta !== null) { + throw new Error( + "Runtime event tool.call.updated payload cannot contain both rawInput and rawInputDelta.", + ); + } + + if (rawOutput !== null && rawOutputDelta !== null) { + throw new Error( + "Runtime event tool.call.updated payload cannot contain both rawOutput and rawOutputDelta.", + ); + } return { content: readOptionalRuntimeEventContentString(record, "content", kind), kind: readOptionalRuntimeEventString(record, "kind", kind), messageId: readOptionalRuntimeEventString(record, "messageId", kind), parentMessageId: readOptionalRuntimeEventString(record, "parentMessageId", kind), - rawInput: readOptionalRuntimeEventString(record, "rawInput", kind), - rawOutput: readOptionalRuntimeEventString(record, "rawOutput", kind), + rawInput, + rawInputDelta, + rawOutput, + rawOutputDelta, status: status as RuntimeEventToolStatus, title: readOptionalRuntimeEventNullableString(record, "title", kind), toolCallId: requireRuntimeEventString(record, "toolCallId", kind), @@ -795,6 +860,11 @@ function readStrictRuntimeRunPayload( requireOptionalEnumValue(record, "lifecycle", runLifecycleStatuses, kind); requireOptionalEnumValue(record, "status", runStatuses, kind); + const status = record["status"]; + + if (status !== undefined && !isRuntimeRunStatusAllowedForKind(kind, status as string)) { + throw new Error(`Runtime event ${kind} payload status is inconsistent.`); + } requireOptionalString(record, "inputSummary", kind); requireOptionalString(record, "reason", kind); requireOptionalString(record, "requestedBy", kind); @@ -805,6 +875,16 @@ function readStrictRuntimeRunPayload( requireOptionalTimestampString(record, "completedAt", kind); requireOptionalTimestampString(record, "startedAt", kind); + if (kind === "run.completed") { + if ("finalMessageId" in record) { + requireRuntimeEventString(record, "finalMessageId", kind); + } + + if ("finalMessageText" in record) { + throw new Error("Runtime event run.completed payload finalMessageText is unsupported."); + } + } + const admitted = omitRuntimeEventPayloadIdentity(record); if ("run" in record && record["run"] !== undefined) { @@ -823,6 +903,22 @@ function readStrictRuntimeRunPayload( throw new Error("Runtime event run.failed payload must include an error."); } + if (kind === "run.failed") { + const error = requireRuntimeEventPayloadRecord(kind, record["error"], "error"); + const recoverable = record["recoverable"]; + const retryable = error["retryable"]; + + if (typeof recoverable !== "boolean" || typeof retryable !== "boolean") { + throw new Error( + "Runtime event run.failed payload recoverable and error.retryable must be booleans.", + ); + } + + if (recoverable !== retryable) { + throw new Error("Runtime event run.failed payload recoverable must match error.retryable."); + } + } + return admitted; } @@ -1152,6 +1248,17 @@ function readOptionalRuntimeEventString( return requireRuntimeEventString(record, field, kind); } +function readOptionalRuntimeEventText( + record: RuntimeEventRecord, + field: string, + kind: RuntimeEventKind, +): string | null { + requireOptionalNullableString(record, field, kind); + + const value = record[field]; + return typeof value === "string" ? value : null; +} + function requireOptionalNullableString( record: RuntimeEventRecord, field: string, @@ -1242,7 +1349,7 @@ function requireNonNegativeNumber(record: RuntimeEventRecord, field: string): nu return value; } -// Driver Contract v2 emits timing timestamps as ISO 8601 strings (completedAt/ +// Driver Contract v3 emits timing timestamps as ISO 8601 strings (completedAt/ // startedAt) while API-produced timing snapshots still carry epoch-ms fields // (completedAtMs/startedAtMs). Accept either and normalize to epoch ms. function requireTimingTimestampMs( diff --git a/pkgs/runtime-events/src/runtime-event.ts b/pkgs/runtime-events/src/runtime-event.ts index 961185f2..b985afdf 100644 --- a/pkgs/runtime-events/src/runtime-event.ts +++ b/pkgs/runtime-events/src/runtime-event.ts @@ -13,7 +13,7 @@ import type { import { admitRuntimeEventPayload } from "./runtime-event-payload"; -export const RUNTIME_EVENT_SCHEMA_VERSION = "2026-05-26" as const; +export const RUNTIME_EVENT_SCHEMA_VERSION = "2026-08-29" as const; export const RUNTIME_EVENT_KINDS = [ "account.limits.updated", @@ -37,7 +37,6 @@ export const RUNTIME_EVENT_KINDS = [ "file.indexed", "hook.completed", "hook.started", - "image.updated", "item.completed", "item.started", "item.updated", @@ -45,8 +44,10 @@ export const RUNTIME_EVENT_KINDS = [ "mcp.server.updated", "mcp.tool.updated", "message.added", + "message.cancelled", "message.completed", "message.delta", + "message.failed", "message.started", "model.routing.updated", "model.verification.updated", @@ -108,6 +109,7 @@ export const RUNTIME_EVENT_KINDS = [ "terminal.output.delta", "terminal.released", "thought.completed", + "thought.cancelled", "thought.delta", "thought.started", "tool.call.updated", @@ -120,6 +122,18 @@ export const RUNTIME_EVENT_KINDS = [ ] as const; export type RuntimeEventKind = (typeof RUNTIME_EVENT_KINDS)[number]; + +type TerminalSessionRunEventKind = Extract< + RuntimeEventKind, + "run.cancelled" | "run.completed" | "run.failed" +>; + +export function createSessionRunTerminalSourceId( + runId: SessionRunId, + kind: TerminalSessionRunEventKind, +): string { + return `session-run-terminal:${runId}:${kind}`; +} export type RuntimeEventActor = "agent" | "api" | "driver" | "system" | "tool" | "user"; export type RuntimeEventOrigin = "api" | "driver" | "file" | "runtime" | "system" | "viewer"; export type RuntimeEventVisibility = "owner_debug" | "participant" | "public" | "system_internal"; @@ -200,6 +214,70 @@ export interface RuntimeEventDraft { readonly visibility?: RuntimeEventVisibility | undefined; } +function sortRuntimeEventSemanticValue(value: unknown): unknown { + if (Array.isArray(value)) { + return value.map(sortRuntimeEventSemanticValue); + } + + if (value !== null && typeof value === "object") { + return Object.fromEntries( + Object.entries(value) + .filter(([, entry]) => entry !== undefined) + .toSorted(([left], [right]) => (left < right ? -1 : left > right ? 1 : 0)) + .map(([key, entry]) => [key, sortRuntimeEventSemanticValue(entry)]), + ); + } + + return value; +} + +/** Stable JSON for durable runtime semantics, independent of object insertion order. */ +export function stringifyRuntimeEventSemanticValue(value: unknown): string { + const serialized = JSON.stringify(sortRuntimeEventSemanticValue(value)); + + if (serialized === undefined) { + throw new TypeError("Runtime semantic value is not JSON serializable."); + } + + return serialized; +} + +/** Stable v3 replay identity; transport-generated ids, timestamps, and seq are intentionally absent. */ +export async function createRuntimeEventSemanticHash(event: RuntimeEventEnvelope): Promise { + const semanticEvent = { + actor: event.actor, + context: event.context ?? null, + correlationId: event.correlationId ?? null, + delivery: event.delivery, + driverInstanceId: event.driverInstanceId ?? null, + kind: event.kind, + native: event.native ?? null, + origin: event.origin, + payload: event.payload, + runId: event.runId ?? null, + runtimeId: event.runtimeId ?? null, + schemaVersion: event.schemaVersion, + sessionId: event.sessionId, + sourceEventId: event.sourceEventId ?? null, + traceId: event.traceId ?? null, + visibility: event.visibility, + }; + const platform = globalThis as unknown as { + crypto: { + subtle: { + digest(algorithm: "SHA-256", data: Uint8Array): Promise; + }; + }; + TextEncoder: new () => { encode(value: string): Uint8Array }; + }; + const bytes = new platform.TextEncoder().encode( + stringifyRuntimeEventSemanticValue(semanticEvent), + ); + const digest = new Uint8Array(await platform.crypto.subtle.digest("SHA-256", bytes)); + + return digest.toHex(); +} + const runtimeEventKindSet = new Set(RUNTIME_EVENT_KINDS); const runtimeEventActors = new Set(["agent", "api", "driver", "system", "tool", "user"]); const runtimeEventOrigins = new Set([ @@ -241,6 +319,20 @@ function isRecord(value: unknown): value is Record { return typeof value === "object" && value !== null && !Array.isArray(value); } +function assertExactKeys( + value: Record, + allowedKeys: readonly string[], + label: string, +): void { + const allowed = new Set(allowedKeys); + + for (const key of Object.keys(value)) { + if (!allowed.has(key)) { + throw new Error(`${label} field ${key} is unsupported.`); + } + } +} + function readString(value: Record, field: string): string | undefined { const entry = value[field]; return typeof entry === "string" && entry.length > 0 ? entry : undefined; @@ -388,6 +480,33 @@ export function parseRuntimeEventEnvelope(value: unknown): RuntimeEventEnvelope throw new Error("Runtime event must be an object."); } + assertExactKeys( + value, + [ + "actor", + "context", + "correlationId", + "delivery", + "driverInstanceId", + "id", + "kind", + "native", + "occurredAt", + "origin", + "payload", + "receivedAt", + "runId", + "runtimeId", + "schemaVersion", + "seq", + "sessionId", + "sourceEventId", + "traceId", + "visibility", + ], + "Runtime event", + ); + if (value["schemaVersion"] !== RUNTIME_EVENT_SCHEMA_VERSION) { throw new Error("Runtime event schema version is unsupported."); } @@ -503,9 +622,18 @@ function parseRuntimeEventContext(value: unknown): RuntimeEventContext { throw new Error("Runtime event context must be an object when provided."); } - if ("organizationId" in value) { - throw new Error("Runtime event context organizationId is not supported."); - } + assertExactKeys( + value, + [ + "agentId", + "callerId", + "deploymentVersionId", + "environmentRevisionId", + "executionActorId", + "surface", + ], + "Runtime event context", + ); const surface = value["surface"] === undefined ? null : parseRuntimeEventSurfaceContext(value["surface"]); @@ -538,6 +666,8 @@ function parseRuntimeEventSurfaceContext( throw new Error("Runtime event context surface must be an object when provided."); } + assertExactKeys(value, ["id", "triggerId", "type"], "Runtime event context surface"); + const type = value["type"]; if (!isRuntimeEventSurfaceType(type)) { @@ -558,6 +688,21 @@ function parseRuntimeEventNativeRef(value: unknown): RuntimeEventNativeRef { throw new Error("Runtime event native reference must be an object when provided."); } + assertExactKeys( + value, + [ + "eventName", + "itemId", + "protocolVersion", + "provider", + "requestId", + "sequence", + "threadId", + "turnId", + ], + "Runtime event native reference", + ); + const provider = readString(value, "provider"); if (provider === undefined) { diff --git a/pkgs/runtime-events/src/session-event-projection.ts b/pkgs/runtime-events/src/session-event-projection.ts index b99aad15..a6229904 100644 --- a/pkgs/runtime-events/src/session-event-projection.ts +++ b/pkgs/runtime-events/src/session-event-projection.ts @@ -10,6 +10,7 @@ import type { AgUiSessionEvent } from "@mosoo/ag-ui-session"; import type { RuntimeEventEnvelope } from "./runtime-event"; import { readRuntimeAgentTaskSnapshot, + readRuntimeEventMessageContent, readRuntimeEventPermissionRequest, readRuntimeEventMessageDelta, readRuntimeEventMessageKey, @@ -18,18 +19,38 @@ import { readRuntimeRunPayload, readRuntimeEventString, readRuntimeEventToolCallUpdate, + readRuntimeEventToolOutputSnapshot, toRuntimeRunLifecycleStatus, } from "./runtime-event-payload"; import { appRuntimeStatus, appRuntimeTimingRecorded } from "./session-runtime-timing"; -function createValidatedSessionCustomEvent(name: string, value: unknown): AgUiSessionEvent { +function runtimeEventTimestamp(event: RuntimeEventEnvelope): number { + return Date.parse(event.occurredAt); +} + +function createValidatedSessionCustomEvent( + name: string, + value: unknown, + timestamp?: number, +): AgUiSessionEvent { return parseAgUiSessionEvent({ name, + ...(timestamp === undefined ? {} : { timestamp }), type: EventType.CUSTOM, value, }); } +function requireRuntimeEventMessageKey(event: RuntimeEventEnvelope): string { + const messageKey = readRuntimeEventMessageKey(event); + + if (messageKey === null) { + throw new Error(`Runtime event ${event.kind} projection requires a stream ID.`); + } + + return messageKey; +} + function appPermissionRequest(event: RuntimeEventEnvelope): AgUiSessionEvent { const request = readRuntimeEventPermissionRequest(event); @@ -37,24 +58,23 @@ function appPermissionRequest(event: RuntimeEventEnvelope): AgUiSessionEvent { throw new Error("Runtime event permission projection requires a permission request event."); } + const permissionRequest = { + driverInstanceId: request.driverInstanceId, + rawInput: request.rawInput, + requestId: request.requestId, + runId: request.runId, + title: request.title, + toolCallId: request.toolCallId, + toolKind: request.toolKind, + }; return createValidatedSessionCustomEvent(MOSOO_CUSTOM_EVENT.sessionPermissionsUpdated.name, { - permissionRequests: [ - { - driverInstanceId: request.driverInstanceId, - rawInput: request.rawInput, - requestId: request.requestId, - runId: request.runId, - title: request.title, - toolCallId: request.toolCallId, - toolKind: request.toolKind, - }, - ], + permissionRequest, + permissionRequests: [], }); } function appMessageAdded(event: RuntimeEventEnvelope): AgUiSessionEvent[] { - const payload = readRuntimeEventPayload(event); - const content = readRuntimeEventString(payload, "content"); + const content = readRuntimeEventMessageContent(event); if (content === null) { return []; @@ -63,8 +83,9 @@ function appMessageAdded(event: RuntimeEventEnvelope): AgUiSessionEvent[] { return [ { delta: content, - messageId: readRuntimeEventString(payload, "messageId") ?? event.id, + messageId: requireRuntimeEventMessageKey(event), role: readRuntimeEventMessageRole(event) === "user" ? "user" : "assistant", + timestamp: runtimeEventTimestamp(event), type: EventType.TEXT_MESSAGE_CHUNK, }, ]; @@ -153,50 +174,25 @@ function appAgentTaskUpdated(event: RuntimeEventEnvelope): AgUiSessionEvent[] { function appPermissionResolved(event: RuntimeEventEnvelope): AgUiSessionEvent[] { const payload = readRuntimeEventPayload(event); - const permissionRequests = payload["permissionRequests"]; + const requestId = readRuntimeEventString(payload, "requestId"); + if (requestId === null || event.runId === undefined) { + throw new Error("A permission resolution requires exact request and run identities."); + } return [ createValidatedSessionCustomEvent(MOSOO_CUSTOM_EVENT.sessionPermissionsUpdated.name, { - permissionRequests: Array.isArray(permissionRequests) ? permissionRequests : [], + permissionRequests: [], + resolvedRequestId: requestId, + runId: event.runId, }), ]; } -function toolCallStartEvent( - toolCall: ReturnType, -): AgUiSessionEvent | null { - const parentMessageId = toolCall.parentMessageId ?? toolCall.messageId; - - if (parentMessageId === null) { - return null; - } - - return { - parentMessageId, - toolCallId: toolCall.toolCallId, - toolCallName: toolCall.title ?? toolCall.kind ?? "Tool", - type: EventType.TOOL_CALL_START, - }; -} - -function toolCallArgsEvent( - toolCall: ReturnType, -): AgUiSessionEvent | null { - if (toolCall.rawInput === null || toolCall.rawInput.length === 0) { - return null; - } - - return { - delta: toolCall.rawInput, - toolCallId: toolCall.toolCallId, - type: EventType.TOOL_CALL_ARGS, - }; -} - -function appendIfPresent(target: T[], value: T | null): void { - if (value !== null) { - target.push(value); - } +export function createRuntimeToolResultMessageId(input: { + runId: string | null; + toolCallId: string; +}): string { + return input.runId ?? `tool-result:${JSON.stringify([null, input.toolCallId])}`; } export function appRuntimeEventToAgUiSessionEvents( @@ -228,8 +224,9 @@ export function appRuntimeEventToAgUiSessionEvents( case "message.started": { return [ { - messageId: readRuntimeEventMessageKey(event) ?? event.id, + messageId: requireRuntimeEventMessageKey(event), role: readRuntimeEventMessageRole(event) === "user" ? "user" : "assistant", + timestamp: runtimeEventTimestamp(event), type: EventType.TEXT_MESSAGE_START, }, ]; @@ -238,15 +235,18 @@ export function appRuntimeEventToAgUiSessionEvents( return [ { delta: readRuntimeEventMessageDelta(event), - messageId: readRuntimeEventMessageKey(event) ?? event.id, + messageId: requireRuntimeEventMessageKey(event), + timestamp: runtimeEventTimestamp(event), type: EventType.TEXT_MESSAGE_CONTENT, }, ]; } - case "message.completed": { + case "message.cancelled": + case "message.completed": + case "message.failed": { return [ { - messageId: readRuntimeEventMessageKey(event) ?? event.id, + messageId: requireRuntimeEventMessageKey(event), type: EventType.TEXT_MESSAGE_END, }, ]; @@ -254,7 +254,7 @@ export function appRuntimeEventToAgUiSessionEvents( case "thought.started": { return [ { - messageId: readRuntimeEventMessageKey(event) ?? event.id, + messageId: requireRuntimeEventMessageKey(event), role: "reasoning", type: EventType.REASONING_MESSAGE_START, }, @@ -264,49 +264,42 @@ export function appRuntimeEventToAgUiSessionEvents( return [ { delta: readRuntimeEventMessageDelta(event), - messageId: readRuntimeEventMessageKey(event) ?? event.id, + messageId: requireRuntimeEventMessageKey(event), type: EventType.REASONING_MESSAGE_CONTENT, }, ]; } + case "thought.cancelled": case "thought.completed": { return [ { - messageId: readRuntimeEventMessageKey(event) ?? event.id, + messageId: requireRuntimeEventMessageKey(event), type: EventType.REASONING_MESSAGE_END, }, ]; } case "tool.call.updated": { const toolCall = readRuntimeEventToolCallUpdate(event); - const projected: AgUiSessionEvent[] = []; - - appendIfPresent(projected, toolCallStartEvent(toolCall)); - appendIfPresent(projected, toolCallArgsEvent(toolCall)); - - if (toolCall.status === "completed" || toolCall.status === "failed") { - const rawOutput = toolCall.rawOutput ?? toolCall.content; - const result = - rawOutput ?? - (toolCall.status === "failed" - ? `${toolCall.title ?? toolCall.kind ?? "Tool"} failed.` - : null); - - return result === null - ? [...projected, { toolCallId: toolCall.toolCallId, type: EventType.TOOL_CALL_END }] - : [ - ...projected, - { - content: result, - messageId: toolCall.messageId ?? event.id, - toolCallId: toolCall.toolCallId, - type: EventType.TOOL_CALL_RESULT, - }, - { toolCallId: toolCall.toolCallId, type: EventType.TOOL_CALL_END }, - ]; - } - - return projected; + return [ + createValidatedSessionCustomEvent( + MOSOO_CUSTOM_EVENT.sessionToolUpdated.name, + { + inputDelta: toolCall.rawInputDelta, + inputSnapshot: toolCall.rawInput, + outputDelta: toolCall.rawOutputDelta, + outputSnapshot: readRuntimeEventToolOutputSnapshot(toolCall), + parentMessageId: toolCall.parentMessageId ?? toolCall.messageId, + resultMessageId: createRuntimeToolResultMessageId({ + runId: event.runId ?? null, + toolCallId: toolCall.toolCallId, + }), + runId: event.runId ?? null, + toolCallId: toolCall.toolCallId, + toolName: toolCall.title ?? toolCall.kind ?? "Tool", + }, + runtimeEventTimestamp(event), + ), + ]; } case "plan.updated": { const payload = readRuntimeEventPayload(event); diff --git a/pkgs/runtime-events/tests/ag-ui-adapter.test.ts b/pkgs/runtime-events/tests/ag-ui-adapter.test.ts index 18cfdb45..514d4650 100644 --- a/pkgs/runtime-events/tests/ag-ui-adapter.test.ts +++ b/pkgs/runtime-events/tests/ag-ui-adapter.test.ts @@ -6,6 +6,7 @@ import { PLATFORM_ID_FIXTURES } from "@mosoo/id/testing"; import { createProcessDraftFromRuntimeEvent, createRuntimeEvent, + createRuntimeToolResultMessageId, parseRuntimeEventEnvelope, appRuntimeEventToAgUiSessionEvents, toRuntimeEventInput, @@ -36,6 +37,15 @@ function first(values: readonly T[]): T { } describe("runtime event AG-UI adapter", () => { + test("uses the Run ULID as the parentless tool message identity", () => { + expect( + createRuntimeToolResultMessageId({ + runId: PLATFORM_ID_FIXTURES.sessionRun, + toolCallId: "tool-1", + }), + ).toBe(PLATFORM_ID_FIXTURES.sessionRun); + }); + test("normalizes canonical driver drafts into canonical runtime envelopes", () => { const event = first( toRuntimeEventInput(createContext(), { @@ -50,7 +60,7 @@ describe("runtime event AG-UI adapter", () => { expect(event.kind).toBe("run.started"); expect(event.runId).toBe(PLATFORM_ID_FIXTURES.sessionRun); expect(event.sessionId).toBe(PLATFORM_ID_FIXTURES.session); - expect(event.schemaVersion).toBe("2026-05-26"); + expect(event.schemaVersion).toBe("2026-08-29"); }); test("projects task snapshots as one participant state replacement", () => { @@ -95,6 +105,103 @@ describe("runtime event AG-UI adapter", () => { expect(() => parseRuntimeEventEnvelope({ ...event, visibility: "owner_debug" })).toThrow(); }); + test.each([ + [ + "message.cancelled", + { messageId: "message-cancelled", role: "agent" }, + EventType.TEXT_MESSAGE_END, + ], + [ + "message.completed", + { messageId: "message-completed", role: "agent" }, + EventType.TEXT_MESSAGE_END, + ], + [ + "message.failed", + { + error: { code: "runtime.failed", message: "Runtime failed." }, + messageId: "message-failed", + role: "agent", + }, + EventType.TEXT_MESSAGE_END, + ], + ["thought.cancelled", { thoughtId: "thought-cancelled" }, EventType.REASONING_MESSAGE_END], + ["thought.completed", { thoughtId: "thought-completed" }, EventType.REASONING_MESSAGE_END], + ] as const)("projects %s as its canonical AG-UI end", (kind, payload, type) => { + const event = first(toRuntimeEventInput(createContext(), { kind, payload })); + const messageId = "messageId" in payload ? payload.messageId : payload.thoughtId; + + expect(appRuntimeEventToAgUiSessionEvents(event)).toEqual([{ messageId, type }]); + }); + + test.each([ + ["message.cancelled", {}, "messageId"], + ["thought.cancelled", {}, "thoughtId"], + ] as const)("does not use the envelope ID for anonymous %s", (kind, payload, idField) => { + const event = createRuntimeEvent({ + id: createPlatformId(), + kind, + occurredAt: OCCURRED_AT, + payload, + sessionId: PLATFORM_ID_FIXTURES.session, + }); + + expect(() => appRuntimeEventToAgUiSessionEvents(event)).toThrow( + `${idField} must be a non-empty string`, + ); + }); + + test("marks failed message drafts as errors without appending the failure to message text", () => { + const event = first( + toRuntimeEventInput(createContext(), { + kind: "message.failed", + payload: { + error: { code: "runtime.failed", message: "Sensitive provider failure." }, + messageId: "message-1", + role: "agent", + }, + }), + ); + + expect(createProcessDraftFromRuntimeEvent(event)).toEqual({ + content: "Message updated.", + status: "error", + type: "agent.message.delta", + }); + }); + + test("projects cancelled tools without fabricating a result", () => { + const event = first( + toRuntimeEventInput(createContext(), { + kind: "tool.call.updated", + payload: { status: "cancelled", toolCallId: "tool-1" }, + }), + ); + + expect(appRuntimeEventToAgUiSessionEvents(event)).toEqual([ + { + name: MOSOO_CUSTOM_EVENT.sessionToolUpdated.name, + timestamp: Date.parse(OCCURRED_AT), + type: EventType.CUSTOM, + value: { + inputDelta: null, + inputSnapshot: null, + outputDelta: null, + outputSnapshot: null, + parentMessageId: null, + resultMessageId: createRuntimeToolResultMessageId({ + runId: event.runId ?? null, + toolCallId: "tool-1", + }), + runId: event.runId ?? null, + toolCallId: "tool-1", + toolName: "Tool", + }, + }, + ]); + expect(createProcessDraftFromRuntimeEvent(event).type).toBe("tool.use.completed"); + }); + test("uses the build context run id as the canonical runtime run id", () => { const event = first( toRuntimeEventInput( @@ -144,7 +251,9 @@ describe("runtime event AG-UI adapter", () => { error: { code: "runtime.failed", message: "Runtime failed.", + retryable: false, }, + recoverable: false, }, runId: PLATFORM_ID_FIXTURES.sessionRun, sessionId: PLATFORM_ID_FIXTURES.session, @@ -351,7 +460,7 @@ describe("runtime event AG-UI adapter", () => { message: "ok", }, runId: PLATFORM_ID_FIXTURES.sessionRun.toLowerCase(), - schemaVersion: "2026-05-26", + schemaVersion: "2026-08-29", sessionId: PLATFORM_ID_FIXTURES.session.toLowerCase(), visibility: "participant", }; @@ -379,7 +488,7 @@ describe("runtime event AG-UI adapter", () => { organizationId: PLATFORM_ID_FIXTURES.organization, }, }), - ).toThrow("Runtime event context organizationId is not supported."); + ).toThrow("Runtime event context field organizationId is unsupported."); }); test("rejects malformed public runtime event payloads at ingress", () => { @@ -504,8 +613,8 @@ describe("runtime event AG-UI adapter", () => { } expect(deliveryEvent.name).toBe(MOSOO_CUSTOM_EVENT.sessionPermissionsUpdated.name); - expect(deliveryEvent.value.permissionRequests).toHaveLength(1); - expect(deliveryEvent.value.permissionRequests[0]).toMatchObject({ + expect(deliveryEvent.value.permissionRequests).toEqual([]); + expect(deliveryEvent.value.permissionRequest).toMatchObject({ driverInstanceId: PLATFORM_ID_FIXTURES.driverInstance, rawInput: '{"command":"pwd"}', requestId: "permission-1", @@ -516,7 +625,7 @@ describe("runtime event AG-UI adapter", () => { }); }); - test("projects failed tool output as a tool result before ending the call", () => { + test("projects failed tool output through the shared tool update", () => { const event = createRuntimeEvent({ id: createPlatformId(), kind: "tool.call.updated", @@ -533,20 +642,101 @@ describe("runtime event AG-UI adapter", () => { expect(appRuntimeEventToAgUiSessionEvents(event)).toEqual([ { - content: - "Tool failed before returning a result: Runtime driver control socket is not connected.", - messageId: event.id, + name: MOSOO_CUSTOM_EVENT.sessionToolUpdated.name, + timestamp: Date.parse(OCCURRED_AT), + type: EventType.CUSTOM, + value: { + inputDelta: null, + inputSnapshot: null, + outputDelta: null, + outputSnapshot: + "Tool failed before returning a result: Runtime driver control socket is not connected.", + parentMessageId: null, + resultMessageId: createRuntimeToolResultMessageId({ + runId: event.runId ?? null, + toolCallId: "tool-1", + }), + runId: event.runId ?? null, + toolCallId: "tool-1", + toolName: "Shell", + }, + }, + ]); + }); + + test("preserves an explicit empty tool output snapshot", () => { + const event = createRuntimeEvent({ + id: createPlatformId(), + kind: "tool.call.updated", + occurredAt: OCCURRED_AT, + payload: { + rawOutput: "", + status: "completed", toolCallId: "tool-1", - type: EventType.TOOL_CALL_RESULT, }, + sessionId: PLATFORM_ID_FIXTURES.session, + }); + + expect(appRuntimeEventToAgUiSessionEvents(event)).toEqual([ { + name: MOSOO_CUSTOM_EVENT.sessionToolUpdated.name, + timestamp: Date.parse(OCCURRED_AT), + type: EventType.CUSTOM, + value: { + inputDelta: null, + inputSnapshot: null, + outputDelta: null, + outputSnapshot: "", + parentMessageId: null, + resultMessageId: createRuntimeToolResultMessageId({ + runId: event.runId ?? null, + toolCallId: "tool-1", + }), + runId: event.runId ?? null, + toolCallId: "tool-1", + toolName: "Tool", + }, + }, + ]); + }); + + test("projects a tool output delta as an append", () => { + const event = createRuntimeEvent({ + id: createPlatformId(), + kind: "tool.call.updated", + occurredAt: OCCURRED_AT, + payload: { + rawOutputDelta: "partial", + status: "completed", toolCallId: "tool-1", - type: EventType.TOOL_CALL_END, + }, + sessionId: PLATFORM_ID_FIXTURES.session, + }); + + expect(appRuntimeEventToAgUiSessionEvents(event)).toEqual([ + { + name: MOSOO_CUSTOM_EVENT.sessionToolUpdated.name, + timestamp: Date.parse(OCCURRED_AT), + type: EventType.CUSTOM, + value: { + inputDelta: null, + inputSnapshot: null, + outputDelta: "partial", + outputSnapshot: null, + parentMessageId: null, + resultMessageId: createRuntimeToolResultMessageId({ + runId: event.runId ?? null, + toolCallId: "tool-1", + }), + runId: event.runId ?? null, + toolCallId: "tool-1", + toolName: "Tool", + }, }, ]); }); - test("projects running tool input updates as tool args without fabricating a new start", () => { + test("projects a running tool input snapshot as a replacement", () => { const event = createRuntimeEvent({ id: createPlatformId(), kind: "tool.call.updated", @@ -561,14 +751,28 @@ describe("runtime event AG-UI adapter", () => { expect(appRuntimeEventToAgUiSessionEvents(event)).toEqual([ { - delta: '{"command":"pwd"}', - toolCallId: "tool-1", - type: EventType.TOOL_CALL_ARGS, + name: MOSOO_CUSTOM_EVENT.sessionToolUpdated.name, + timestamp: Date.parse(OCCURRED_AT), + type: EventType.CUSTOM, + value: { + inputDelta: null, + inputSnapshot: '{"command":"pwd"}', + outputDelta: null, + outputSnapshot: null, + parentMessageId: null, + resultMessageId: createRuntimeToolResultMessageId({ + runId: event.runId ?? null, + toolCallId: "tool-1", + }), + runId: event.runId ?? null, + toolCallId: "tool-1", + toolName: "Tool", + }, }, ]); }); - test("projects running tool start metadata before tool args", () => { + test("projects running tool identity and input snapshot atomically", () => { const event = createRuntimeEvent({ id: createPlatformId(), kind: "tool.call.updated", @@ -585,15 +789,23 @@ describe("runtime event AG-UI adapter", () => { expect(appRuntimeEventToAgUiSessionEvents(event)).toEqual([ { - parentMessageId: "assistant-1", - toolCallId: "tool-1", - toolCallName: "Bash", - type: EventType.TOOL_CALL_START, - }, - { - delta: '{"command":"pwd"}', - toolCallId: "tool-1", - type: EventType.TOOL_CALL_ARGS, + name: MOSOO_CUSTOM_EVENT.sessionToolUpdated.name, + timestamp: Date.parse(OCCURRED_AT), + type: EventType.CUSTOM, + value: { + inputDelta: null, + inputSnapshot: '{"command":"pwd"}', + outputDelta: null, + outputSnapshot: null, + parentMessageId: "assistant-1", + resultMessageId: createRuntimeToolResultMessageId({ + runId: event.runId ?? null, + toolCallId: "tool-1", + }), + runId: event.runId ?? null, + toolCallId: "tool-1", + toolName: "Bash", + }, }, ]); }); @@ -607,6 +819,7 @@ describe("runtime event AG-UI adapter", () => { outcome: "allow_once", requestId: "permission-1", }, + runId: PLATFORM_ID_FIXTURES.sessionRun, sessionId: PLATFORM_ID_FIXTURES.session, }); @@ -616,6 +829,8 @@ describe("runtime event AG-UI adapter", () => { name: MOSOO_CUSTOM_EVENT.sessionPermissionsUpdated.name, value: { permissionRequests: [], + resolvedRequestId: "permission-1", + runId: PLATFORM_ID_FIXTURES.sessionRun, }, }); }); diff --git a/pkgs/runtime-events/tests/runtime-event-ingress.test.ts b/pkgs/runtime-events/tests/runtime-event-ingress.test.ts index fdb6d483..b252e878 100644 --- a/pkgs/runtime-events/tests/runtime-event-ingress.test.ts +++ b/pkgs/runtime-events/tests/runtime-event-ingress.test.ts @@ -4,17 +4,40 @@ import { createPlatformId } from "@mosoo/id"; import { PLATFORM_ID_FIXTURES } from "@mosoo/id/testing"; import { createRuntimeEvent, + createRuntimeEventSemanticHash, getRuntimeEventSessionFamily, ingestRuntimeDiagnosticEvent, ingestRuntimeEventInput, isRuntimeEventRecord, + parseRuntimeEventEnvelope, + readRuntimeEventPayload, readRuntimeEventPermissionRequest, + readRuntimeEventToolCallId, + readRuntimeEventToolCallUpdate, toRuntimeEventInput, } from "@mosoo/runtime-events"; import type { RuntimeEventBuildContext } from "@mosoo/runtime-events"; const OCCURRED_AT = "2026-05-26T00:00:00.000Z"; +declare const foreignRuntimeEventIdBrand: unique symbol; +interface ForeignRuntimeEvent { + readonly id: string & { readonly [foreignRuntimeEventIdBrand]: "foreign" }; + readonly kind: "tool.call.updated"; + readonly payload: unknown; +} + +const payloadReaderAcceptsForeignEvent: ForeignRuntimeEvent extends Parameters< + typeof readRuntimeEventPayload +>[0] + ? true + : false = true; +const toolCallReaderAcceptsForeignEvent: ForeignRuntimeEvent extends Parameters< + typeof readRuntimeEventToolCallId +>[0] + ? true + : false = true; + function createContext(): RuntimeEventBuildContext { return { createId: createPlatformId, @@ -37,6 +60,67 @@ function first(values: readonly T[]): T { } describe("runtime event ingress", () => { + test("keeps foreign ID brands out of pure event readers", () => { + const event = { + id: "foreign-event-1", + kind: "tool.call.updated", + payload: { toolCallId: "tool-1" }, + }; + + expect(payloadReaderAcceptsForeignEvent).toBeTrue(); + expect(toolCallReaderAcceptsForeignEvent).toBeTrue(); + expect(readRuntimeEventPayload(event)).toEqual({ toolCallId: "tool-1" }); + expect(readRuntimeEventToolCallId(event)).toBe("tool-1"); + }); + + test("rejects the previous runtime event schema", () => { + expect(() => + parseRuntimeEventEnvelope({ + ...createRuntimeEvent({ + actor: "driver", + id: PLATFORM_ID_FIXTURES.runtimeEvent, + kind: "diagnostic.reported", + occurredAt: OCCURRED_AT, + origin: "driver", + payload: { message: "ok" }, + sessionId: PLATFORM_ID_FIXTURES.session, + }), + schemaVersion: "2026-05-26", + }), + ).toThrow("Runtime event schema version is unsupported"); + }); + + test.each([ + ["envelope", { extra: true }], + ["context", { context: { extra: true } }], + ["surface", { context: { surface: { extra: true, type: "web" } } }], + ["native", { native: { extra: true, provider: "openai" } }], + ] as const)("rejects undeclared canonical %s fields", (_label, override) => { + const event = createRuntimeEvent({ + id: PLATFORM_ID_FIXTURES.runtimeEvent, + kind: "diagnostic.reported", + occurredAt: OCCURRED_AT, + payload: { message: "ok" }, + sessionId: PLATFORM_ID_FIXTURES.session, + }); + + expect(() => parseRuntimeEventEnvelope({ ...event, ...override })).toThrow("unsupported"); + }); + + test("preserves extension payload fields while parsing the exact envelope", () => { + const event = parseRuntimeEventEnvelope( + createRuntimeEvent({ + id: PLATFORM_ID_FIXTURES.runtimeEvent, + kind: "diagnostic.reported", + occurredAt: OCCURRED_AT, + payload: { extension: { enabled: true }, message: "ok" }, + sessionId: PLATFORM_ID_FIXTURES.session, + }), + ); + + expect(event.payload).toEqual({ extension: { enabled: true }, message: "ok" }); + }); + test("owns session family classification for projected runtime events", () => { expect( getRuntimeEventSessionFamily( @@ -64,6 +148,21 @@ describe("runtime event ingress", () => { }), ), ).toBe("tool"); + expect( + getRuntimeEventSessionFamily( + createRuntimeEvent({ + actor: "driver", + driverInstanceId: PLATFORM_ID_FIXTURES.driverInstance, + id: PLATFORM_ID_FIXTURES.runtimeEvent, + kind: "agent.tasks.replaced", + occurredAt: OCCURRED_AT, + origin: "driver", + payload: { tasks: [] }, + runId: PLATFORM_ID_FIXTURES.sessionRun, + sessionId: PLATFORM_ID_FIXTURES.session, + }), + ), + ).toBe("state"); }); test("returns typed rejections for unsupported event kinds", () => { @@ -100,6 +199,117 @@ describe("runtime event ingress", () => { expect(outcome.rejection.kind).toBe("tool.call.updated"); }); + test.each([ + ["input snapshot", { rawInput: "" }, { rawInput: "", rawInputDelta: null }], + ["input delta", { rawInputDelta: "" }, { rawInput: null, rawInputDelta: "" }], + ["output snapshot", { rawOutput: "" }, { rawOutput: "", rawOutputDelta: null }], + ["output delta", { rawOutputDelta: "" }, { rawOutput: null, rawOutputDelta: "" }], + ] as const)("preserves an explicit empty tool %s", (_label, fields, expected) => { + const outcome = ingestRuntimeEventInput(createContext(), { + kind: "tool.call.updated", + payload: { ...fields, status: "running", toolCallId: "tool-1" }, + }); + + if (outcome.status !== "accepted") { + throw new Error("Expected a canonical tool event."); + } + + expect(readRuntimeEventToolCallUpdate(outcome.event)).toMatchObject(expected); + }); + + test.each([ + [{ rawInput: "{}", rawInputDelta: "{" }, "rawInput"], + [{ rawOutput: "done", rawOutputDelta: "d" }, "rawOutput"], + ] as const)("rejects mixed tool %s snapshot and delta fields", (fields, field) => { + expect( + ingestRuntimeEventInput(createContext(), { + kind: "tool.call.updated", + payload: { ...fields, status: "running", toolCallId: "tool-1" }, + }), + ).toMatchObject({ + rejection: { code: "malformed_event", message: expect.stringContaining(field) }, + }); + }); + + test.each([ + ["message.cancelled", { messageId: "message-1", role: "agent" }], + [ + "message.failed", + { + error: { code: "runtime.failed", message: "Runtime failed." }, + messageId: "message-1", + role: "agent", + }, + ], + ["thought.cancelled", { thoughtId: "thought-1" }], + ["tool.call.updated", { status: "cancelled", toolCallId: "tool-1" }], + ])("admits canonical terminal payloads for %s", (kind, payload) => { + expect(ingestRuntimeEventInput(createContext(), { kind, payload })).toMatchObject({ + event: { kind }, + status: "accepted", + }); + }); + + test.each([ + "message.added", + "message.cancelled", + "message.completed", + "message.delta", + "message.failed", + "message.started", + ])("rejects %s without messageId", (kind) => { + expect( + ingestRuntimeEventInput(createContext(), { + kind, + payload: + kind === "message.failed" + ? { error: { code: "runtime.failed", message: "Runtime failed." } } + : kind === "message.added" || kind === "message.delta" + ? { content: "text" } + : {}, + }), + ).toMatchObject({ + rejection: { code: "malformed_event", kind }, + status: "rejected", + }); + }); + + test.each(["thought.cancelled", "thought.completed", "thought.delta", "thought.started"])( + "rejects %s without thoughtId", + (kind) => { + expect( + ingestRuntimeEventInput(createContext(), { + kind, + payload: kind === "thought.delta" ? { content: "text" } : {}, + }), + ).toMatchObject({ + rejection: { code: "malformed_event", kind }, + status: "rejected", + }); + }, + ); + + test.each([ + {}, + { error: { code: "runtime.failed", message: "Runtime failed." } }, + { messageId: "message-1" }, + { error: { code: "runtime.failed" }, messageId: "message-1" }, + { error: { message: "Runtime failed." }, messageId: "message-1" }, + ])("rejects malformed message.failed payload %#", (payload) => { + expect( + ingestRuntimeEventInput(createContext(), { + kind: "message.failed", + payload, + }), + ).toMatchObject({ + rejection: { + code: "malformed_event", + kind: "message.failed", + }, + status: "rejected", + }); + }); + test("rejects malformed run lifecycle payloads before projection can repair them", () => { const missingRunId = ingestRuntimeEventInput( { @@ -150,6 +360,94 @@ describe("runtime event ingress", () => { }); }); + test.each([ + ["run.completed", { status: "running" }], + [ + "run.failed", + { + error: { code: "failed", message: "failed", retryable: false }, + recoverable: false, + status: "running", + }, + ], + ["run.started", { startedAt: OCCURRED_AT, status: "completed" }], + ["run.queued", { status: "failed" }], + ] as const)("rejects a status owned by another run event kind for %s", (kind, payload) => { + expect(ingestRuntimeEventInput(createContext(), { kind, payload })).toMatchObject({ + rejection: { code: "malformed_event", kind }, + status: "rejected", + }); + }); + + test.each([ + ["run.completed", { status: "completed" }], + [ + "run.failed", + { + error: { code: "failed", message: "failed", retryable: false }, + recoverable: false, + status: "failed", + }, + ], + ["run.started", { startedAt: OCCURRED_AT, status: "running" }], + ["run.queued", { status: "queued" }], + ] as const)("accepts the canonical top-level status for %s", (kind, payload) => { + expect(ingestRuntimeEventInput(createContext(), { kind, payload })).toMatchObject({ + event: { kind }, + status: "accepted", + }); + }); + + test.each([ + [false, false, "accepted"], + [true, true, "accepted"], + [false, true, "rejected"], + [true, false, "rejected"], + ] as const)( + "requires run.failed recoverable=%s to match error.retryable=%s", + (recoverable, retryable, status) => { + expect( + ingestRuntimeEventInput(createContext(), { + kind: "run.failed", + payload: { + error: { code: "runtime.failed", message: "Runtime failed.", retryable }, + recoverable, + }, + }).status, + ).toBe(status); + }, + ); + + test("owns the completed-run final message reference schema at the Mosoo ingress", () => { + expect( + ingestRuntimeEventInput(createContext(), { + kind: "run.completed", + payload: { finalMessageId: "message-1", stopReason: "end_turn" }, + }).status, + ).toBe("accepted"); + expect( + ingestRuntimeEventInput(createContext(), { + kind: "run.completed", + payload: { stopReason: "end_turn" }, + }).status, + ).toBe("accepted"); + + for (const payload of [ + { finalMessageId: "" }, + { finalMessageId: null }, + { finalMessageId: 1 }, + { finalMessageId: "message-1", finalMessageText: "answer" }, + { finalMessageText: "answer" }, + ]) { + expect( + ingestRuntimeEventInput(createContext(), { kind: "run.completed", payload }), + ).toMatchObject({ + rejection: { code: "malformed_event", kind: "run.completed" }, + status: "rejected", + }); + } + }); + test("rejects permission requests without a canonical run owner", () => { const outcome = ingestRuntimeEventInput( { @@ -376,7 +674,7 @@ describe("runtime event ingress", () => { }); }); - test("admits Driver Contract v2 timing payloads with ISO timestamps", () => { + test("admits Driver Contract v3 timing payloads with ISO timestamps", () => { const event = first( toRuntimeEventInput(createContext(), { kind: "runtime.timing.recorded", @@ -465,7 +763,7 @@ describe("runtime event ingress", () => { payload: { message: "ok", }, - schemaVersion: "2026-05-26", + schemaVersion: "2026-08-29", sessionId: PLATFORM_ID_FIXTURES.session, visibility: "participant", }); @@ -511,4 +809,35 @@ describe("runtime event ingress", () => { status: "failed", }); }); + + test("hashes replay semantics independently of transport metadata and object key order", async () => { + const firstEvent = createRuntimeEvent({ + driverInstanceId: PLATFORM_ID_FIXTURES.driverInstance, + id: createPlatformId(), + kind: "diagnostic.reported", + occurredAt: "2026-08-29T00:00:00.000Z", + payload: { nested: { a: 1, b: 2 }, z: true }, + runtimeId: "runtime-1", + sessionId: PLATFORM_ID_FIXTURES.session, + sourceEventId: "source-1", + }); + const replay = createRuntimeEvent({ + driverInstanceId: PLATFORM_ID_FIXTURES.driverInstance, + id: createPlatformId(), + kind: "diagnostic.reported", + occurredAt: "2026-08-30T00:00:00.000Z", + payload: { z: true, nested: { b: 2, a: 1 } }, + runtimeId: "runtime-1", + sessionId: PLATFORM_ID_FIXTURES.session, + sourceEventId: "source-1", + }); + const changedRuntime = { ...replay, runtimeId: "runtime-2" }; + + expect(await createRuntimeEventSemanticHash(firstEvent)).toBe( + await createRuntimeEventSemanticHash(replay), + ); + expect(await createRuntimeEventSemanticHash(firstEvent)).not.toBe( + await createRuntimeEventSemanticHash(changedRuntime), + ); + }); }); diff --git a/pkgs/session-policy/package.json b/pkgs/session-policy/package.json index 8b33a0e0..ba9b9244 100644 --- a/pkgs/session-policy/package.json +++ b/pkgs/session-policy/package.json @@ -16,6 +16,6 @@ }, "devDependencies": { "typescript": "^6.0.3", - "vite-plus": "^0.1.23" + "vite-plus": "^0.3.0" } } diff --git a/pkgs/skill-package/package.json b/pkgs/skill-package/package.json index 6e3c6ec6..4dba4464 100644 --- a/pkgs/skill-package/package.json +++ b/pkgs/skill-package/package.json @@ -16,6 +16,6 @@ }, "devDependencies": { "typescript": "^6.0.3", - "vite-plus": "^0.1.23" + "vite-plus": "^0.3.0" } }